"""cheap_assert.py — A11 切片三 M5 三结构断言机器门。 plan① 线 154:A11 完成判据 = 九门 + 三层校验 + 三结构断言。九门只验"仍能玩",验不出"改对了没、有没有 误伤";三断言补这一层: ① change_applied —— 请求的结构化改动真实生效(非 no-op 重构); ② nontarget_stable —— 除目标文件外其余字节未变(没顺手改坏别的模块); ③ lineage_queryable —— 新版本带 baseVersionId(可回溯改自哪一版)。 设计要点: · 断言②由 base→new files **真比对**得出(算内容差),**不信执行器自报的 untouchedStable 标记**——防执行器 自身 bug 自证(评审 Opus P1-2:目标文件内顺手改坏、或执行器误报,真比对才抓得到)。 · 跨两档统一:deterministic manifest=list[{file,kind,key,old,new,found}] / regenerate manifest=dict {file,kind,intent,changed,untouchedStable};assert_change_applied 两形态都认。 · 纯函数无网络无 LLM、可单测;断言③的"可查"在 worker 侧只验 baseVersionId 在(必要条件),DB 真可查由 受计费后端 e2e 验(M1 已把 base_version_id 回填 game_source_project)。 """ def assert_change_applied(manifest) -> dict: """断言① 改动真生效(非 no-op)。 deterministic(manifest=list):至少一条 found 且 old≠new(字符串化比对,容数值/字符串); regenerate(manifest=dict):changed 为真。返 {pass, reason}。 """ if isinstance(manifest, list): applied = [m for m in manifest if m.get("found") and str(m.get("old")) != str(m.get("new"))] return {"pass": bool(applied), "applied": len(applied), "reason": "" if applied else "无任一条 found 且值真变化(no-op)"} if isinstance(manifest, dict): ok = bool(manifest.get("changed")) return {"pass": ok, "reason": "" if ok else "regenerate changed=false(目标文件未变,no-op)"} return {"pass": False, "reason": f"manifest 形态未知:{type(manifest).__name__}"} def _changed_files(base_files: dict, new_files: dict) -> set: """base→new 之间内容真变了的文件集(含新增/删除);base/new 形如 路径→文本。""" base = base_files or {} new = new_files or {} keys = set(base) | set(new) return {k for k in keys if base.get(k) != new.get(k)} def assert_nontarget_stable(base_files: dict, new_files: dict, target_files) -> dict: """断言② 非目标稳定:base→new 真变了的文件必须 ⊆ 目标文件集;有非目标变动 = 误伤。 target_files:允许改动的目标文件相对路径集合(deterministic=manifest 涉及的文件;regenerate={game-logic.js})。 返 {pass, changedFiles, nontargetChanged}。 """ targets = set(target_files or ()) changed = _changed_files(base_files, new_files) nontarget = sorted(changed - targets) return {"pass": not nontarget, "changedFiles": sorted(changed), "nontargetChanged": nontarget} def assert_lineage(base_version_id) -> dict: """断言③ 血缘可查:新版本带 baseVersionId(worker 侧验必要条件;DB 真可查由后端 e2e 验)。""" ok = base_version_id is not None return {"pass": ok, "baseVersionId": base_version_id, "reason": "" if ok else "缺 baseVersionId(血缘断链)"} def three_assertions(manifest, base_files: dict, new_files: dict, target_files, base_version_id) -> dict: """合并三断言 verdict:三条全过才算"改成"(配合九门 + 三层校验)。 Returns: {a1_changeApplied, a2_nontargetStable, a3_lineageQueryable, allPass}。 """ a1 = assert_change_applied(manifest) a2 = assert_nontarget_stable(base_files, new_files, target_files) a3 = assert_lineage(base_version_id) return { "a1_changeApplied": a1, "a2_nontargetStable": a2, "a3_lineageQueryable": a3, "allPass": bool(a1["pass"] and a2["pass"] and a3["pass"]), }