lili 87777975c7 feat(w-axis): R1 生成可信闸代码收口(actor/judge闭合+W-GOLD-LIVE+full_gate+三批闸门)
actor/judge prompt eval 闭合: actor 3.0.5 正文强化+baseline(7轮); judge 3.0.14 正文强化+anyTerms同义词补全+baseline(judge-a 13轮/judge-b 33轮); registry/checker 版本对齐+顺序锚点 fail-closed 双保险
W-GOLD-LIVE live_prompt: play-loop 契约开口(request/3+provenance/3+ReferenceAssetRecord/1+Registry/1+迁移清单15条); cheap_verify 切v3+消费对账六闸(只消费active)+生成prompt注入接线(0 active不注入); Node runner playtest-v3.cdp.cjs 升/3+绊线恢复; 真模型回归验向后兼容
full_gate+三批基线: full_gate.py 集成runner串六子门+降级+22测; baseline_gates.py fresh25阈值+historical11预期表(2 needs_human交创始人定标)+shadow20框架+38测
注: registry cheap-system 登记 1.8.0→1.8.1 同步此前在途 cheap-system.md frontmatter 升版(修 pre-commit 版本漂移); cheap_verify/cheap_studio/validate/test_acceptance_v3/test_cheap_service_driver 为在途M含本会话叠加+此前W-AXIS在途(同一线无法hunk分离); 3红测试根因与raw审计未入本提交
2026-07-25 18:47:47 -07:00

478 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""baseline_gates.py — 生成线验收 v3 三批基线闸门W-AXIS 收口 R1
把设计档 §5.2/§5.3/§5.4 的三批基线达标阈值落成机器断言,输入批结果、输出 PASS/FAIL + 逐项明细:
· fresh25§5.3 波 3 新基线):五品类各五局共 25 distinct gidaccepted≥20/25 且每品类≥3/5、
firstPassAccepted≥18/25、writer repair 启动率≤5/25、rescuedByRoll≤5/25、单局验收成本≤¥1.5、
parentRun 全链≤¥15。分子只认 row.accepted硬证完整 + 双 Judge 共识 + finalPostguard 全绿的最终权威),
acceptedAfterRepair 只作修复救回率观测绝不作成功率分子inconclusive/tester_error 单列不剔出分母。
· historical11§5.2 固定预期表):假阳放行 0 + 逐局预期相符 + needs_human 项定标前不得自动 accept
reject/inconclusive 挂起等创始人定标,闸门不自动 PASS。预期表 fixture =
contracts/play-loop/historical-11-expectations.json。
· shadow20§5.4 生产分布 shadowproof 完整率 100% + 确认假阳 0 + accepted 与 problems/缺证/矛盾共存 0
+ tester_error=020 局口径 <5%+ inconclusive≤1<10%+ 人工复核覆盖固定六项commit/Chrome/
Actor+Judge 模型/prompt 版本/配置快照/人工标签)漂移即 failfresh25 达标为硬前置§5.4「fresh 25 达标后」)。
三批依赖顺序在代码里是可选链式校验fresh25 结果可传入 historical/shadow20 闸门——historical 收到未过的
fresh25 时 warn 并阻断 PASS顺序是执行建议不阻断单跑的指标评估shadow20 按 §5.4 硬查前置。
本模块纯函数 + 罐头单测,零模型零 I/O除读预期表 fixture真跑留 mini-desktop。
"""
import json
from pathlib import Path
# ────────────────────────── fresh25设计档 §5.3)──────────────────────────
# 五个【有 per-genre 模板】的已覆盖品类,与 hard_genre_batch.COVERED_BRIEFS 同集(硬编码保持闸门自包含)。
FRESH25_GENRES = ("narrative", "trpg", "heritage", "puzzle", "sim-business")
# 阈值严格按设计档 §5.3,不放松。
FRESH25_THRESHOLDS = {
"totalGames": 25, # 五品类各五局共 25 distinct gid
"gamesPerGenre": 5,
"minAccepted": 20, # MVP 终态成功率按 accepted 计 ≥ 20/25
"minAcceptedPerGenre": 3, # 任一品类不低于 3/5
"minFirstPassAccepted": 18, # firstPassAccepted ≥ 18/25最多二掷、尚未 repair 的终态)
"maxRepairAttempted": 5, # writer repair 启动率 ≤ 5/25
"maxRescuedByRoll": 5, # rescuedByRoll二掷救回≤ 5/25
"maxRunAcceptanceCostRmb": 1.5, # 单局验收成本 ≤ ¥1.5
"maxParentChainCostRmb": 15.0, # parentRun 全链总成本 ≤ ¥15 硬地板
}
_COST_EPS = 1e-9 # 金额比较容差(与 _final_postguard_v3 的 +1e-9 口径一致)
def evaluate_fresh25_gate(rows, *, thresholds=None) -> dict:
"""fresh25 达标断言输入批结果行hard_genre_batch 行 schema输出 {pass, checks, metrics, warnings}。
row 消费字段gid/genre/accepted/firstPassAccepted/repairAttempted/rescuedByRoll/
acceptanceCostRmb/parentChainCostRmb/outcome/acceptedAfterRepair。
"""
th = dict(FRESH25_THRESHOLDS)
if thresholds:
th.update(thresholds)
rows = [r for r in (rows or []) if isinstance(r, dict)]
checks = []
warnings = []
def check(name, ok, actual, required, detail=""):
checks.append({"name": name, "pass": bool(ok), "actual": actual, "required": required, "detail": detail})
# ── 样本完整性25 distinct gid、恰好 5 品类、每品类 5 局(分母不完整 → 基线不成立,直接 fail
gids = [r.get("gid") for r in rows]
dup_gids = sorted({g for g in gids if gids.count(g) > 1 and g is not None})
by_genre = {}
for r in rows:
by_genre.setdefault(r.get("genre"), []).append(r)
genre_counts = {g: len(by_genre.get(g) or []) for g in FRESH25_GENRES}
extra_genres = sorted(g for g in by_genre if g not in FRESH25_GENRES)
size_ok = (len(rows) == th["totalGames"] and not dup_gids and not extra_genres
and all(c == th["gamesPerGenre"] for c in genre_counts.values()))
check("sampleSize", size_ok, f"{len(rows)} 局/品类分布 {genre_counts}",
f"{th['totalGames']} 局(每品类 {th['gamesPerGenre']}gid 不重复)",
f"重复 gid={dup_gids}" if dup_gids else (f"非覆盖品类={extra_genres}" if extra_genres else ""))
# ── accepted 总数(分子 = 硬证完整 + 双 Judge 共识 + finalPostguard 全绿的最终权威)。
accepted = [r for r in rows if r.get("accepted") is True]
check("acceptedTotal", len(accepted) >= th["minAccepted"], f"{len(accepted)}/{len(rows)}",
f"{th['minAccepted']}/{th['totalGames']}")
# ── 每品类 accepted ≥ 3/5。
per_genre_accepted = {g: sum(1 for r in by_genre.get(g) or [] if r.get("accepted") is True)
for g in FRESH25_GENRES}
failing_genres = sorted(g for g, c in per_genre_accepted.items() if c < th["minAcceptedPerGenre"])
check("perGenreMin", not failing_genres, per_genre_accepted,
f"每品类 ≥ {th['minAcceptedPerGenre']}/{th['gamesPerGenre']}",
f"未达标品类={failing_genres}" if failing_genres else "")
# ── firstPassAccepted ≥ 18/25首轮直接成功 + 二掷内成功repair 前的终态)。
first_pass = sum(1 for r in rows if r.get("firstPassAccepted") is True)
check("firstPassAccepted", first_pass >= th["minFirstPassAccepted"], f"{first_pass}/{len(rows)}",
f"{th['minFirstPassAccepted']}/{th['totalGames']}")
# ── writer repair 启动率 ≤ 5/25。
repair_attempted = sum(1 for r in rows if r.get("repairAttempted") is True)
check("repairRate", repair_attempted <= th["maxRepairAttempted"], f"{repair_attempted}/{len(rows)}",
f"{th['maxRepairAttempted']}/{th['totalGames']}")
# ── rescuedByRoll二掷救回取值 2≤ 5/25与 hard_genre_batch 主汇总口径一致。
rescued = sum(1 for r in rows if r.get("rescuedByRoll") == 2)
check("rescuedByRollRate", rescued <= th["maxRescuedByRoll"], f"{rescued}/{len(rows)}",
f"{th['maxRescuedByRoll']}/{th['totalGames']}")
# ── 单局验收成本 ≤ ¥1.5(缺成本 = 无法证明达标 → fail-closed 该检查)。
run_costs = []
missing_run_cost = []
for r in rows:
cost = r.get("acceptanceCostRmb")
if isinstance(cost, (int, float)) and cost == cost: # 拒 NaNcost != cost 即 NaN
run_costs.append((r.get("gid"), float(cost)))
else:
missing_run_cost.append(r.get("gid"))
over_run = [(g, c) for g, c in run_costs if c > th["maxRunAcceptanceCostRmb"] + _COST_EPS]
check("perRunCost", rows != [] and not over_run and not missing_run_cost,
f"max=¥{max((c for _, c in run_costs), default=0.0):.5f}/缺 {len(missing_run_cost)}",
f"每局 ≤ ¥{th['maxRunAcceptanceCostRmb']}",
f"超限={over_run[:3]}" if over_run else (f"缺成本 gid={missing_run_cost[:3]}" if missing_run_cost else ""))
# ── parentRun 全链总成本 ≤ ¥15repair 历史 + writer + 验收的不重不漏权威总成本)。
chain_costs = []
missing_chain_cost = []
for r in rows:
cost = r.get("parentChainCostRmb")
if isinstance(cost, (int, float)) and cost == cost:
chain_costs.append((r.get("gid"), float(cost)))
else:
missing_chain_cost.append(r.get("gid"))
over_chain = [(g, c) for g, c in chain_costs if c > th["maxParentChainCostRmb"] + _COST_EPS]
check("parentChainCost", rows != [] and not over_chain and not missing_chain_cost,
f"max=¥{max((c for _, c in chain_costs), default=0.0):.5f}/缺 {len(missing_chain_cost)}",
f"全链 ≤ ¥{th['maxParentChainCostRmb']}",
f"超限={over_chain[:3]}" if over_chain else
(f"缺成本 gid={missing_chain_cost[:3]}" if missing_chain_cost else ""))
# ── 观测项(不作闸门):四态分布 + acceptedAfterRepair只统计修复救回率绝不当成功率分子
outcomes = {name: sum(1 for r in rows if r.get("outcome") == name)
for name in ("accept", "reject", "inconclusive", "tester_error")}
repaired = sum(1 for r in rows if r.get("acceptedAfterRepair") is True)
if outcomes["inconclusive"] or outcomes["tester_error"]:
warnings.append(f"inconclusive={outcomes['inconclusive']} tester_error={outcomes['tester_error']} "
f"单列观测,未并入游戏失败也未剔出分母")
metrics = {
"accepted": len(accepted), "firstPassAccepted": first_pass,
"repairAttempted": repair_attempted, "rescuedByRoll": rescued,
"perGenreAccepted": per_genre_accepted, "outcomes": outcomes,
"acceptedAfterRepair_observation": repaired, # 修复救回率观测,不是成功率分子
"maxRunAcceptanceCostRmb": max((c for _, c in run_costs), default=None),
"maxParentChainCostRmb": max((c for _, c in chain_costs), default=None),
}
return {"pass": all(c["pass"] for c in checks), "checks": checks,
"metrics": metrics, "warnings": warnings, "thresholds": th}
# ────────────────────────── historical11设计档 §5.2)──────────────────────────
# 预期表 fixture11 局固定预期3 narrative 正例 / 5 旧假阳 / puzzle-r2 真 bug / 2 疑似假阴)。
_HISTORICAL_EXPECTATIONS_PATH = (Path(__file__).resolve().parents[1]
/ "contracts" / "play-loop" / "historical-11-expectations.json")
_EXPECTED_VALUES = ("accept", "reject", "not_accept", "needs_human")
def load_historical_expectations(path=None) -> dict:
"""读并校验 11 局固定预期表;结构非法直接抛 ValueError闸门 fixture 必须机器可信)。"""
p = Path(path) if path else _HISTORICAL_EXPECTATIONS_PATH
data = json.loads(p.read_text(encoding="utf-8"))
rows = data.get("expectations")
if not isinstance(rows, list) or len(rows) != 11:
raise ValueError(f"historical 预期表必须恰有 11 局,实际 {len(rows) if isinstance(rows, list) else 'N/A'}")
gids = [r.get("gid") for r in rows if isinstance(r, dict)]
if len(set(gids)) != 11 or any(not isinstance(g, str) or not g for g in gids):
raise ValueError("historical 预期表 gid 必须 11 个非空且不重复")
for r in rows:
if r.get("expected") not in _EXPECTED_VALUES:
raise ValueError(f"historical 预期 {r.get('gid')} expected 非法:{r.get('expected')}")
return data
def evaluate_historical_gate(replay_rows, *, expectations=None, expectations_path=None,
fresh25_gate=None) -> dict:
"""historical replay 闸门:假阳放行 0 + 逐局预期相符 + needs_human 定标前不自动 accept。
replay_rows[{gid, outcome}]outcome ∈ accept/reject/inconclusive/tester_errorhistorical_replay 产物)。
fresh25_gate可选链式校验——提供且未过 → warn 并阻断 PASS顺序是执行建议不阻断指标评估None → warn 单跑。
"""
data = expectations if isinstance(expectations, dict) else load_historical_expectations(expectations_path)
exp_by_gid = {r["gid"]: r for r in data["expectations"]}
rows = [r for r in (replay_rows or []) if isinstance(r, dict)]
replay_by_gid = {}
for r in rows:
gid = r.get("gid")
if gid in exp_by_gid and gid not in replay_by_gid:
replay_by_gid[gid] = r
warnings = []
per_game = []
false_positive_released = [] # critical 局被 accept已知坏例放行闸门红线 = 0
mismatches = [] # 逐局预期不符
pre_calibration_accepts = [] # needs_human 局定标前被 accept红线
pending_human = [] # needs_human 局待定标(挂起,阻断自动 PASS交创始人
extra = sorted({r.get("gid") for r in rows if r.get("gid") not in exp_by_gid})
if extra:
warnings.append(f"重放含预期表外 gid忽略不计:{extra}")
for gid, exp in exp_by_gid.items():
expected = exp["expected"]
row = replay_by_gid.get(gid)
if row is None:
mismatches.append({"gid": gid, "expected": expected, "actual": None, "reason": "重放缺该局"})
per_game.append({"gid": gid, "expected": expected, "actual": None, "match": False})
continue
outcome = row.get("outcome")
match = False
reason = ""
if expected == "accept":
match = outcome == "accept"
if not match:
reason = "正例应保留accept实际未 accept——验收器可能回退"
elif expected == "not_accept":
if outcome == "accept":
false_positive_released.append(gid)
reason = "旧假阳无新硬证被 accept = 确认假阳放行"
elif outcome in ("reject", "inconclusive"):
match = True
else:
reason = f"tester_error 不构成证据({outcome}),需重跑"
elif expected == "reject":
if outcome == "accept":
false_positive_released.append(gid)
reason = "真 bug 局被 accept = 假阳放行"
elif outcome == "reject":
match = True
else:
reason = f"固定预期 reject实际 {outcome} 不符——查验收器为何拿不到硬证"
elif expected == "needs_human":
if outcome == "accept":
pre_calibration_accepts.append(gid)
reason = "疑似假阴未定标即 accept——定标前不得自动接受"
elif outcome in ("reject", "inconclusive", "tester_error"):
pending_human.append(gid)
reason = "挂起等真人真浏览器定标(不计自动 accept也不自动 PASS"
else:
reason = f"未知 outcome:{outcome}"
if not match and gid not in pending_human:
mismatches.append({"gid": gid, "expected": expected, "actual": outcome, "reason": reason})
per_game.append({"gid": gid, "expected": expected, "actual": outcome,
"match": match, "pendingHuman": gid in pending_human, "detail": reason})
# ── 链式校验可选fresh25 未过 → warn + 阻断 PASS未提供 → warn 单跑。
chain_blocked = False
if fresh25_gate is None:
warnings.append("未提供 fresh25 闸门结果:单跑模式(设计档建议顺序 fresh25 → historical11 → shadow20")
elif not isinstance(fresh25_gate, dict) or fresh25_gate.get("pass") is not True:
chain_blocked = True
warnings.append("链式校验fresh25 未达标 → historical 闸门 PASS 受阻(执行顺序建议,不阻断指标评估)")
checks = [
{"name": "coverage", "pass": len(replay_by_gid) == 11,
"actual": f"{len(replay_by_gid)}/11", "required": "11 局全部重放",
"detail": ",".join(m["gid"] for m in mismatches if m["actual"] is None)},
{"name": "falsePositiveRelease", "pass": not false_positive_released,
"actual": len(false_positive_released), "required": "0已知坏例放行数为 0",
"detail": ",".join(false_positive_released)},
{"name": "preCalibrationAccept", "pass": not pre_calibration_accepts,
"actual": len(pre_calibration_accepts), "required": "0needs_human 定标前不得 accept",
"detail": ",".join(pre_calibration_accepts)},
{"name": "perGameExpectation", "pass": not mismatches,
"actual": f"{11 - len(mismatches)}/11 相符", "required": "逐局固定预期相符",
"detail": ";".join(f"{m['gid']}:{m['reason']}" for m in mismatches if m["actual"] is not None)},
{"name": "humanCalibrationSettled", "pass": not pending_human,
"actual": f"{len(pending_human)} 局待定标", "required": "0疑似假阴须先真人定标",
"detail": ",".join(pending_human)},
{"name": "chainFresh25", "pass": not chain_blocked,
"actual": "受阻" if chain_blocked else "通过/单跑", "required": "fresh25 达标(提供时)"},
]
return {"pass": all(c["pass"] for c in checks), "checks": checks, "perGame": per_game,
"blockedOnHumanCalibration": pending_human,
"falsePositiveReleased": false_positive_released,
"warnings": warnings}
# ────────────────────────── shadow20设计档 §5.4)──────────────────────────
SHADOW20_MIN_SAMPLES = 20 # 「连续不少于 20 个真实生产 prompt」
# 固定六项:代码 commit / Chrome 版本 / Actor 模型 / Judge 模型 / prompt 版本 / 配置快照(+ 人工标签占位)。
SHADOW20_IDENTITY_FIELDS = ("commitHash", "chromeVersion", "actorModel", "judgeModel",
"promptVersion", "configSnapshotHash")
# 20 局口径下 <5% → tester_error 必须 0<10% → inconclusive 最多 1设计档 §5.4 明文换算)。
SHADOW20_THRESHOLDS = {
"minSamples": SHADOW20_MIN_SAMPLES,
"maxConfirmedFalsePositives": 0,
"maxAcceptedWithProblems": 0,
"maxTesterError": 0,
"maxInconclusive": 1,
"minOrdinaryAcceptHumanReviewed": 5, # 普通 accept 至少抽 5accept 不足 5 则全查)
}
def build_shadow_plan(prompts, *, commit_hash, chrome_version, actor_model, judge_model,
prompt_version, config_snapshot_hash, human_labels=None, note="") -> dict:
"""shadow20 跑批计划:固定六项 + ≥20 连续生产 prompt + 人工标签占位。任一固定项缺失/样本不足 → ValueError。"""
prompts = list(prompts or [])
if len(prompts) < SHADOW20_MIN_SAMPLES:
raise ValueError(f"shadow20 需连续 ≥{SHADOW20_MIN_SAMPLES} 个真实生产 prompt实际 {len(prompts)}")
identity = {"commitHash": commit_hash, "chromeVersion": chrome_version,
"actorModel": actor_model, "judgeModel": judge_model,
"promptVersion": prompt_version, "configSnapshotHash": config_snapshot_hash}
empty = [k for k, v in identity.items() if not (isinstance(v, str) and v.strip())]
if empty:
raise ValueError(f"shadow20 固定六项不得为空:{empty}")
return {
"schemaVersion": "shadow20-plan/1",
"identity": identity,
"prompts": prompts,
# 人工标签占位:真跑后由人工逐局回填(分歧/reject/rescued/inconclusive/tester_error 必查accept 抽 ≥5
"humanLabels": dict(human_labels or {}),
"note": str(note or ""),
"acceptanceMode": "v3_shadow", # shadow 新生产 run 只跑 v3、内测隔离、冻结自动发布§3.10
}
async def run_shadow_batch(plan, *, run_one) -> list:
"""shadow runner 框架:串行编排 ≥20 局独立 v3_shadow每局盖固定身份戳。
run_one注入的真跑 callableasync/sync 均可)—— (prompt, identity) → 单局结果行mini-desktop 提供真实现,
经 cheap_verify.run_acceptance_v3(mode=v3_shadow) 产出行);本地单测注入罐头函数,不烧真模型。
本框架只负责串行调度 + 身份固化 + 行规范化,不碰模型。
"""
if not isinstance(plan, dict) or not isinstance(plan.get("prompts"), list):
raise ValueError("shadow plan 非法")
identity = plan.get("identity") if isinstance(plan.get("identity"), dict) else {}
rows = []
for index, prompt in enumerate(plan["prompts"]):
result = run_one(prompt, dict(identity))
if hasattr(result, "__await__"):
result = await result
row = dict(result) if isinstance(result, dict) else {"outcome": "tester_error", "raw": result}
# 每局固化身份 + 序号,供闸门核对固定六项零漂移;人工标签占位随行带出待回填。
row.setdefault("gid", f"shadow20-{index + 1:02d}")
row["identity"] = dict(identity)
row["planIndex"] = index
rows.append(row)
return rows
def _identity_drift(rows, plan) -> list:
"""核对所有行固定六项逐字一致,且与 plan 一致(漂移即 fail——shadow 的可比性前提)。"""
drifts = []
reference = None
if isinstance(plan, dict) and isinstance(plan.get("identity"), dict):
reference = {k: plan["identity"].get(k) for k in SHADOW20_IDENTITY_FIELDS}
for row in rows:
identity = row.get("identity") if isinstance(row.get("identity"), dict) else {}
values = {k: identity.get(k) for k in SHADOW20_IDENTITY_FIELDS}
if any(not (isinstance(v, str) and v) for v in values.values()):
drifts.append({"gid": row.get("gid"), "reason": "固定六项有空值"})
continue
if reference is None:
reference = values
elif values != reference:
diff = [k for k in SHADOW20_IDENTITY_FIELDS if values.get(k) != reference.get(k)]
drifts.append({"gid": row.get("gid"), "reason": f"身份漂移:{diff}"})
return drifts
def evaluate_shadow20_gate(rows, *, fresh25_gate=None, plan=None, thresholds=None) -> dict:
"""shadow20 达标断言§5.4proof 完整率 100% + 确认假阳 0 + accepted 无 problems/缺证/矛盾共存
+ tester_error=0 + inconclusive≤1 + 人工复核覆盖 + 固定六项零漂移fresh25 达标为硬前置。
row 消费字段gid/outcome/proofComplete/confirmedFalsePositive/problems/contradictions/proofMissing/
rescued/discrepancy/humanReviewed/identity{六项}
"""
th = dict(SHADOW20_THRESHOLDS)
if thresholds:
th.update(thresholds)
rows = [r for r in (rows or []) if isinstance(r, dict)]
warnings = []
checks = []
def check(name, ok, actual, required, detail=""):
checks.append({"name": name, "pass": bool(ok), "actual": actual, "required": required, "detail": detail})
# ── 硬前置§5.4「fresh 25 达标后」):未提供 → 前置无法验证,阻断 PASSwarn提供且未过 → 硬 fail。
if fresh25_gate is None:
warnings.append("未提供 fresh25 闸门结果shadow20 硬前置无法验证PASS 阻断§5.4 顺序硬要求)")
prereq_ok, prereq_actual = False, "未验证"
elif isinstance(fresh25_gate, dict) and fresh25_gate.get("pass") is True:
prereq_ok, prereq_actual = True, "fresh25 达标"
else:
prereq_ok, prereq_actual = False, "fresh25 未达标"
check("prerequisiteFresh25", prereq_ok, prereq_actual, "fresh25 闸门通过§5.4 硬前置)")
check("sampleSize", len(rows) >= th["minSamples"], f"{len(rows)}",
f"连续 ≥ {th['minSamples']} 个真实生产 prompt")
drifts = _identity_drift(rows, plan)
check("identityFixed", rows != [] and not drifts, f"{len(rows) - len(drifts)}/{len(rows)} 行身份一致",
"固定六项commit/Chrome/Actor/Judge/prompt/配置)逐字一致",
";".join(f"{d['gid']}:{d['reason']}" for d in drifts[:3]))
accepted = [r for r in rows if r.get("outcome") == "accept"]
tester_errors = [r for r in rows if r.get("outcome") == "tester_error"]
inconclusives = [r for r in rows if r.get("outcome") == "inconclusive"]
# ── accepted 的 proof 完整率 100%(缺 proofComplete 字段 = 无法证明完整 → fail-closed
proof_incomplete = [r.get("gid") for r in accepted if r.get("proofComplete") is not True]
check("proofCompleteOnAccepted", not proof_incomplete,
f"{len(accepted) - len(proof_incomplete)}/{len(accepted)} 完整", "accepted proof 完整率 100%",
f"不完整={proof_incomplete[:3]}")
# ── accepted 与 problems/缺证/矛盾共存 0。
conflicted = [r.get("gid") for r in accepted
if r.get("problems") or r.get("contradictions") or r.get("proofMissing") is True]
check("acceptedWithoutConflict", not conflicted,
f"{len(accepted) - len(conflicted)}/{len(accepted)} 干净", "accepted 与 problems/缺证/矛盾共存 0",
f"共存={conflicted[:3]}")
confirmed_fp = [r.get("gid") for r in rows if r.get("confirmedFalsePositive") is True]
check("confirmedFalsePositives", not confirmed_fp, len(confirmed_fp), "确认假阳 0", ",".join(confirmed_fp))
check("testerError", len(tester_errors) <= th["maxTesterError"], f"{len(tester_errors)}/{len(rows)}",
f"{th['maxTesterError']}20 局口径 <5% → 0", ",".join(r.get("gid", "?") for r in tester_errors[:3]))
check("inconclusive", len(inconclusives) <= th["maxInconclusive"], f"{len(inconclusives)}/{len(rows)}",
f"{th['maxInconclusive']}20 局口径 <10%", ",".join(r.get("gid", "?") for r in inconclusives[:3]))
# ── 人工复核覆盖:全部 reject/rescued/inconclusive/tester_error/分歧必查 + 普通 accept 至少抽 5。
must_review = [r for r in rows
if r.get("outcome") in ("reject", "inconclusive", "tester_error")
or r.get("rescued") is True or r.get("discrepancy") is True]
unreviewed = [r.get("gid") for r in must_review if r.get("humanReviewed") is not True]
reviewed_accepts = sum(1 for r in accepted if r.get("humanReviewed") is True)
accept_quota = min(th["minOrdinaryAcceptHumanReviewed"], len(accepted))
review_ok = not unreviewed and reviewed_accepts >= accept_quota
check("humanReviewCoverage", review_ok,
f"必查 {len(must_review) - len(unreviewed)}/{len(must_review)}accept 抽查 {reviewed_accepts}/{accept_quota}",
f"分歧/reject/rescued/inconclusive/tester_error 全查 + 普通 accept ≥ {accept_quota}",
f"未复核={unreviewed[:3]}")
outcomes = {"accept": len(accepted), "reject": sum(1 for r in rows if r.get("outcome") == "reject"),
"inconclusive": len(inconclusives), "tester_error": len(tester_errors)}
return {"pass": all(c["pass"] for c in checks), "checks": checks,
"metrics": {"outcomes": outcomes, "sampleSize": len(rows),
"proofCompleteOnAccepted": len(accepted) - len(proof_incomplete)},
"warnings": warnings, "thresholds": th}
# ────────────────────────── CLImini-desktop 真跑后对账用)──────────────────────────
def _cli() -> int:
import argparse
parser = argparse.ArgumentParser(description="三批基线闸门fresh25 / historical / shadow20")
parser.add_argument("gate", choices=("fresh25", "historical", "shadow20"))
parser.add_argument("rows_json", help="批结果行 JSON 数组文件")
parser.add_argument("--fresh25-result", help="shadow20/historical 链式校验fresh25 闸门结果 JSON 文件")
parser.add_argument("--expectations", help="historical 预期表(默认 contracts/play-loop/historical-11-expectations.json")
parser.add_argument("--plan", help="shadow20 计划 JSON核对固定六项")
args = parser.parse_args()
rows = json.loads(Path(args.rows_json).read_text(encoding="utf-8"))
fresh25_gate = None
if args.fresh25_result:
fresh25_gate = json.loads(Path(args.fresh25_result).read_text(encoding="utf-8"))
if args.gate == "fresh25":
report = evaluate_fresh25_gate(rows)
elif args.gate == "historical":
report = evaluate_historical_gate(rows, expectations_path=args.expectations, fresh25_gate=fresh25_gate)
else:
plan = json.loads(Path(args.plan).read_text(encoding="utf-8")) if args.plan else None
report = evaluate_shadow20_gate(rows, fresh25_gate=fresh25_gate, plan=plan)
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0 if report.get("pass") else 1
if __name__ == "__main__":
raise SystemExit(_cli())