feat(cheap-worker): Node 退役授权三条齐判定 retire_authorization(M2 U3)
读三份既有落盘报告聚合:① M1 达标(bake-off-M1-final.json overallMeets) ② 002 对照等价(compare-multi-002-final.json summary.equivalentGenres ⊇ 三品类) ③ M2 auto-vs-golden(meets)。三条全绿→authorized;任一 False/报告缺失/字段类型 不符→未授权并列明欠项(不静默当通过)。纯函数零 LLM 零真跑。单测 17/17。 真切路由/D12 扣退在 M3。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
423b0b6df3
commit
dd0d1cc224
@ -12,6 +12,11 @@ tap-targets 不成立(M1 已把自动 spec 断言加厚到与金标逐字段一
|
||||
U1 逐门 delta 判据(本段,纯逻辑可单测);U2 同游戏双驱动 play 真跑;U3 三条齐退役授权判定。
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
_DEFAULT_GENRES = ["click-score", "whack-mole", "shop-serve"]
|
||||
|
||||
# 九门里唯一对驱动器敏感的三门(未驱动时 E_live/H_progress 降 advisory、G_input 无输入 skip):
|
||||
# 「这游戏到底能不能玩」的判据,自动低于金标即驱动更差,容差 0。
|
||||
_KEY_GATES = frozenset({"E_live", "H_progress", "G_input"})
|
||||
@ -91,3 +96,75 @@ def aggregate_delta(genre_runs: dict, required_genres: list) -> dict:
|
||||
"note": "auto-vs-golden delta 门(同款双驱动 · 关键门容差 0 · 零 LLM);"
|
||||
"覆盖面内鉴别力有限、定性为不退化确认,退役强证据=M1 达标+002 等价(见 U3)",
|
||||
}
|
||||
|
||||
|
||||
# ───────────────────────── U3 Node 退役授权三条齐判定(纯逻辑 · 零真跑 · 只读既有报告)─────────────────────────
|
||||
|
||||
def _read_json(path):
|
||||
"""容错读 JSON 报告:文件缺失 / 解析失败 → None(由调用方判「前置证据缺失」、不静默当通过)。"""
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
return None
|
||||
try:
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def retire_authorization(m1_path, compare_path, avg_path, required_genres=None) -> dict:
|
||||
"""Node 退役授权:三个硬前置全绿才出 authorized。
|
||||
|
||||
① M1 ≥80% 达标 = M1 收口报告 overallMeets is True(bake-off-M1-final.json)。
|
||||
② 002 对照等价 = 002 对照报告 summary.equivalentGenres ⊇ required_genres(compare-multi-002-final.json)。
|
||||
③ M2 auto-vs-golden = U2 报告 meets is True(auto-vs-golden-*.json)。
|
||||
|
||||
任一 False / 报告缺失 / 字段类型不符 → authorized=False、missing 列明欠项(不静默当通过)。
|
||||
纯函数、零 LLM、零真跑(只读既有落盘报告)。
|
||||
"""
|
||||
req = list(required_genres) if required_genres else list(_DEFAULT_GENRES)
|
||||
missing = []
|
||||
|
||||
# ① M1 达标。
|
||||
m1 = _read_json(m1_path)
|
||||
if m1 is None:
|
||||
m1_met = False
|
||||
missing.append("M1 达标:前置证据缺失(报告文件缺失或不可解析)")
|
||||
elif m1.get("overallMeets") is not True:
|
||||
m1_met = False
|
||||
missing.append("M1 达标:overallMeets 非 True(≥80% 达标门未绿)")
|
||||
else:
|
||||
m1_met = True
|
||||
|
||||
# ② 002 对照等价。
|
||||
cmp = _read_json(compare_path)
|
||||
equiv = ((cmp or {}).get("summary") or {}).get("equivalentGenres")
|
||||
if cmp is None:
|
||||
parity_ok = False
|
||||
missing.append("对照等价:前置证据缺失(报告文件缺失或不可解析)")
|
||||
elif not isinstance(equiv, list) or not set(req).issubset(set(equiv)):
|
||||
parity_ok = False
|
||||
lack = [g for g in req if g not in (equiv or [])]
|
||||
missing.append(f"对照等价:002 equivalentGenres 未覆盖全部品类(缺 {lack})")
|
||||
else:
|
||||
parity_ok = True
|
||||
|
||||
# ③ M2 auto-vs-golden。
|
||||
avg = _read_json(avg_path)
|
||||
if avg is None:
|
||||
avg_ok = False
|
||||
missing.append("auto-vs-golden:前置证据缺失(报告文件缺失或不可解析)")
|
||||
elif avg.get("meets") is not True:
|
||||
avg_ok = False
|
||||
missing.append("auto-vs-golden:meets 非 True(逐门 delta 门未绿)")
|
||||
else:
|
||||
avg_ok = True
|
||||
|
||||
authorized = m1_met and parity_ok and avg_ok
|
||||
return {
|
||||
"authorized": authorized,
|
||||
"conditions": {"m1Met": m1_met, "parityEquivalent": parity_ok, "autoVsGoldenAligned": avg_ok},
|
||||
"requiredGenres": req,
|
||||
"missing": missing,
|
||||
"note": "Node 退役授权三条齐(M1 达标 ∧ 002 对照等价 ∧ auto-vs-golden 不退化);"
|
||||
"authorized=授权切默认路由的条件齐备,真切路由/D12 扣退在 M3。本机口径,生产复验在 M3。",
|
||||
}
|
||||
|
||||
@ -12,7 +12,9 @@ test_auto_vs_golden.py — M2 auto-vs-golden 门单测(U1 逐门 delta 判据 +
|
||||
跑:cheap-worker/.venv/bin/python cheap-worker/tests/test_auto_vs_golden.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # → cheap-worker/
|
||||
@ -118,6 +120,75 @@ def test_aggregate_missing_genre_fails():
|
||||
assert r["meets"] is False and "shop-serve" in r["missingGenres"]
|
||||
|
||||
|
||||
# ───────────────────────── U3 Node 退役授权三条齐判定 ─────────────────────────
|
||||
|
||||
_REQ = ["click-score", "whack-mole", "shop-serve"]
|
||||
|
||||
|
||||
def _write(d: Path, name: str, obj) -> Path:
|
||||
p = d / name
|
||||
p.write_text(json.dumps(obj, ensure_ascii=False), encoding="utf-8")
|
||||
return p
|
||||
|
||||
|
||||
def _reports(d: Path, m1=True, equiv=None, avg=True):
|
||||
"""造三份报告:M1 达标 / 002 等价品类集 / M2 auto-vs-golden meets。"""
|
||||
m1p = _write(d, "m1.json", {"overallMeets": m1})
|
||||
equiv = _REQ if equiv is None else equiv
|
||||
cmp = _write(d, "cmp.json", {"summary": {"equivalentGenres": equiv}})
|
||||
avgp = _write(d, "avg.json", {"meets": avg})
|
||||
return m1p, cmp, avgp
|
||||
|
||||
|
||||
def test_retire_all_three_green_authorized():
|
||||
with tempfile.TemporaryDirectory() as t:
|
||||
m1p, cmp, avgp = _reports(Path(t))
|
||||
r = A.retire_authorization(m1p, cmp, avgp, _REQ)
|
||||
assert r["authorized"] is True
|
||||
assert r["conditions"] == {"m1Met": True, "parityEquivalent": True, "autoVsGoldenAligned": True}
|
||||
assert not r["missing"]
|
||||
|
||||
|
||||
def test_retire_m1_not_met_unauthorized():
|
||||
with tempfile.TemporaryDirectory() as t:
|
||||
m1p, cmp, avgp = _reports(Path(t), m1=False)
|
||||
r = A.retire_authorization(m1p, cmp, avgp, _REQ)
|
||||
assert r["authorized"] is False and r["conditions"]["m1Met"] is False
|
||||
assert any("M1" in m or "达标" in m for m in r["missing"])
|
||||
|
||||
|
||||
def test_retire_parity_missing_genre_unauthorized():
|
||||
"""002 等价品类集缺一品类(某品类 regressed)→ 对照等价未达成。"""
|
||||
with tempfile.TemporaryDirectory() as t:
|
||||
m1p, cmp, avgp = _reports(Path(t), equiv=["click-score", "whack-mole"])
|
||||
r = A.retire_authorization(m1p, cmp, avgp, _REQ)
|
||||
assert r["authorized"] is False and r["conditions"]["parityEquivalent"] is False
|
||||
assert any("对照" in m or "equivalent" in m.lower() for m in r["missing"])
|
||||
|
||||
|
||||
def test_retire_avg_not_met_unauthorized():
|
||||
with tempfile.TemporaryDirectory() as t:
|
||||
m1p, cmp, avgp = _reports(Path(t), avg=False)
|
||||
r = A.retire_authorization(m1p, cmp, avgp, _REQ)
|
||||
assert r["authorized"] is False and r["conditions"]["autoVsGoldenAligned"] is False
|
||||
assert any("auto-vs-golden" in m or "auto" in m.lower() for m in r["missing"])
|
||||
|
||||
|
||||
def test_retire_missing_report_file_unauthorized():
|
||||
"""任一报告文件缺失 → 前置证据缺失、不静默当通过。"""
|
||||
with tempfile.TemporaryDirectory() as t:
|
||||
m1p, cmp, avgp = _reports(Path(t))
|
||||
r = A.retire_authorization(m1p, cmp, Path(t) / "nonexistent.json", _REQ)
|
||||
assert r["authorized"] is False and r["conditions"]["autoVsGoldenAligned"] is False
|
||||
assert any("缺失" in m or "证据" in m for m in r["missing"])
|
||||
|
||||
|
||||
def test_retire_deterministic_zero_llm():
|
||||
with tempfile.TemporaryDirectory() as t:
|
||||
m1p, cmp, avgp = _reports(Path(t))
|
||||
assert A.retire_authorization(m1p, cmp, avgp, _REQ) == A.retire_authorization(m1p, cmp, avgp, _REQ)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
|
||||
_failed = 0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user