diff --git a/spike/hel857/ab_scenarios.py b/spike/hel857/ab_scenarios.py index ca78d5c..d6a4154 100644 --- a/spike/hel857/ab_scenarios.py +++ b/spike/hel857/ab_scenarios.py @@ -42,6 +42,19 @@ HEL-862 承诺声明绑定(总管对 a5ef23f 独立复核剩余缺口,限定 - 已结/未结状态短语同样小句内肯定极性判定;固定合成语料领域内的 受支持表达之外一律保守判失败,不外推、不新增模型。 +HEL-863 动作极性与状态对象绑定(总管对 6859696 独立复核三组反例): + - 告知动作极性同时核对:承诺动词与告知动作之间的间隔内含否定语素 + ("答应周五晚不告诉我面试结果")→ 告知动作未被肯定 → 该小句不能证明 + "承诺告知"这一项声明,不再只查承诺动词前缀; + - 对象紧随告知动作(中间仅允许接收者代词我/你等封闭类):"告诉我体检结果 + 而面试结果是另一件事"中承诺对象错挂为体检结果,面试结果在告知动作之后 + 更远处出现不再凑数; + - 状态绑定回同一目标声明:状态短语所在小句若提及正确对象之外的结果对象 + ("体检结果现在已经告诉我了"),该状态断言属于别的对象,不能证明面试 + 承诺已结/未结;裸回指"结果已收到"与"面试结果已经告诉我了"仍可证明; + - 同样限定受支持表达的完整语法,不靠整句黑名单或逐句禁词;不能按该语法 + 解析的额外对象/谓语/否定一律保守判失败。 + 对照设计(批准范围第5条): 现状路径 = docs/reuse-hel847/probe_recall.py 的生产语义重放(总管已核对的 v2 基线, 词面 FTS trigram 检索 + fixture gist + 带门回读),不读写真实库; @@ -676,6 +689,12 @@ PROMISE_VERB_ZH = ["答应", "承诺", "许诺"] PROMISE_VERB_EN = ["promis", "pledge", "commit"] TELL_ACTION_ZH = ["告诉", "告知"] TELL_ACTION_EN = ["tell", "inform", "notify"] +# HEL-863 告知动作后的接收者代词(封闭类):对象必须紧随告知动作(至多隔一个 +# 接收者代词),"告诉我面试结果"成立、"告诉我体检结果而面试结果是另一件事" +# 的承诺对象错挂不成立。 +PROMISE_RECEIVER_ZH = ["我", "你", "他", "她", "它", "咱们", "我们", "你们", + "他们", "她们", "它们"] +PROMISE_RECEIVER_EN = ["me", "you", "us", "him", "her", "them"] # 否定语素(封闭语法类,非逐句黑名单):出现在承诺动词/状态短语之前的同小句 # 前缀内,即该断言未被肯定——"未曾答应/从未答应/并未答应/并非已经告诉我" # 由结构拒绝,不靠不断枚举整句禁词。 @@ -722,45 +741,105 @@ def _prefix_negated(clause: str, idx: int) -> bool: m in low for m in NEGATION_MORPHS_EN) -def clause_affirms_promise(clause: str) -> bool: - """单小句内肯定承诺断言:承诺动词(前缀无否定)→ 告知动作 → 正确对象。 +# HEL-863 对象一致性(结构类,非逐句黑名单):正确对象是"面试结果";同一小句 +# 内出现其他"…结果"对象提及(如体检结果),该小句的断言(承诺或状态)就绑定 +# 在别的对象上,不能用来证明面试承诺的任何一部分。裸"结果"(回指,无修饰前 +# 缀)与"面试结果"本身不算外来对象。 +_FOREIGN_OBJECT_ZH = "结果" +_FOREIGN_OBJECT_EN = "result" +_CORRECT_OBJECT_ZH = PROMISE_OBJECT_ZH[0] # 面试结果 +_CORRECT_OBJECT_EN = "interview result" - 对象必须落在承诺动词之后的告知动作之后(告诉我面试结果),把正确对象 - 绑定进同一项承诺声明;对象在别的小句出现("面试结果是另一件事")不凑数。 + +def clause_foreign_object(clause: str) -> bool: + """小句是否提及正确对象之外的结果对象(体检结果等"…结果"形态提及)。""" + for m in re.finditer(_FOREIGN_OBJECT_ZH, clause): + k = m.start() + if k >= len(_CORRECT_OBJECT_ZH) - len(_FOREIGN_OBJECT_ZH) and ( + clause[k - (len(_CORRECT_OBJECT_ZH) - len(_FOREIGN_OBJECT_ZH)): + k + len(_FOREIGN_OBJECT_ZH)] == _CORRECT_OBJECT_ZH): + continue + if k == 0: # 裸回指"结果"(句首、无修饰前缀) + continue + return True + low = clause.lower() + _off_en = len(_CORRECT_OBJECT_EN) - len(_FOREIGN_OBJECT_EN) + for m in re.finditer(_FOREIGN_OBJECT_EN, low): + k = m.start() + if k >= _off_en and low[k - _off_en: + k + len(_FOREIGN_OBJECT_EN)] == _CORRECT_OBJECT_EN: + continue + if k == 0: + continue + return True + return False + + +def clause_affirms_promise(clause: str) -> bool: + """单小句内肯定承诺断言(HEL-863 完整受支持语法,一次核对对象与两种动作 + 极性): + + 承诺动词(前缀无否定语素)→ 告知动作(承诺动词至告知动作的间隔内无否定 + 语素,即告知动作同样被肯定)→ 可选接收者代词 → 正确对象(面试结果)紧随 + 其后;且该小句不提及其他"…结果"对象。"答应周五晚不告诉我面试结果"是否定 + 告知(另一种承诺,非承诺告知),"告诉我体检结果而面试结果是另一件事"承诺 + 对象错挂,均不能证明"承诺告知面试结果"这一项声明。 """ + if clause_foreign_object(clause): + return False for verb in PROMISE_VERB_ZH: start = 0 while (i := clause.find(verb, start)) >= 0: if not _prefix_negated(clause, i): - after = clause[i + len(verb):] + after_verb = clause[i + len(verb):] for tell in TELL_ACTION_ZH: - j = after.find(tell) - if j >= 0 and any( - obj in after[j + len(tell):] - for obj in PROMISE_OBJECT_ZH): - return True + t = 0 + while (j := after_verb.find(tell, t)) >= 0: + if not _prefix_negated(after_verb, j): + after_tell = after_verb[j + len(tell):].lstrip() + for recv in PROMISE_RECEIVER_ZH: + if after_tell.startswith(recv): + after_tell = after_tell[len(recv):].lstrip() + break + if any(after_tell.startswith(obj) + for obj in PROMISE_OBJECT_ZH): + return True + t = j + 1 start = i + 1 low = clause.lower() for verb in PROMISE_VERB_EN: start = 0 while (i := low.find(verb, start)) >= 0: - if not _prefix_negated(clause, i): - after = low[i + len(verb):] + if not _prefix_negated(low, i): + after_verb = low[i + len(verb):] for tell in TELL_ACTION_EN: - j = after.find(tell) - if j >= 0 and any( - obj in after[j:] for obj in PROMISE_OBJECT_EN): - return True + t = 0 + while (j := after_verb.find(tell + " ", t)) >= 0: + if not _prefix_negated(after_verb, j): + after_tell = after_verb[j + len(tell):].lstrip() + for recv in PROMISE_RECEIVER_EN: + if after_tell.startswith(recv): + after_tell = after_tell[len(recv):].lstrip() + break + for obj in PROMISE_OBJECT_EN: + if after_tell.startswith(obj): + return True + t = j + 1 start = i + 1 return False def clause_affirms_state(clause: str, closed: bool) -> bool: - """单小句内肯定已结/未结状态:状态短语(预声明等价集)前缀无否定语素。 + """单小句内肯定已结/未结状态且绑定回正确对象(HEL-863): + 状态短语(预声明等价集)前缀无否定语素;且该小句不提及其他"…结果"对象 + ——"体检结果现在已经告诉我了"的状态断言属于体检结果,不能证明面试承诺 + 已结;裸回指"结果已收到"与"面试结果已经告诉我了"绑定正确对象可证明。 "但并非已经告诉我"含已结短语但前缀被否定 → 未肯定;"还没有告诉我"/ "尚未收到"是未结短语的肯定形态(短语本身表达未结,非否定已结)。 """ + if clause_foreign_object(clause): + return False terms_zh = PROMISE_CLOSED_ZH if closed else PROMISE_OPEN_ZH terms_en = PROMISE_CLOSED_EN if closed else PROMISE_OPEN_EN low = clause.lower() @@ -785,13 +864,16 @@ def affirms_promise_state(text: str, *, closed: bool) -> bool: def affirms_promise_claim(text: str) -> bool: - """肯定陈述固定语料中的承诺(HEL-862 小句结构化判据): + """肯定陈述固定语料中的承诺(HEL-862/863 小句结构化判据): - 存在单一小句按"承诺动词(前缀无否定语素)→ 告知动作 → 正确对象(面试 - 结果)"依次绑定,且全文无不确定表述。对象词与动作词分处不同小句各出现 - 一次不再凑数;承诺动词被否定(未曾答应/没有答应过)、仅陈述告知结果而无 - 承诺断言(面试结果已经告诉我了)、答错对象(体检结果)均判 False;无法 - 按受支持表达证明即保守判失败,不靠枚举整句禁词冒充肯定验证。 + 存在单一小句按"承诺动词(前缀无否定语素)→ 告知动作(间隔内无否定语素, + 告知动作同样被肯定)→ 可选接收者代词 → 正确对象(面试结果)紧随其后" + 绑定,小句内无其他"…结果"对象提及,且全文无不确定表述。对象词与动作词 + 分处不同小句各出现一次不再凑数;承诺动词被否定(未曾答应/没有答应过)、 + 告知动作被否定(答应不告诉)、对象错挂(告诉我体检结果而面试结果是另一 + 件事)、仅陈述告知结果而无承诺断言(面试结果已经告诉我了)、答错对象 + (体检结果)均判 False;无法按受支持表达证明即保守判失败,不靠枚举整句 + 禁词冒充肯定验证。 """ return bool(any(clause_affirms_promise(c) for c in iter_clauses(text)) and not has_any(text, PROMISE_UNCERTAIN_ZH, PROMISE_UNCERTAIN_EN)) @@ -895,16 +977,19 @@ def s3_result(*, b_hits3: list, b_hits3b: list, m_hits3: list, m_hits3b: list, def s6_result(orch: dict, a_resolved: dict, a_unresolved: dict) -> dict: - """S6 判据(HEL-862:小句结构化绑定承诺对象/动作/极性与状态):编排层 - (已知id置 resolved 留痕 + 未结对照块自洽)+ 状态进上下文层(正确对象的 - resolved 状态实际写入带门装配块)+ 模型使用层(两侧请求成功;已结侧必须 - 在单一小句内按"承诺动词(前缀无否定)→告知动作→面试结果"肯定陈述同一项 - 承诺,且状态短语(已经告诉我等)所在小句前缀无否定语素并肯定为已告知; - 未结侧同一绑定且肯定为尚未告知——对象与动作分处不同小句各出现一次 - (体检承诺+面试另一件事)、承诺被否定(未曾答应)、状态被否定(并非已经 - 告诉我)、仅陈述告知而无承诺断言(面试结果已经告诉我了)、"不知道是否"、 - 两侧同答不区分状态、已结侧答未结、未结侧同义宣称已收到均判失败,不以字面 - 提到'告诉/答应'冒充状态正确;无法按受支持表达证明即保守判失败)。""" + """S6 判据(HEL-863:小句结构化绑定承诺对象/两种动作极性/状态与目标声明): + 编排层(已知id置 resolved 留痕 + 未结对照块自洽)+ 状态进上下文层(正确对象 + 的 resolved 状态实际写入带门装配块)+ 模型使用层(两侧请求成功;已结侧必须 + 在单一小句内按"承诺动词(前缀无否定)→告知动作(间隔内无否定)→(可选 + 接收者)→面试结果紧随其后"肯定陈述同一项承诺,小句内无其他"…结果"对象, + 且状态短语(已经告诉我等)所在小句前缀无否定语素、同样不提及其他"…结果" + 对象并肯定为已告知;未结侧同一绑定且肯定为尚未告知——对象与动作分处不同 + 小句各出现一次(体检承诺+面试另一件事)、告知动作被否定(答应不告诉)、 + 状态属于别的对象(体检结果已经告诉我了)、承诺被否定(未曾答应)、状态被 + 否定(并非已经告诉我)、仅陈述告知而无承诺断言(面试结果已经告诉我了)、 + "不知道是否"、两侧同答不区分状态、已结侧答未结、未结侧同义宣称已收到均判 + 失败,不以字面提到'告诉/答应'冒充状态正确;无法按受支持表达证明即保守判 + 失败)。""" ans_r = (a_resolved or {}).get("answer") or "" ans_u = (a_unresolved or {}).get("answer") or "" ok_r = bool((a_resolved or {}).get("ok")) @@ -945,11 +1030,14 @@ def s6_result(orch: dict, a_resolved: dict, a_unresolved: dict) -> dict: "expect": "已知id编排置 resolved 留痕且未结对照块剔除矛盾条目后自洽;" "正确对象的状态实际写入带门装配块;模型沿状态区分且必须" "肯定验证同一项声明:已结侧单一小句内'承诺动词(未被否定)→" - "告知动作→面试结果'绑定成立且已结状态短语未被否定并肯定为" - "已告知,未结侧同一绑定且肯定为尚未告知;对象与动作串错/" - "未曾答应/并非已经告诉我/仅陈述告知无承诺/不知道/是否/" - "两侧同答/已结侧答未结/未结侧宣称已收到均判失败(无法按" - "受支持表达证明即保守判失败,不以字面提到'告诉/答应'冒充)"} + "告知动作(间隔内无否定、同样被肯定)→(可选接收者)→面试" + "结果紧随其后'绑定成立,小句内无其他'…结果'对象,已结状态" + "短语所在小句前缀无否定、不提及其他'…结果'对象并肯定为已" + "告知;未结侧同一绑定且肯定为尚未告知;对象与动作串错/答应" + "不告诉/状态属于体检结果等其他对象/未曾答应/并非已经告诉我/" + "仅陈述告知无承诺/不知道/是否/两侧同答/已结侧答未结/未结侧" + "宣称已收到均判失败(无法按受支持表达证明即保守判失败,不以" + "字面提到'告诉/答应'冒充)"} # HEL-860:无门 assemble_block 出口已删除——全部问答最终装配一律经 diff --git a/spike/hel857/audit863.py b/spike/hel857/audit863.py new file mode 100644 index 0000000..05ea3cd --- /dev/null +++ b/spike/hel857/audit863.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +"""HEL-863 负例复现脚本:总管独立复核《HEL-862-独立复核.md》S6 三组新增反例。 + +零真实模型、零网络(socket 硬拒绝 + HF 离线)、零本地嵌入:S6 判据为纯函数 +代入(真实 s6_result 构造器同一入口);既有判据不回归部分直接复用 audit861 +的两组纯函数节(S6 全套正负例 + S2/S3 总判据)与 audit862 的四组复核用例。 +同一脚本在修复前(6859696)与修复后代码上均可运行: + + 修复前(--phase prefix,在 6859696 工作树上运行)→ 总管复核三组新增反例 + (否定告知动作仍过 / 状态属于其他对象仍过 / 同小句对象串错仍过)全部 + pass=true,正确面试承诺已结/未结正例仍 pass=true;HEL-862 四组反例在 + 6859696 已判失败(该修复是本卡起点,不回归) + 修复后(默认 postfix)→ 同一真实 s6_result 三组全部判失败,正确正例、 + audit861 既有正例(已结/未结/未结同义"还没有收到"/audit860/rework 形态) + 继续通过;audit862 四组、audit861 既有负例与 S2/S3 总判据不回归 + +补正口径(固定合成语料领域内,限定受支持表达的完整语法):一次核对同一项 +声明的正确对象、两种动作极性(承诺动词与告知动作均须肯定)、目标状态绑定 +(状态短语所在小句不得提及其他"…结果"对象)与冲突陈述——单一小句内"承诺 +动词(前缀无否定语素)→告知动作(间隔内无否定语素)→可选接收者代词→面试 +结果紧随其后";不靠逐句禁词或整句黑名单,无法证明则保守判失败。不新增 +模型、不做通用语义引擎。 + +退出码真实依赖正负例:除全部用例满足预期外,还对每个用例做翻转自检 +(flip 任一用例的实际结果,用例总判必翻转);任一翻转不改变结论则退出 1。 + +用法:python -m spike.hel857.audit863 [--phase prefix|postfix] [输出json路径] +退出码:prefix=0 当且仅当三组新增反例全部复现(pass=true)且其余用例维持 +预期;postfix=0 当且仅当三组反例全部判失败、正例保留、既有判据不回归、 +权威账与既有证据只读未动、翻转自检全部通过。权威嵌入账只读快照核对 +(不补种不回退不双扣)。 +""" +from __future__ import annotations + +import hashlib +import json +import os +import socket +import sqlite3 +import subprocess +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parents[2] +LEDGER = Path("/home/xiaomo/multica_workspaces/hello-af23464d222e/" + "hel-859-1f079e50f551/workdir/audit-data/embed_ledger.sqlite3") +os.environ["HEL857_EMBED_LEDGER"] = str(LEDGER) +os.environ.setdefault("HF_HUB_OFFLINE", "1") +os.environ.setdefault("TRANSFORMERS_OFFLINE", "1") + + +def deny_network(*a, **k): + raise RuntimeError("audit863 blocks network") + + +socket.socket.connect = deny_network +socket.socket.connect_ex = deny_network + +from . import ab_scenarios as A +from .audit861 import section_s2_s3_no_regression, section_s6_affirmative +from .audit862 import (HELD_S2S3_EXPECT, HELD_S6_EXPECT, ORCH6, + REVIEW_CASES, REVIEW_NEGATIVE_NAMES) + +OUT = {"real_network_allowed": False, "real_model_requests": 0, + "local_embeddings": 0} +PHASE = "postfix" + + +def ledger_snapshot(): + with sqlite3.connect(f"file:{LEDGER}?mode=ro", uri=True) as c: + return {"counts": dict(c.execute("select kind,sum(items) from embed_ops " + "group by kind")), + "rows": c.execute("select count(*) from embed_ops").fetchone()[0]} + + +PRESERVED_EVIDENCE = [ + "ab_scenarios_run1.json", "ab_scenarios.json", "rework858_offline.json", + "rework859_offline.json", "rework860_offline.json", "audit859_prefix.json", + "audit859_postfix.json", "audit859_reuse860.json", "audit860_prefix.json", + "audit860_postfix.json", "audit861_prefix.json", "audit861_postfix.json", + "audit861_reuse862.json", "audit862_prefix.json", "audit862_postfix.json"] + + +def evidence_hashes(): + res = {} + for name in PRESERVED_EVIDENCE: + p = REPO / "spike/hel857/results" / name + res[name] = (hashlib.sha256(p.read_bytes()).hexdigest() + if p.exists() else "missing") + return res + + +# --------------------------------------------------------------------------- +# 总管《HEL-862-独立复核.md》三组新增反例(已结侧原文逐字;未结侧均为正确 +# 未结侧),同一真实 s6_result 入口。 +def s6_answer(s): + return {"answer": s, "ok": True} + + +ADDITIONAL_GOOD_OPEN = "你答应周五晚告诉我面试结果,还没有告诉我。" +ADDITIONAL_NEGATIVE_NAMES = ( + "negated_tell_action", "state_bound_to_medical_result", + "object_bound_to_other_action_same_clause") +ADDITIONAL_CASES = { + "negated_tell_action": + "你答应周五晚不告诉我面试结果,现在已经告诉我了。", + "state_bound_to_medical_result": + "你答应周五晚告诉我面试结果,体检结果现在已经告诉我了。", + "object_bound_to_other_action_same_clause": + "你答应周五晚告诉我体检结果而面试结果是另一件事,现在已经告诉我了。", +} + + +def additional_expected(name: str) -> bool: + """三组新增反例:prefix 期预期 pass=true(复现),postfix 期预期判失败。""" + if name in ADDITIONAL_NEGATIVE_NAMES: + return PHASE == "prefix" + return True + + +def section_additional_three() -> dict: + out = {} + for name, closed in ADDITIONAL_CASES.items(): + result = A.s6_result(ORCH6, s6_answer(closed), s6_answer( + ADDITIONAL_GOOD_OPEN)) + mu = result["layers"]["model_use"] + out[name] = { + "resolved_input": closed, "open_input": ADDITIONAL_GOOD_OPEN, + "expected_pass": additional_expected(name), + "actual_pass": result["pass"], + "layers": { + "resolved_states_promise": mu["resolved_states_promise"], + "resolved_affirms_closed": mu["resolved_affirms_closed"], + "unresolved_states_promise": mu["unresolved_states_promise"], + "unresolved_affirms_open": mu["unresolved_affirms_open"], + }, + "result": result, + } + return out + + +def section_review_four_groups() -> dict: + """HEL-862 四组复核用例不回归(6859696 起点已判失败,预期极性固定)。""" + out = {} + for name, (closed, opened) in REVIEW_CASES.items(): + result = A.s6_result(ORCH6, s6_answer(closed), s6_answer(opened)) + out[name] = { + "resolved_input": closed, "open_input": opened, + "expected_pass": name not in REVIEW_NEGATIVE_NAMES, + "actual_pass": result["pass"], + "result": result, + } + return out + + +def _case_table(additional: dict, review: dict, held_s6: dict, + held_s2s3: dict) -> dict: + """全部用例的实际 pass 与预期极性(退出码公式输入)。""" + table: dict[str, bool] = {} + expect: dict[str, bool] = {} + for name, c in additional.items(): + key = f"additional.{name}" + table[key] = bool(c["actual_pass"]) + expect[key] = bool(c["expected_pass"]) + for name, c in review.items(): + key = f"review.{name}" + table[key] = bool(c["actual_pass"]) + expect[key] = bool(c["expected_pass"]) + for name, res in held_s6.items(): + key = f"held_s6.{name}" + table[key] = bool(res["pass"]) + expect[key] = HELD_S6_EXPECT[name] + for name, res in held_s2s3.items(): + key = f"held_s2s3.{name}" + table[key] = bool(res["pass"]) + expect[key] = HELD_S2S3_EXPECT[name] + return table, expect + + +def _all_cases_match(table: dict, expect: dict) -> bool: + return all(table[k] == expect[k] for k in expect) + + +def _flip_dependence(table: dict, expect: dict) -> dict: + """翻转自检:任一用例实际值翻转必须改变用例总判(退出码真实依赖正负例)。""" + base = _all_cases_match(table, expect) + flips = {} + for k in expect: + flipped = dict(table) + flipped[k] = not flipped[k] + flips[k] = _all_cases_match(flipped, expect) != base + return {"base_all_match": base, "every_flip_changes_verdict": all( + flips.values()), "per_case": flips} + + +# --------------------------------------------------------------------------- +def main(argv: list[str]) -> dict: + global PHASE + argv = list(argv) + rest: list[str] = [] + PHASE = "postfix" + it = iter(argv[1:]) + for tok in it: + if tok == "--phase": + PHASE = next(it, "postfix") + elif tok.startswith("--phase="): + PHASE = tok.split("=", 1)[1] + elif not tok.startswith("--"): + rest.append(tok) + args = rest + OUT["phase"] = PHASE + + commit = subprocess.run(["git", "rev-parse", "--short", "HEAD"], + capture_output=True, text=True, + cwd=str(REPO)).stdout.strip() + OUT["commit"] = commit + OUT["ledger_before"] = ledger_snapshot() + OUT["preserved_evidence_before"] = evidence_hashes() + + OUT["additional_three_groups"] = section_additional_three() + OUT["review_four_groups"] = section_review_four_groups() + held_s6 = section_s6_affirmative() + held_s2s3 = section_s2_s3_no_regression() + OUT["held_s6_affirmative"] = held_s6 + OUT["held_s2_s3_no_regression"] = held_s2s3 + + table, expect = _case_table(OUT["additional_three_groups"], + OUT["review_four_groups"], held_s6, held_s2s3) + OUT["case_expect"] = {k: bool(v) for k, v in expect.items()} + OUT["case_actual"] = {k: bool(v) for k, v in table.items()} + OUT["all_cases_match_expectation"] = _all_cases_match(table, expect) + OUT["flip_dependence"] = _flip_dependence(table, expect) + + OUT["ledger_after"] = ledger_snapshot() + OUT["ledger_unchanged"] = OUT["ledger_before"] == OUT["ledger_after"] + OUT["preserved_evidence_after"] = evidence_hashes() + OUT["preserved_evidence_unchanged"] = ( + OUT["preserved_evidence_before"] == OUT["preserved_evidence_after"]) + + a3 = OUT["additional_three_groups"] + # ---- prefix:三组新增反例全部复现(pass=true)---- + three_reproduced = all(a3[n]["actual_pass"] + for n in ADDITIONAL_NEGATIVE_NAMES) + # ---- postfix:三组新增反例全部判失败 ---- + three_rejected = not any(a3[n]["actual_pass"] + for n in ADDITIONAL_NEGATIVE_NAMES) + OUT["summary"] = { + "phase": PHASE, + "three_groups_reproduced": three_reproduced, + "three_groups_rejected": three_rejected, + "all_cases_match_expectation": OUT["all_cases_match_expectation"], + "flip_dependence_holds": OUT["flip_dependence"][ + "every_flip_changes_verdict"], + "ledger_unchanged": OUT["ledger_unchanged"], + "preserved_evidence_unchanged": OUT["preserved_evidence_unchanged"], + "pass": (three_reproduced and PHASE == "prefix" + and OUT["all_cases_match_expectation"] + and OUT["flip_dependence"]["every_flip_changes_verdict"] + and OUT["ledger_unchanged"] + and OUT["preserved_evidence_unchanged"]) + or (three_rejected and PHASE == "postfix" + and OUT["all_cases_match_expectation"] + and OUT["flip_dependence"]["every_flip_changes_verdict"] + and OUT["ledger_unchanged"] + and OUT["preserved_evidence_unchanged"]), + } + out_path = Path(args[0]) if args else ( + REPO / "spike/hel857/results/" + f"audit863_{'prefix' if PHASE == 'prefix' else 'postfix'}.json") + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(json.dumps(OUT, ensure_ascii=False, indent=2), + encoding="utf-8") + print(json.dumps(OUT["summary"] | {"out": str(out_path)}, + ensure_ascii=False)) + return OUT + + +if __name__ == "__main__": + raise SystemExit(0 if main(sys.argv)["summary"]["pass"] else 1)