HEL-865: bind same-promise roles (promisor/receiver/action/object/status) to the one scene declaration - per manager independent review of 070f4fd: parse_supported_answer now captures and validates role slots per clause instead of discarding subject/receiver after regex consumption; pre-declared scene target from p1 user quote (promisor=user '你/you', told-receiver=companion '我/me', object=interview result) with declared anaphora for omitted slots (promise clause omitted subject->promisor, omitted receiver->scene receiver; state clauses by verb class: tell-verb subject=promisor, receive-verb recipient=scene receiver with open-side receive subject compatible with both receiver-not-told and promisor-not-received readings, fulfill-verb subject=promisor); explicit different recipient (现在已经告诉你了/told them), promise to someone else (告诉她/her), reversed promisor (我答应/我承诺/I promised...), multi-clause role conflicts and unknown roles (we/我们/他) all fail conservatively; canonical positives incl. 我还没有收到 and omission-inheritance forms kept passing; no eight-sentence or pronoun blacklist, no new model, no generic NLU engine, supported surface unchanged (EN closed-receive subject slot still excludes promisor pronouns)

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
zonggong
2026-10-26 02:54:40 +08:00
co-authored by multica-agent
parent 070f4fd89b
commit c0cb540c9f
2 changed files with 717 additions and 66 deletions
+282 -66
View File
@@ -72,6 +72,27 @@ HEL-864 完整受支持语法全量解析(总管对 7aa9280 独立复核七组
- 不加七句黑名单、不补"报告/吗/假"禁词、不新增模型、不做通用语义
引擎;只约束本实验判据,不限制产品表达。
HEL-865 同一承诺是谁答应又告诉了谁(总管对 070f4fd 独立复核八组
"同一承诺串人"反例):全量解析只产出 promise/closed/open 类型、主语/
接收者被正则消费后丢弃,"某承诺+某已结/未结状态"不能证明属于本场景
同一项承诺声明。本轮只修受支持语法的角色与声明绑定:
- 场景目标(预声明):本合成场景唯一被验收的承诺声明是 p1 用户原话
"面试结果出来我周五晚上告诉你"——承诺者=用户、告知接收者=伴侣、
对象=面试结果;答案框架(验证器/伴侣作答)内承诺者="你"/"you"、
接收者="我"/"me";
- 每个小句捕获并核准角色槽(承诺者主语、告知接收者、动作、对象、
状态),全部绑定同一项场景声明;跨小句状态承前省略按预声明回指
规则继承明确目标(承诺小句省略主语→承诺者、省略接收者→场景接收者;
状态小句按动词类别:tell 类主语=承诺者、receive 类受收方=场景接收者
〔未结 receive 主语兼容接收者未收到告知/承诺者未收到结果两读法〕、
fulfill 类主语=承诺者);
- 明确不同接收者("现在已经告诉你了"/"还没有告诉你"/told them)、
承诺指向他人("告诉她")、承诺方反转("我答应周五晚告诉你")、多个
P/C/O 小句角色冲突、未知角色(we/我们/他/她等)一律失败;
- "我还没有收到"等已接受等价表达的回指规则按上条预声明解释保留;
中英文声明支持即同等核验;不加八句/人称禁词黑名单,不新增自然语言
引擎,不重做实验,不限制产品表达。
对照设计(批准范围第5条):
现状路径 = docs/reuse-hel847/probe_recall.py 的生产语义重放(总管已核对的 v2 基线,
词面 FTS trigram 检索 + fixture gist + 带门回读),不读写真实库;
@@ -752,95 +773,281 @@ def iter_clauses(text: str) -> list[str]:
# 肯定验证=全量解析:整个回答的每个小句必须被且仅被 P/C/O 三类结构完整消费
# (fullmatch);语气必须为陈述;任何无法消费的额外小句、对象、谓语、条件、
# 疑问、否认或冲突一律保守判失败(fail-closed)。词表均取上方预声明闭类。
#
# HEL-865 场景角色与同一声明绑定(预声明回指规则,总管对 070f4fd 独立复核
# 八组"同一承诺串人"反例的补正):全量解析不再只产出 promise/closed/open 三
# 类型——每个小句同时捕获并核准角色槽(承诺者主语、告知接收者、动作、对象、
# 状态),全部绑定到本合成场景唯一被验收的那项承诺声明。场景目标来自 p1
# 用户原话"面试结果出来我周五晚上告诉你":承诺者=用户,告知接收者=伴侣,
# 对象=面试结果。答案由验证器/伴侣一侧作答,故答案框架内:承诺者="你"/
# "you",接收者="我"/"me"。
SCENE_PROMISOR = "user" # 承诺者(答案框架"你"/"you")
SCENE_RECEIVER = "companion" # 告知接收者(答案框架"我"/"me")
SCENE_OBJECT_ZH = "面试结果"
SCENE_OBJECT_EN = "interview result"
# 答案框架代词 → 场景角色(封闭类)。映射不到场景角色(他/她/咱们/我们/
# 你们/them/we 等=他人或不明)的显式槽位不绑定 → 整篇保守失败(未知也失败)。
_PRONOUN_ROLE_ZH = {"你": SCENE_PROMISOR, "我": SCENE_RECEIVER}
_PRONOUN_ROLE_EN = {"you": SCENE_PROMISOR, "me": SCENE_RECEIVER,
"i": SCENE_RECEIVER}
_ZH_PRONOUN_ALT = "|".join(PROMISE_RECEIVER_ZH)
# P 承诺小句:主语?→承诺动词→体标记?→时间(周五晚/周五)?→告知动作→
# 接收者代词?→正确对象(面试结果),小句到此为止(尾随残料即失败——
# "告诉我体检结果""答应不告诉""如果/未曾答应"等均无法完整匹配)。
# HEL-865:主语/接收者改为命名捕获并绑定场景角色——主语必须是承诺者
# (你/you,省略继承),接收者必须是场景接收者(我/me,省略继承);
# "我答应周五晚告诉你"(承诺方反转)与"答应告诉她"(承诺指向他人)
# 均属另一项声明,不绑定本场景目标 → 失败。
_PROMISE_CLAUSE_ZH = re.compile(
r"(?:你|我)?(?:答应|承诺|许诺)(?:过|了)?(?:周五晚|周五)?"
rf"(?:告诉|告知)(?:{_ZH_PRONOUN_ALT})?面试结果")
rf"(?P<subject>你|我)?(?:答应|承诺|许诺)(?:过|了)?(?:周五晚|周五)?"
rf"(?:告诉|告知)(?P<receiver>{_ZH_PRONOUN_ALT})?面试结果")
# C 已结状态小句:正确对象/裸回指?→现在?→已经/已?→告知动作→接收者?→了;
# 或 正确对象/裸回指?→现在?→已经/已?→收到/拿到/兑现/解决→了?。
# 主语槽只认 面试结果/裸"结果"——"体检报告现在已经告诉我了"(外来对象无
# "结果"后缀)与"现在已经告诉我了体检情况"(状态后置对象)均无法匹配。
# HEL-865:tell 类接收者槽绑定场景接收者——"现在已经告诉你了"(告知的
# 是用户本人)不能证明已告知伴侣;receive 类受收方=场景接收者(省略/话题
# 形态继承,无显式角色槽)。
_CLOSED_STATE_ZH = tuple(re.compile(p) for p in (
rf"(?:面试结果|结果)?(?:现在)?(?:已经|已)?(?:告诉|告知)(?:我|你)?了",
rf"(?:面试结果|结果)?(?:现在)?(?:已经|已)?(?:收到|拿到|兑现|解决)(?:了)?"))
rf"(?:面试结果|结果)?(?:现在)?(?:已经|已)?(?P<verb>告诉|告知)"
rf"(?P<receiver>我|你)?了",
rf"(?:面试结果|结果)?(?:现在)?(?:已经|已)?(?P<verb>收到|拿到|兑现|解决)"
rf"(?:了)?"))
# O 未结状态小句:正确对象/裸回指?→现在?→主语我?→还没有/还没/尚未→
# 告知/收到/拿到/兑现/说→接收者?。
# HEL-865 按动词类别绑定主语:tell 类(告诉/告知/说)主语=承诺者(省略
# 继承"你");receive 类(收到/拿到)主语=场景接收者("我还没有收到",
# 省略兼容两读法);fulfill 类(兑现)主语=承诺者(兑现承诺的一方)。
# 接收者槽一律必须是场景接收者(我,省略继承)——"还没有告诉你"不能
# 证明尚未告知"我"。
_OPEN_STATE_ZH = re.compile(
rf"(?:面试结果|结果)?(?:现在)?(?:我)?(?:还没有|还没|尚未)"
rf"(?:告诉|告知|收到|拿到|兑现|说)(?:我|你)?呢?")
rf"(?:面试结果|结果)?(?:现在)?(?P<subject>我)?(?:还没有|还没|尚未)"
rf"(?P<verb>告诉|告知|收到|拿到|兑现|说)(?P<receiver>我|你)?呢?")
# EN 同等结构(大小写不敏感;句末允许 .!?;条件 if/unless 前缀与疑问天然
# 无法完整匹配,另由语气检查整篇排除 ?/?)。
# 无法完整匹配,另由语气检查整篇排除 ?/?)。HEL-865:主语/接收者命名
# 捕获并同规则绑定。
_PROMISE_CLAUSE_EN = re.compile(
r"(?:you|i|we)?\s*(?:promised|pledged|have promised|has promised|"
r"(?P<subject>you|i|we)?\s*(?:promised|pledged|have promised|has promised|"
r"committed to)\s*(?:to\s+)?(?:tell|inform|notify)\s*"
r"(?:me|you|us|him|her|them)?\s*(?:about\s+)?(?:the\s+)?"
r"(?P<receiver>me|you|us|him|her|them)?\s*(?:about\s+)?(?:the\s+)?"
r"(?:interview result|interview results|interview)"
r"(?:\s*[.!?])?", re.IGNORECASE)
_CLOSED_STATE_EN = tuple(re.compile(
p + r"(?:\s*[.!?])?", re.IGNORECASE) for p in (
r"(?:the interview result|the result|i|we|you)?\s*(?:have|has)?\s*"
r"(?:already\s+)?(?:told|informed|notified)\s*(?:me|you|us|them)?\s*"
r"(?P<subject>the interview result|the result|i|we|you)?\s*(?:have|has)?\s*"
r"(?:already\s+)?(?P<verb>told|informed|notified)\s*"
r"(?P<receiver>me|you|us|them)?\s*"
r"(?:about\s+it|it|the interview result|interview result)?",
r"(?:the interview result|the result|i|we)?\s*(?:have|has)?\s*"
r"(?:already\s+)?(?:received|got)\s*"
r"(?P<subject>the interview result|the result|i|we)?\s*(?:have|has)?\s*"
r"(?:already\s+)?(?P<verb>received|got)\s*"
r"(?:the interview result|the result|it|the notification)?"))
_OPEN_STATE_EN = re.compile(
r"(?:you|i|we)?\s*(?:haven't|have not|hasn't|has not)\s*(?:yet\s*)?"
r"(?:told|informed|notified|received|got)\s*"
r"(?:me|you|us|them)?\s*"
r"(?P<subject>you|i|we)?\s*(?:haven't|have not|hasn't|has not)\s*(?:yet\s*)?"
r"(?P<verb>told|informed|notified|received|got)\s*"
r"(?P<receiver>me|you|us|them)?\s*"
r"(?:it|the result|the interview result|the notification)?\s*(?:yet)?"
r"(?:\s*[.!?])?", re.IGNORECASE)
# 语气(封闭类,非逐句黑名单):问号或句末疑问/祈使语气词 → 非陈述语气。
_QUESTION_MARK_CHARS = ("?", "?")
_ZH_MOOD_PARTICLES = "吗呢吧嘛呗"
# 动词类别(角色绑定用,预声明闭类):tell=告知动作(主语=承诺者、
# 接收者=场景接收者);receive=收到/拿到(已结受收方=场景接收者;未结主语
# 可为接收者〔未收到告知〕或承诺者〔未收到结果、告知前提未成立〕);
# fulfill=兑现(主语=承诺者)。
_TELL_VERBS_ZH = ("告诉", "告知", "说")
_TELL_VERBS_EN = ("told", "informed", "notified")
_RECEIVE_VERBS_ZH = ("收到", "拿到")
_RECEIVE_VERBS_EN = ("received", "got")
def _pronoun_role(pronoun: str | None, table: dict) -> str | None:
"""答案框架代词 → 场景角色;省略返回 "omitted",映射不到返回 None。"""
if pronoun is None:
return "omitted"
return table.get(pronoun)
def _en_pronoun_role(pronoun: str | None) -> str | None:
return _pronoun_role(None if pronoun is None else pronoun.lower(),
_PRONOUN_ROLE_EN)
def _bind_promise_clause(m) -> dict:
"""P 承诺小句角色绑定:主语槽→承诺者,接收者槽→场景接收者。
省略继承(预声明回指规则:承诺小句省略主语=场景承诺者"你/you"作出该
承诺;省略接收者=告知场景接收者"我/me")。显式主语反转承诺方(我/i
作承诺者)、显式接收者指向他人(你/他/她/them 等)或未知(we/我们),
都是与场景目标不同的另一项声明 → bound=False。
"""
is_en = m.re is _PROMISE_CLAUSE_EN
subj = m.group("subject")
recv = m.group("receiver")
s_role = (_en_pronoun_role(subj) if is_en
else _pronoun_role(subj, _PRONOUN_ROLE_ZH))
r_role = (_en_pronoun_role(recv) if is_en
else _pronoun_role(recv, _PRONOUN_ROLE_ZH))
return {
"kind": "promise",
"subject": subj or "", "subject_role": s_role,
"receiver": recv or "", "receiver_role": r_role,
"verb": "promise", "object": SCENE_OBJECT_EN if is_en else SCENE_OBJECT_ZH,
"bound": bool(s_role in ("omitted", SCENE_PROMISOR)
and r_role in ("omitted", SCENE_RECEIVER)),
}
def _bind_state_clause(m, kind: str, is_en: bool) -> dict:
"""C/O 状态小句角色绑定(按动词类别):
- tell 类(告诉/告知/说;told/informed/notified):主语=承诺者(省略
继承——状态小句承前省略主语即承诺者"你/you"),接收者=场景接收者;
- receive 类(收到/拿到;received/got):已结——受收方=场景接收者
(主语只认映射为接收者的代词或省略;承诺者"收到结果"不证明已告知,
EN 已结受收主语槽本身不含承诺者代词,we/对象主语不绑定);未结——
主语可为接收者(未收到告知)或承诺者(未收到结果、告知前提未成立),
省略兼容两读法均证未结;
- fulfill 类(兑现):主语=承诺者(省略继承),其余不绑定。
接收者槽(me/我)一律必须是场景接收者或省略。显式角色与场景目标冲突、
他人或未知 → bound=False(与承诺小句不属于同一项声明)。
"""
gd = m.groupdict()
subj = gd.get("subject")
recv = gd.get("receiver")
verb = gd.get("verb") or ""
verb_low = verb.lower()
s_role = (_en_pronoun_role(subj) if is_en
else _pronoun_role(subj, _PRONOUN_ROLE_ZH))
r_role = (_en_pronoun_role(recv) if is_en
else _pronoun_role(recv, _PRONOUN_ROLE_ZH))
recv_ok = r_role in ("omitted", SCENE_RECEIVER)
tell = verb_low in _TELL_VERBS_EN if is_en else verb in _TELL_VERBS_ZH
receive = (verb_low in _RECEIVE_VERBS_EN if is_en
else verb in _RECEIVE_VERBS_ZH)
if tell:
subj_ok = s_role in ("omitted", SCENE_PROMISOR)
elif receive:
if kind == "closed":
subj_ok = s_role in ("omitted", SCENE_RECEIVER)
else:
subj_ok = s_role in ("omitted", SCENE_RECEIVER, SCENE_PROMISOR)
else: # fulfill(兑现/解决)——兑现承诺的一方是承诺者
subj_ok = s_role in ("omitted", SCENE_PROMISOR)
return {
"kind": kind,
"subject": subj or "", "subject_role": s_role,
"receiver": recv or "", "receiver_role": r_role,
"verb": verb, "bound": bool(subj_ok and recv_ok),
}
def _parse_clause(clause: str) -> dict:
"""单个小句 → 结构类型 + 角色槽绑定结果。
返回 {"text", "kind", "roles", "bound"};kind=None=无法按受支持结构
完整消费;kind 非 None 而 bound=False=语法可消费但角色槽与场景目标
冲突(他人/反转/未知)。两者都使整篇 all_consumed=False(保守失败),
区分记录以便审计复现。
"""
m = _PROMISE_CLAUSE_ZH.fullmatch(clause)
if m:
out = _bind_promise_clause(m)
else:
m = _PROMISE_CLAUSE_EN.fullmatch(clause)
if m:
out = _bind_promise_clause(m)
else:
for r in _CLOSED_STATE_ZH:
m = r.fullmatch(clause)
if m:
out = _bind_state_clause(m, "closed", False)
break
else:
for r in _CLOSED_STATE_EN:
m = r.fullmatch(clause)
if m:
out = _bind_state_clause(m, "closed", True)
break
else:
m = _OPEN_STATE_ZH.fullmatch(clause)
if m:
out = _bind_state_clause(m, "open", False)
else:
m = _OPEN_STATE_EN.fullmatch(clause)
if m:
out = _bind_state_clause(m, "open", True)
else:
out = {"kind": None, "bound": False}
roles = {k: v for k, v in out.items() if k not in ("kind", "bound")}
return {"text": clause, "kind": out.get("kind"),
"roles": roles, "bound": bool(out.get("bound"))}
def _clause_kind(clause: str) -> str | None:
"""小句被哪类受支持结构完整消费:'promise'/'closed'/'open',否则 None。"""
if _PROMISE_CLAUSE_ZH.fullmatch(clause) or _PROMISE_CLAUSE_EN.fullmatch(clause):
return "promise"
if any(r.fullmatch(clause) for r in _CLOSED_STATE_ZH) or any(
r.fullmatch(clause) for r in _CLOSED_STATE_EN):
return "closed"
if _OPEN_STATE_ZH.fullmatch(clause) or _OPEN_STATE_EN.fullmatch(clause):
return "open"
return None
"""小句被哪类受支持结构完整消费且角色绑定场景目标:
'promise'/'closed'/'open',否则 None。"""
p = _parse_clause(clause)
return p["kind"] if p["bound"] else None
def parse_supported_answer(text: str) -> dict:
"""HEL-864 全量解析:整个回答按预声明受支持结构与语气逐小句绑定。
"""HEL-864/865 全量解析:整个回答按预声明受支持结构、语气与场景角色
逐小句绑定。
返回 clauses/declarative/all_consumed/has_promise/closed_clauses/
open_clauses/unparsed——肯定验证(affirms_*)与审计复现共用同一解析,
不再按子串存在+否定词排除拼装。
open_clauses/unparsed/role_binding——肯定验证(affirms_*)与审计复现
共用同一解析,不再按子串存在+否定词排除拼装。HEL-865 起 clauses 逐项
含角色槽(subject/receiver/verb/object 与 bound 标记);角色冲突小句
与无法消费小句同样记入 unparsed(附区分原因),均使 all_consumed=False。
每个被消费的小句都绑定同一项场景声明:承诺者=用户(答案框架"你"/"you")、
告知接收者=伴侣("我"/"me")、对象=面试结果;显式他人/反转/未知角色或
多个小句角色冲突 → 整篇保守失败。
"""
raw = text or ""
clauses = iter_clauses(raw)
kinds = [(c, _clause_kind(c)) for c in clauses]
unparsed = [c for c, k in kinds if k is None]
parsed = [_parse_clause(c) for c in iter_clauses(raw)]
consumed = [p for p in parsed if p["kind"] is not None and p["bound"]]
declarative = (
not any(ch in raw for ch in _QUESTION_MARK_CHARS)
and not any(c and c[-1] in _ZH_MOOD_PARTICLES for c in clauses))
and not any(c and c[-1] in _ZH_MOOD_PARTICLES for c in
(p["text"] for p in parsed)))
role_conflicts = [p["text"] for p in parsed
if p["kind"] is not None and not p["bound"]]
return {
"clauses": [{"text": c, "kind": k} for c, k in kinds],
"clauses": parsed,
"declarative": declarative,
"all_consumed": bool(clauses) and not unparsed,
"has_promise": any(k == "promise" for _, k in kinds),
"closed_clauses": [c for c, k in kinds if k == "closed"],
"open_clauses": [c for c, k in kinds if k == "open"],
"unparsed": unparsed,
"all_consumed": bool(parsed) and len(consumed) == len(parsed),
"has_promise": any(p["kind"] == "promise" and p["bound"]
for p in parsed),
"closed_clauses": [p["text"] for p in consumed if p["kind"] == "closed"],
"open_clauses": [p["text"] for p in consumed if p["kind"] == "open"],
"unparsed": [p["text"] for p in parsed
if p["kind"] is None or not p["bound"]],
"role_binding": {
"scene_target": {
"promisor": SCENE_PROMISOR, "receiver": SCENE_RECEIVER,
"object_zh": SCENE_OBJECT_ZH, "object_en": SCENE_OBJECT_EN,
"note": "场景唯一被验收的承诺声明=p1 用户原话'面试结果出来我周五"
"晚上告诉你';答案框架中承诺者=你/you(用户),告知接收"
"者=我/me(伴侣),对象=面试结果。主语/接收者省略按预声"
"明回指规则继承:承诺小句省略主语→承诺者;省略接收者→"
"场景接收者;状态小句按动词类别继承(tell 类主语=承诺"
"者,receive 类受收方=场景接收者,未结 receive 主语兼容"
"接收者/承诺者两读法,fulfill 类主语=承诺者)。",
},
"role_conflict_clauses": role_conflicts,
"conflict_free": not role_conflicts,
},
}
def affirms_promise_state(text: str, *, closed: bool) -> bool:
"""肯定陈述已结/未结状态并绑回同一项承诺声明(HEL-864 全量解析):
全篇陈述语气 AND 每个小句都被受支持结构完整消费 AND ≥1 承诺小句 AND
≥1 对应极性状态小句 AND 0 个反向极性小句 AND 无不确定表述。状态短语
属于别的对象(体检报告/体检情况)、裸名词小句、全段冲突尾句、条件句、
疑问语气、状态后否认均无法消费 → 保守判失败。
"""肯定陈述已结/未结状态并绑回同一项承诺声明(HEL-864 全量解析 +
HEL-865 场景角色绑定):全篇陈述语气 AND 每个小句都被受支持结构完整消费
且角色绑定场景目标 AND ≥1 承诺小句 AND ≥1 对应极性状态小句 AND 0 个
反向极性小句 AND 无不确定表述。状态短语属于别的对象(体检报告/体检情况)、
裸名词小句、全段冲突尾句、条件句、疑问语气、状态后否认均无法消费;
告知接收者与承诺接收者不同("现在已经告诉你了"/told them)、承诺指向
他人(告诉她)、承诺方反转(我答应告诉你)均属另一项声明 → 保守判失败。
"""
p = parse_supported_answer(text)
wanted = p["closed_clauses"] if closed else p["open_clauses"]
@@ -851,13 +1058,14 @@ def affirms_promise_state(text: str, *, closed: bool) -> bool:
def affirms_promise_claim(text: str) -> bool:
"""肯定陈述固定语料中的承诺(HEL-864 全量解析):
全篇陈述语气 AND 每个小句都被受支持结构完整消费 AND ≥1 承诺小句 AND
无不确定表述。承诺动词被否定(未曾答应)、告知动作被否定(答应不告诉)、
对象错挂(体检结果/状态后置对象)、条件句(如果你答应)、跨小句各出现
一个词凑数、仅陈述告知结果而无承诺断言(面试结果已经告诉我了)均判
False;无法按受支持表达完整解析即保守判失败,不靠枚举整句禁词或
七句黑名单冒充肯定验证。
"""肯定陈述固定语料中的承诺(HEL-864 全量解析 + HEL-865 场景角色绑定):
全篇陈述语气 AND 每个小句都被受支持结构完整消费且角色绑定场景目标 AND
≥1 承诺小句 AND 无不确定表述。承诺动词被否定(未曾答应)、告知动作被
否定(答应不告诉)、对象错挂(体检结果/状态后置对象)、条件句(如果你
答应)、跨小句各出现一个词凑数、仅陈述告知结果而无承诺断言(面试结果
已经告诉我了)、承诺方反转(我答应周五晚告诉你)、承诺指向他人(告诉
她/them)均判 False;无法按受支持表达完整解析或角色不能绑定同一项场景
声明即保守判失败,不靠枚举整句禁词、八句黑名单或人称禁词表冒充肯定验证。
"""
p = parse_supported_answer(text)
return bool(p["declarative"] and p["all_consumed"] and p["has_promise"]
@@ -962,16 +1170,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-864:有界语法全量解析绑定承诺对象/动作极性/状态/语气):
"""S6 判据(HEL-864 有界语法全量解析 + HEL-865 场景角色绑定):
编排层(已知id置 resolved 留痕 + 未结对照块自洽)+ 状态进上下文层(正确对象
的 resolved 状态实际写入带门装配块)+ 模型使用层(两侧请求成功;两侧回答
均须全篇陈述语气且每个小句被受支持结构完整消费——承诺小句"主语?→承诺动词
(未被否定)→告知动作(未被否定)→(可选接收者)→面试结果到此为止"+
对应极性状态小句(已结/未结);外来对象(含无"结果"后缀)、状态后置对象、
裸名词小句、全段冲突尾句、条件句、疑问语气、状态短语后否认、对象与动作
串错、仅陈述告知而无承诺断言、不确定表述、两侧同答不区分状态均判失败,
不以字面提到'告诉/答应'冒充状态正确;无法按受支持表达完整解析即保守判
失败)。"""
均须全篇陈述语气且每个小句被受支持结构完整消费并绑定同一项场景声明——
承诺小句"主语(=承诺者你/you 或省略继承)→承诺动词(未被否定)→告知
动作(未被否定)→接收者(=场景接收者我/me 或省略继承)→面试结果到此
为止"+ 对应极性状态小句(已结/未结,角色同样绑定);外来对象(含无"结果"
后缀)、状态后置对象、裸名词小句、全段冲突尾句、条件句、疑问语气、状态
短语后否认、对象与动作串错、仅陈述告知而无承诺断言、告知接收者与承诺
接收者不同(告诉你/told them)、承诺指向他人(告诉她)、承诺方反转
(我答应告诉你)、不确定表述、两侧同答不区分状态均判失败,不以字面
提到'告诉/答应'冒充状态正确;无法按受支持表达完整解析或角色不能绑定
同一项声明即保守判失败)。"""
ans_r = (a_resolved or {}).get("answer") or ""
ans_u = (a_unresolved or {}).get("answer") or ""
ok_r = bool((a_resolved or {}).get("ok"))
@@ -1011,14 +1222,19 @@ def s6_result(orch: dict, a_resolved: dict, a_unresolved: dict) -> dict:
return {"layers": layers, "pass": ok,
"expect": "已知id编排置 resolved 留痕且未结对照块剔除矛盾条目后自洽;"
"正确对象的状态实际写入带门装配块;模型沿状态区分且两侧"
"回答均须按预声明受支持语法全量解析:全篇陈述语气(无问号"
"/句末吗呢吧嘛呗)、每个小句被承诺小句(主语?→承诺动词→"
"告知动作→(可选接收者)→面试结果到此为止)或对应极性状态"
"小句完整消费、整篇含承诺断言——外来对象(含无'结果'后缀)、"
"状态后置对象、裸名词小句、全段冲突尾句、条件句、疑问、状态"
"短语后否认、对象与动作串错、仅陈述告知无承诺、不确定表述、"
"两侧同答、已结侧答未结、未结侧宣称已收到均判失败(无法完整"
"解析即保守判失败,不以字面提到'告诉/答应'冒充)"}
"回答均须按预声明受支持语法全量解析并绑定同一项场景声明"
"(承诺者=用户'你/you'、告知接收者=伴侣'我/me'、对象=面试"
"结果,省略槽按预声明回指规则继承):全篇陈述语气(无问号"
"/句末吗呢吧嘛呗)、每个小句被承诺小句(主语→承诺动词→"
"告知动作→接收者→面试结果到此为止)或对应极性状态小句"
"完整消费且角色不冲突、整篇含承诺断言——外来对象(含无"
"'结果'后缀)、状态后置对象、裸名词小句、全段冲突尾句、"
"条件句、疑问、状态短语后否认、对象与动作串错、仅陈述告知"
"无承诺、接收者与承诺接收者不同(告诉你/told them)、承诺"
"指向他人(告诉她)、承诺方反转(我答应)、不确定表述、"
"两侧同答、已结侧答未结、未结侧宣称已收到均判失败(无法"
"完整解析或角色不能绑定同一项声明即保守判失败,不以字面"
"提到'告诉/答应'冒充)"}
# HEL-860:无门 assemble_block 出口已删除——全部问答最终装配一律经
+435
View File
@@ -0,0 +1,435 @@
#!/usr/bin/env python3
"""HEL-865 负例复现脚本:总管独立复核《HEL-864-独立复核.md》八组
"同一承诺是谁答应又告诉了谁"角色串绑反例。
零真实模型、零网络(socket 硬拒绝 + HF 离线)、零本地嵌入:S6 判据为纯函数
代入(真实 s6_result 构造器同一入口);既有判据不回归部分复用 audit861 的
两组纯函数节(S6 全套正负例 + S2/S3 总判据)、audit862 的四组复核用例、
audit863 的三组复核用例与 audit864 的七组+未结侧镜像及英文正负例。同一
脚本在修复前(070f4fd)与修复后代码上均可运行:
修复前(--phase prefix,在 070f4fd 工作树上运行)→ 总管复核八组反例
(每次仅替换已结/未结一侧,另一侧为原规范正例)全部 pass=true
(复现);结构变体(他/我们/承诺动词换词/英文 her/us/we/I/混排多承诺
小句)同缺口复现;省略继承正例与既有用例维持预期
修复后(默认 postfix)→ 八组反例与结构变体全部判失败;省略槽按预声明
回指规则继承的规范正例(含"我还没有收到")保留通过;audit861/862/
863/864 全部既有正负例、S2/S3 总判据、英文正负例维持预期
补正口径(固定合成语料领域内的有界槽位绑定,预声明场景目标与回指规则):
全量解析不再只产出 promise/closed/open 三类型——每个小句捕获并核准承诺者
主语、告知接收者、动作、对象与状态,全部绑定同一项场景声明(p1 用户原话
"面试结果出来我周五晚上告诉你":承诺者=用户〔答案框架"你"/"you"〕、告知
接收者=伴侣〔"我"/"me"〕、对象=面试结果)。省略槽按预声明回指规则继承:
承诺小句省略主语→承诺者、省略接收者→场景接收者;状态小句按动词类别继承
(tell 类主语=承诺者;receive 类受收方=场景接收者,未结 receive 主语兼容
接收者未收到告知/承诺者未收到结果两读法;fulfill 类主语=承诺者)。明确
不同接收者(现在已经告诉你了/还没有告诉你/told them)、承诺指向他人
(告诉她/her)、承诺方反转(我答应/我承诺/I promised)、多个小句角色冲突、
未知角色(we/我们/他)一律失败;未知也失败;不加八句/人称禁词黑名单,
不新增模型、不做通用自然语言引擎、不重做实验,只约束本实验判据。
退出码真实依赖正负例:除全部用例满足预期外,还对每个用例做翻转自检
(flip 任一用例的实际结果,用例总判必翻转);任一翻转不改变结论则退出 1。
用法:python -m spike.hel857.audit865 [--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("audit865 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)
from .audit863 import ADDITIONAL_CASES
from .audit864 import (EN_NEGATIVE, EN_POSITIVE, GOOD_CLOSED, GOOD_OPEN,
SEVEN_CLOSED, SEVEN_OPEN_MIRROR)
OUT = {"real_network_allowed": False, "real_model_requests": 0,
"local_embeddings": 0}
PHASE = "postfix"
def ledger_snapshot():
"""权威账只读快照(含逐行摘要:全部行序列化 SHA256,前后核对用)。"""
with sqlite3.connect(f"file:{LEDGER}?mode=ro", uri=True) as c:
rows = c.execute("select * from embed_ops order by id").fetchall()
return {"counts": dict(c.execute("select kind,sum(items) from embed_ops "
"group by kind")),
"rows": len(rows),
"row_digest": hashlib.sha256(
json.dumps(rows).encode()).hexdigest()}
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",
"audit861_reuse863.json", "audit863_prefix.json", "audit863_postfix.json",
"audit864_prefix.json", "audit864_postfix.json", "audit861_reuse864.json",
"gate0_ledger.json", "gate1_backend.json", "gate2_privacy.json",
"preflight.json", "resource_manifest_pre.json", "lock-notes.md"]
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-864-独立复核.md》八组反例(原文逐字;每次仅替换已结/未结一侧,
# 另一侧为原规范正例,排除其他原因)。同一真实 s6_result 入口。
def s6_answer(s):
return {"answer": s, "ok": True}
MANAGER_EIGHT = {
"recipient_changed_closed":
("你答应周五晚告诉我面试结果,现在已经告诉你了。", GOOD_OPEN),
"recipient_changed_open":
(GOOD_CLOSED, "你答应周五晚告诉我面试结果,还没有告诉你。"),
"promise_to_someone_else_closed":
("你答应周五晚告诉她面试结果,现在已经告诉我了。", GOOD_OPEN),
"promise_to_someone_else_open":
(GOOD_CLOSED, "你答应周五晚告诉她面试结果,还没有告诉我。"),
"promisor_reversed_closed":
("我答应周五晚告诉你面试结果,现在已经告诉你了。", GOOD_OPEN),
"promisor_reversed_open":
(GOOD_CLOSED, "我答应周五晚告诉你面试结果,还没有告诉你。"),
"english_other_recipient_closed":
("You promised to tell me the interview result, "
"and you have already told them.", GOOD_OPEN),
"english_other_recipient_open":
(GOOD_CLOSED, "You promised to tell me the interview result, "
"but you haven't told them yet."),
}
# 结构变体(证明按角色类绑定而非八句/人称黑名单:同一缺口换代词/换承诺
# 动词/英文换格/混排多承诺小句,修前同样误判成功;both_sides_ta 与
# unknown_subject_ta 在 070f4fd 已被 HEL-864 语法判失败,作双期负例保留)。
STRUCTURAL_NEGATIVES = {
"structural_recv_ta_closed":
("你答应周五晚告诉他面试结果,现在已经告诉我了。", GOOD_OPEN),
"structural_recv_women_open":
(GOOD_CLOSED, "你答应周五晚告诉我们面试结果,还没有告诉我。"),
"structural_both_sides_ta_open":
(GOOD_CLOSED, "你答应周五晚告诉她面试结果,还没有告诉她。"),
"structural_promisor_reversed_chengnuo_closed":
("我承诺周五晚告诉你面试结果,现在已经告诉你了。", GOOD_OPEN),
"structural_en_promise_to_her_closed":
("You promised to tell her the interview result, "
"and you have already told me.", GOOD_OPEN),
"structural_en_recv_us_closed":
("You promised to tell me the interview result, "
"and you have already told us.", GOOD_OPEN),
"structural_en_unknown_subject_we_closed":
("We promised to tell me the interview result, "
"and you have already told me.", GOOD_OPEN),
"structural_en_closed_tell_subject_i_closed":
("You promised to tell me the interview result, "
"and I have already told you.", GOOD_OPEN),
"structural_zh_open_tell_subject_wo_open":
(GOOD_CLOSED, "你答应周五晚告诉我面试结果,我还没有告诉你。"),
"structural_zh_open_fulfill_subject_wo_open":
(GOOD_CLOSED, "你答应周五晚告诉我面试结果,我还没兑现。"),
"structural_en_closed_receive_subject_you_closed":
("You promised to tell me the interview result, "
"and you have received the result.", GOOD_OPEN),
"structural_mixed_promise_recipients_closed":
("你答应周五晚告诉她面试结果,你答应周五晚告诉我面试结果,"
"现在已经告诉我了。", GOOD_OPEN),
"structural_unknown_subject_ta_closed":
("他答应周五晚告诉我面试结果,现在已经告诉我了。", GOOD_OPEN),
}
# 070f4fd 已判失败(维持负例,两期极性固定):
STRUCTURAL_ALWAYS_NEGATIVE = (
"structural_both_sides_ta_open",
"structural_en_closed_receive_subject_you_closed",
"structural_unknown_subject_ta_closed")
# 省略槽按预声明回指规则继承的规范正例(主语/接收者省略、"我还没有收到"
# 等已接受等价表达、裸回指/话题形态;两期均须通过——证明绑定按槽位而非
# 按字面禁词)。
EGC = ("You promised to tell me the interview result, "
"and you have already told me.")
EGO = ("You promised to tell me the interview result, "
"but you haven't told me yet.")
OMISSION_INHERIT_POSITIVES = {
"omit_p_receiver_closed":
("你答应周五晚告诉面试结果,现在已经告诉我了。", GOOD_OPEN),
"omit_p_subject_closed":
("答应周五晚告诉我面试结果,现在已经告诉我了。", GOOD_OPEN),
"omit_state_receiver_closed":
("你答应周五晚告诉我面试结果,现在已经告诉了。", GOOD_OPEN),
"omit_open_receiver_open":
(GOOD_CLOSED, "你答应周五晚告诉我面试结果,还没有告诉。"),
"omit_open_receive_subject_open":
(GOOD_CLOSED, "你答应周五晚告诉我面试结果,还没有收到。"),
"bare_anaphor_closed":
("你答应周五晚告诉我面试结果,结果已收到。", GOOD_OPEN),
"topic_tell_closed":
("你答应周五晚告诉我面试结果,面试结果已经告诉我了。", GOOD_OPEN),
"omit_en_open_receive_subject_open":
(EGC, "You promised to tell me the interview result, "
"haven't received it yet."),
"en_closed_receive_i_closed":
("You promised to tell me the interview result, "
"and I have received it.", EGO),
}
# 总管 review864.py 既有变体(070f4fd 已拒绝,不回归)+ 规范正例。
P_PREFIX = "你答应周五晚告诉我面试结果,"
HELD_VARIANTS = {
"variant_other_object": P_PREFIX + "血压现在已经告诉我了。",
"variant_afterthought": P_PREFIX + "已经告诉我了血压。",
"variant_denial": P_PREFIX + "现在已经告诉我了,这并不是真的。",
"variant_opposite_closed": P_PREFIX + "现在已经告诉我了,还没有告诉我。",
"variant_question_no_mark": P_PREFIX + "现在已经告诉我了吧",
"variant_condition": "除非你答应周五晚告诉我面试结果,现在已经告诉我了。",
}
def _record(name: str, resolved: str, unresolved: str, expected: bool) -> dict:
result = A.s6_result(ORCH6, s6_answer(resolved), s6_answer(unresolved))
return {
"resolved_input": resolved, "unresolved_input": unresolved,
"expected_pass": expected, "actual_pass": result["pass"],
"resolved_parse": A.parse_supported_answer(resolved),
"unresolved_parse": A.parse_supported_answer(unresolved),
"result": result,
}
def section_manager_eight() -> dict:
"""八组反例:prefix 期预期 pass=true(复现),postfix 期判失败。"""
return {name: _record(name, r, u, PHASE == "prefix")
for name, (r, u) in MANAGER_EIGHT.items()}
def section_structural() -> dict:
"""结构变体:same-class 换形(prefix 复现/ postfix 失败)与双期负例。"""
out = {}
for name, (r, u) in STRUCTURAL_NEGATIVES.items():
expected = (False if name in STRUCTURAL_ALWAYS_NEGATIVE
else PHASE == "prefix")
out[name] = _record(name, r, u, expected)
return out
def section_omission_positives() -> dict:
"""省略继承正例:两期均须通过(绑定按槽位+预声明回指,不按字面禁词)。"""
return {name: _record(name, r, u, True)
for name, (r, u) in OMISSION_INHERIT_POSITIVES.items()}
def section_held_previous() -> dict:
"""audit864 七组+镜像(070f4fd 已判失败,极性固定)与英文正负例。"""
out = {}
for name, closed in SEVEN_CLOSED.items():
out[f"eightyfour.{name}"] = _record(name, closed, GOOD_OPEN, False)
for name, opened in SEVEN_OPEN_MIRROR.items():
out[f"eightyfour.{name}"] = _record(name, GOOD_CLOSED, opened, False)
if PHASE == "postfix":
for name, (r, u) in EN_POSITIVE.items():
out[f"eightyfour.en.{name}"] = _record(name, r, u, True)
for name, (r, u) in EN_NEGATIVE.items():
out[f"eightyfour.en.{name}"] = _record(name, r, u, False)
return out
def section_held_rounds() -> dict:
"""HEL-862 四组与 HEL-863 三组:070f4fd 已按预期判失败,极性固定。"""
out = {}
for name, (closed, opened) in REVIEW_CASES.items():
out[f"review.{name}"] = _record(
name, closed, opened, name not in REVIEW_NEGATIVE_NAMES)
for name, closed in ADDITIONAL_CASES.items():
out[f"additional.{name}"] = _record(
name, closed, "你答应周五晚告诉我面试结果,还没有告诉我。", False)
return out
def section_held_variants() -> dict:
"""总管 review864.py 既有变体(不回归)与规范正例。"""
out = {f"variant.{name}": _record(name, v, GOOD_OPEN, False)
for name, v in HELD_VARIANTS.items()}
out["variant.canonical_positive"] = _record(
"canonical_positive", GOOD_CLOSED, GOOD_OPEN, True)
return out
def _case_table() -> tuple[dict, dict]:
"""全部用例的实际 pass 与预期极性(退出码公式输入)。"""
table: dict[str, bool] = {}
expect: dict[str, bool] = {}
for prefix, section in (
("manager8", OUT["manager_eight"]),
("structural", OUT["structural_variants"]),
("omission", OUT["omission_positives"]),
("held864", OUT["held_previous"]),
("held_rounds", OUT["held_rounds"]),
("held_variants", OUT["held_variants"])):
for name, c in section.items():
key = f"{prefix}.{name}"
table[key] = bool(c["actual_pass"])
expect[key] = bool(c["expected_pass"])
for name, res in OUT["held_s6_affirmative"].items():
key = f"held_s6.{name}"
table[key] = bool(res["pass"])
expect[key] = HELD_S6_EXPECT[name]
for name, res in OUT["held_s2_s3_no_regression"].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", "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["manager_eight"] = section_manager_eight()
OUT["structural_variants"] = section_structural()
OUT["omission_positives"] = section_omission_positives()
OUT["held_previous"] = section_held_previous()
OUT["held_rounds"] = section_held_rounds()
OUT["held_variants"] = section_held_variants()
OUT["held_s6_affirmative"] = section_s6_affirmative()
OUT["held_s2_s3_no_regression"] = section_s2_s3_no_regression()
table, expect = _case_table()
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"])
me = OUT["manager_eight"]
st = OUT["structural_variants"]
om = OUT["omission_positives"]
# ---- prefix:八组反例全部复现(pass=true)----
eight_reproduced = all(c["actual_pass"] for c in me.values())
# ---- postfix:八组反例全部判失败 ----
eight_rejected = not any(c["actual_pass"] for c in me.values())
# 结构变体:prefix 期同缺口复现、postfix 期全部判失败(双期负例除外,
# 其极性由 all_cases_match_expectation 核对)
structural_reproduced = all(
st[n]["actual_pass"] for n in st if n not in STRUCTURAL_ALWAYS_NEGATIVE)
structural_rejected = not any(c["actual_pass"] for c in st.values())
# 省略继承正例与规范正例保留
positives_kept = (all(c["actual_pass"] for c in om.values())
and OUT["held_variants"]["variant.canonical_positive"][
"actual_pass"])
OUT["summary"] = {
"phase": PHASE,
"eight_reproduced": eight_reproduced,
"eight_rejected": eight_rejected,
"structural_variants_reproduced": structural_reproduced,
"structural_variants_rejected": structural_rejected,
"omission_inheritance_positives_kept": positives_kept,
"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": (eight_reproduced and structural_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 (eight_rejected and structural_rejected
and positives_kept
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"audit865_{'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)