diff --git a/cheap-worker/cheap_assert.py b/cheap-worker/cheap_assert.py new file mode 100644 index 00000000..119b16f9 --- /dev/null +++ b/cheap-worker/cheap_assert.py @@ -0,0 +1,76 @@ +"""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"]), + } diff --git a/cheap-worker/cheap_modify.py b/cheap-worker/cheap_modify.py index 4fbd7201..6a443ae8 100644 --- a/cheap-worker/cheap_modify.py +++ b/cheap-worker/cheap_modify.py @@ -27,6 +27,7 @@ import json import re from pathlib import Path +import cheap_assert import cheap_run # A11 M4 模块重生成:behavior 在便宜档映射到玩法文件(KTD4)——只重写它,其余文件保持不变。 @@ -272,7 +273,7 @@ def materialize_source_project(game_id: str, files) -> Path: # ───────────────────────── ③ execute_deterministic_modify(集成) ───────────────────────── def execute_deterministic_modify(game_id: str, source_project, modify_patch, - *, port: int = 4320, cdp_port: int = 9222) -> dict: + *, base_version_id=None, port: int = 4320, cdp_port: int = 9222) -> dict: """确定性执行集成:apply 改一处 → 落盘已知良源 → 重建 → 九门。复用 cheap_run 九门,不另造。 流程:apply_deterministic_modify → 若 ok:(缺 plumbing 才 scaffold 已知良 entry-bundle/index.html)→ @@ -326,11 +327,21 @@ def execute_deterministic_modify(game_id: str, source_project, modify_patch, pr = cheap_run.play(game_id, port=port, cdp_port=cdp_port) verdict = pr.get("verdict") gates_pass = isinstance(verdict, dict) and verdict.get("pass") is True - status = "succeeded" if gates_pass else "failed" - log(f"确定性改重建+九门完成 game_id={game_id} status={status} smokeOk={sm.get('ok')} playOk={pr.get('ok')}") - return {"status": status, "manifest": manifest, "verdict": verdict, - "stage": "play", "error": None if status == "succeeded" else "九门未通过", - "smokeOk": sm.get("ok"), "playOk": pr.get("ok")} + + # M5 三结构断言:base→改后 真比对(断言①改动真生效 / 断言②非目标稳)+ 血缘(③ baseVersionId)。 + # 断言②对 deterministic 是兜 apply bug(理应只改目标文件,真比对抓"是否顺手碰了非目标")。 + base_files = _as_dict(source_project).get("files") if isinstance(_as_dict(source_project).get("files"), dict) else {} + target_files = {m["file"] for m in manifest if m.get("file")} + assertions = cheap_assert.three_assertions(manifest, base_files, applied["files"], target_files, base_version_id) + a12_pass = assertions["a1_changeApplied"]["pass"] and assertions["a2_nontargetStable"]["pass"] + status = "succeeded" if (gates_pass and a12_pass) else "failed" + err = None + if status != "succeeded": + err = "九门未通过" if not gates_pass else "三断言未通过:" + json.dumps( + {k: v for k, v in assertions.items() if isinstance(v, dict) and not v.get("pass")}, ensure_ascii=False) + log(f"确定性改重建+九门+三断言完成 game_id={game_id} status={status} gates={gates_pass} a12={a12_pass}") + return {"status": status, "manifest": manifest, "verdict": verdict, "assertions": assertions, + "stage": "play", "error": err, "smokeOk": sm.get("ok"), "playOk": pr.get("ok")} # ───────────────────────── ④ execute_regenerate_modify(模块重生成 · 改玩法)───────────────────────── @@ -368,10 +379,12 @@ def _regen_manifest(intent: str, *, changed, stable) -> dict: "changed": changed, "untouchedStable": stable} -def _regen_why(gates_pass: bool, untouched_stable: bool) -> str: - """失败诊断文案:断言②泄漏优先(误伤非目标比九门不过更严重)。""" +def _regen_why(gates_pass: bool, untouched_stable: bool, changed: bool = True) -> str: + """失败诊断文案:断言②泄漏优先(误伤非目标比九门不过更严重)→ 断言①no-op → 九门。""" if not untouched_stable: return "断言②失败:模块重生成误伤了非目标文件(core/render/assets/host-config 等应保持不变)" + if not changed: + return "断言①失败:模块重生成未改变 game-logic.js(no-op,目标文件没动)" if not gates_pass: return "九门未通过" return "模块重生成失败" @@ -408,7 +421,7 @@ def _default_rewrite(game_id: str, intent: str, *, port: int = 4320, cdp_port: i def execute_regenerate_modify(game_id: str, source_project, modify_patch, - *, rewrite_fn=None, port: int = 4320, cdp_port: int = 9222) -> dict: + *, base_version_id=None, rewrite_fn=None, port: int = 4320, cdp_port: int = 9222) -> dict: """模块重生成执行(改玩法):取 base 源 + intent → 有界单文件重写 game-logic.js → 九门 → 断言②非目标稳定 + 改动清单。 流程:取 intent(空→failed 不重写)→ 取 base 源 files(无源→failed)→ scaffold-if-needed + materialize @@ -487,14 +500,25 @@ def execute_regenerate_modify(game_id: str, source_project, modify_patch, "verdict": None, "summary": None, "error": f"模块重生成异常:{type(e).__name__}: {e}"} - # 7. 据九门 verdict + 断言②判 status:九门过 ∧ 非目标稳 才算成功(误伤非目标=边界泄漏,即便九门过也判失败)。 + # 7. 据九门 verdict + 三断言判 status:九门过 ∧ 改动真生效(①) ∧ 非目标稳(②) 才算成功 + # (误伤非目标=边界泄漏、目标没变=no-op,即便九门过也判失败)。 verdict_full = run_summary.get("verdictFull") if isinstance(run_summary.get("verdictFull"), dict) else None vb = run_summary.get("verdict") if isinstance(run_summary.get("verdict"), dict) else None gates_pass = bool(vb and vb.get("pass") is True) - status = "succeeded" if (gates_pass and untouched_stable) else "failed" + # 三断言 verdict(统一形态,与 deterministic 路一致):①changed ②非目标稳(含误伤清单)③血缘。 + nontarget_changed = sorted(k for k in set(nontarget_before) | set(nontarget_after) + if nontarget_before.get(k) != nontarget_after.get(k)) + assertions = { + "a1_changeApplied": {"pass": changed, "reason": "" if changed else "目标文件未变(no-op)"}, + "a2_nontargetStable": {"pass": untouched_stable, "nontargetChanged": nontarget_changed}, + "a3_lineageQueryable": cheap_assert.assert_lineage(base_version_id), + "allPass": bool(changed and untouched_stable and base_version_id is not None), + } + a12_pass = changed and untouched_stable + status = "succeeded" if (gates_pass and a12_pass) else "failed" stage = run_summary.get("stage") or "code" log(f"模块重生成完成 game_id={game_id} status={status} changed={changed} " f"untouchedStable={untouched_stable} gatesPass={gates_pass} stage={stage}") return {"status": status, "stage": stage, "manifest": manifest, - "verdict": verdict_full, "summary": run_summary, - "error": None if status == "succeeded" else _regen_why(gates_pass, untouched_stable)} + "verdict": verdict_full, "summary": run_summary, "assertions": assertions, + "error": None if status == "succeeded" else _regen_why(gates_pass, untouched_stable, changed)} diff --git a/cheap-worker/tests/test_a11_m3_deterministic.py b/cheap-worker/tests/test_a11_m3_deterministic.py index f024e1e5..0b58bca5 100644 --- a/cheap-worker/tests/test_a11_m3_deterministic.py +++ b/cheap-worker/tests/test_a11_m3_deterministic.py @@ -209,7 +209,7 @@ def test_worker_deterministic_route_attaches_manifest_on_failed(): fake = {"status": "failed", "manifest": [{"file": "src/core.js", "kind": "config", "key": "X", "old": None, "new": "5", "found": False}], "verdict": None, "error": "no-op:常量未找到", "stage": "apply"} - state = W.WorkerState(modify_fn=lambda gid, sp, patch: fake, + state = W.WorkerState(modify_fn=lambda gid, sp, patch, **kw: fake, send_fn=lambda u, p, s: captured.update(payload=p) or (200, ""), profile_fn=lambda j, g: None) job = {"job_id": "m1", "traceId": "m1", "modifyMode": "deterministic", "gameId": "nope-x", @@ -234,7 +234,7 @@ def test_worker_deterministic_route_success_carries_bundle_and_source(): "old": "30000", "new": "20000", "found": True}], "verdict": {"pass": True, "guards": {}}, "error": None, "stage": "play"} profile = {"tickModel": "realtime", "inputModel": "discrete-choice", "progressModel": "metric"} - state = W.WorkerState(modify_fn=lambda *a: fake, + state = W.WorkerState(modify_fn=lambda *a, **kw: fake, send_fn=lambda u, p, s: captured.update(payload=p) or (200, ""), profile_fn=lambda j, g: profile) job = {"job_id": "m2", "traceId": "m2", "modifyMode": "deterministic", "gameId": gid, diff --git a/cheap-worker/tests/test_a11_m4_regenerate.py b/cheap-worker/tests/test_a11_m4_regenerate.py index 5a0614c3..9b8984dc 100644 --- a/cheap-worker/tests/test_a11_m4_regenerate.py +++ b/cheap-worker/tests/test_a11_m4_regenerate.py @@ -188,7 +188,7 @@ def test_worker_regenerate_route_success_carries_manifest_bundle_source(): "attempts": 4, "stage": "play", "costRmb": 0.7, "models": {"code": "MiniMax-M3"}, "gameId": gid}, "error": None} profile = {"tickModel": "realtime", "inputModel": "discrete-choice", "progressModel": "metric"} - state = W.WorkerState(regen_fn=lambda *a: fake, + state = W.WorkerState(regen_fn=lambda *a, **kw: fake, send_fn=lambda u, p, s: captured.update(payload=p) or (200, ""), profile_fn=lambda j, g: profile) job = {"job_id": "rmok", "traceId": "rmok", "modifyMode": "regenerate-module", "gameId": gid, @@ -214,7 +214,7 @@ def test_worker_regenerate_route_failed_attaches_error_and_manifest(): "changed": True, "untouchedStable": False}, "verdict": {"pass": True, "guards": {}}, "summary": None, "error": "断言②失败:模块重生成误伤了非目标文件"} - state = W.WorkerState(regen_fn=lambda *a: fake, + state = W.WorkerState(regen_fn=lambda *a, **kw: fake, send_fn=lambda u, p, s: captured.update(payload=p) or (200, ""), profile_fn=lambda j, g: None) job = {"job_id": "rmf", "traceId": "rmf", "modifyMode": "regenerate-module", "gameId": gid, @@ -226,7 +226,7 @@ def test_worker_regenerate_route_failed_attaches_error_and_manifest(): assert "sourceProject" not in captured["payload"], "失败路不带新版源" -def _boom_regen(*a): +def _boom_regen(*a, **kw): raise AssertionError("deterministic/create 路不应调用 regen_fn") @@ -240,7 +240,7 @@ def test_worker_deterministic_unaffected_by_regen_wiring(): det = {"status": "succeeded", "manifest": [{"file": "src/core.js", "kind": "config", "key": "SPEED", "old": "5", "new": "8", "found": True}], "verdict": {"pass": True, "guards": {}}, "error": None, "stage": "play"} - state = W.WorkerState(modify_fn=lambda *a: det, regen_fn=_boom_regen, + state = W.WorkerState(modify_fn=lambda *a, **kw: det, regen_fn=_boom_regen, send_fn=lambda u, p, s: captured.update(payload=p) or (200, ""), profile_fn=lambda j, g: None) job = {"job_id": "det1", "traceId": "det1", "modifyMode": "deterministic", "gameId": gid, diff --git a/cheap-worker/tests/test_a11_m5_assertions.py b/cheap-worker/tests/test_a11_m5_assertions.py new file mode 100644 index 00000000..f0d21f31 --- /dev/null +++ b/cheap-worker/tests/test_a11_m5_assertions.py @@ -0,0 +1,117 @@ +"""test_a11_m5_assertions.py — A11 切片三 M5 三结构断言机器门单测。 + +plan① 线 154 的 A11 完成判据(九门之外补三条,验"改对了没、有没有误伤"): + ① 改动真生效(非 no-op):请求的结构化改动可断言真实落地; + ② 非目标稳定:除目标文件外,其余文件字节未变(没顺手改坏别的模块); + ③ 血缘可查:新版本带 baseVersionId(可回溯改自哪一版)。 +三断言跨两档统一(deterministic manifest=list / regenerate manifest=dict),由 base→new files 真比对得出, +不只信执行器自报的标记(防执行器 bug 自证)。 + +跑:cheap-worker/.venv/bin/python cheap-worker/tests/test_a11_m5_assertions.py +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # → cheap-worker/ +import cheap_assert as A # noqa: E402 + +_BASE = { + "src/core.js": "export const ROUND_MS = 30000;\n", + "src/game-logic.js": "// 玩法 v1\n", + "src/render.js": "// 渲染\n", + "src/assets.js": "export const IMAGE_FILES = {};\n", +} + + +# ── 断言①:改动真生效(非 no-op)── + +def test_a1_deterministic_real_change(): + manifest = [{"file": "src/core.js", "kind": "config", "key": "ROUND_MS", "old": "30000", "new": "20000", "found": True}] + assert A.assert_change_applied(manifest)["pass"] is True + + +def test_a1_deterministic_noop_found_false(): + manifest = [{"file": "src/core.js", "kind": "config", "key": "NOPE", "old": None, "new": None, "found": False}] + assert A.assert_change_applied(manifest)["pass"] is False + + +def test_a1_deterministic_noop_same_value(): + manifest = [{"file": "src/core.js", "kind": "config", "key": "ROUND_MS", "old": "30000", "new": "30000", "found": True}] + assert A.assert_change_applied(manifest)["pass"] is False, "found 但值没变=no-op" + + +def test_a1_regenerate_changed_true(): + assert A.assert_change_applied({"file": "src/game-logic.js", "kind": "behavior", "changed": True})["pass"] is True + + +def test_a1_regenerate_changed_false(): + assert A.assert_change_applied({"file": "src/game-logic.js", "kind": "behavior", "changed": False})["pass"] is False + + +# ── 断言②:非目标稳定(base→new 真比对,不信自报)── + +def test_a2_only_target_changed(): + new = dict(_BASE) + new["src/core.js"] = "export const ROUND_MS = 20000;\n" # 只改目标 + out = A.assert_nontarget_stable(_BASE, new, target_files={"src/core.js"}) + assert out["pass"] is True + assert out["nontargetChanged"] == [] + + +def test_a2_collateral_damage_caught(): + new = dict(_BASE) + new["src/core.js"] = "export const ROUND_MS = 20000;\n" + new["src/render.js"] = "// 渲染 被顺手改坏\n" # 误伤非目标 + out = A.assert_nontarget_stable(_BASE, new, target_files={"src/core.js"}) + assert out["pass"] is False, "误伤非目标必须被抓" + assert "src/render.js" in out["nontargetChanged"] + + +def test_a2_regenerate_target_gamelogic(): + new = dict(_BASE) + new["src/game-logic.js"] = "// 玩法 v2 连击\n" + out = A.assert_nontarget_stable(_BASE, new, target_files={"src/game-logic.js"}) + assert out["pass"] is True + + +# ── 断言③:血缘可查(baseVersionId 在)── + +def test_a3_lineage_present(): + assert A.assert_lineage(4096)["pass"] is True + + +def test_a3_lineage_missing(): + assert A.assert_lineage(None)["pass"] is False + + +# ── 合并三断言 verdict ── + +def test_three_assertions_all_pass(): + new = dict(_BASE) + new["src/core.js"] = "export const ROUND_MS = 20000;\n" + manifest = [{"file": "src/core.js", "kind": "config", "key": "ROUND_MS", "old": "30000", "new": "20000", "found": True}] + v = A.three_assertions(manifest, _BASE, new, target_files={"src/core.js"}, base_version_id=4096) + assert v["allPass"] is True + assert v["a1_changeApplied"]["pass"] and v["a2_nontargetStable"]["pass"] and v["a3_lineageQueryable"]["pass"] + + +def test_three_assertions_fails_on_collateral(): + new = dict(_BASE) + new["src/core.js"] = "export const ROUND_MS = 20000;\n" + new["src/render.js"] = "// 误伤\n" + manifest = [{"file": "src/core.js", "kind": "config", "key": "ROUND_MS", "old": "30000", "new": "20000", "found": True}] + v = A.three_assertions(manifest, _BASE, new, target_files={"src/core.js"}, base_version_id=4096) + assert v["allPass"] is False, "误伤非目标 → 整体不过" + + +def _run_all(): + fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)] + for fn in fns: + fn() + print(f" ✓ {fn.__name__}") + print(f"\n[test_a11_m5_assertions] {len(fns)}/{len(fns)} passed") + + +if __name__ == "__main__": + _run_all() diff --git a/cheap-worker/worker_service.py b/cheap-worker/worker_service.py index 2b72b310..d56fb622 100644 --- a/cheap-worker/worker_service.py +++ b/cheap-worker/worker_service.py @@ -277,7 +277,8 @@ def _process_modify_job(state: WorkerState, job: dict, modify: dict) -> dict: import cheap_modify modify_fn = cheap_modify.execute_deterministic_modify - result = modify_fn(game_id, modify.get("sourceProject"), modify.get("modifyPatch")) + result = modify_fn(game_id, modify.get("sourceProject"), modify.get("modifyPatch"), + base_version_id=modify.get("baseVersionId")) # M5 三断言③血缘透传 summary = _modify_summary(result, game_id) game_dir = cheap_run.game_dir(game_id) @@ -353,7 +354,8 @@ def _process_regenerate_job(state: WorkerState, job: dict, modify: dict) -> dict import cheap_modify regen_fn = cheap_modify.execute_regenerate_modify - result = regen_fn(game_id, modify.get("sourceProject"), modify.get("modifyPatch")) + result = regen_fn(game_id, modify.get("sourceProject"), modify.get("modifyPatch"), + base_version_id=modify.get("baseVersionId")) # M5 三断言③血缘透传 summary = _regen_summary(result, game_id) game_dir = cheap_run.game_dir(game_id) diff --git a/docs/plans/2026-06-28-003-feat-studio-A11-对话式调整回路-plan.md b/docs/plans/2026-06-28-003-feat-studio-A11-对话式调整回路-plan.md index e9545b85..00f2b58d 100644 --- a/docs/plans/2026-06-28-003-feat-studio-A11-对话式调整回路-plan.md +++ b/docs/plans/2026-06-28-003-feat-studio-A11-对话式调整回路-plan.md @@ -306,6 +306,13 @@ stateDiagram-v2 - **一次真 LLM smoke(子代理单跑,本机 Chrome + new-api M3)**:base=`amgen-bake-click-score-0`、intent="把命中加分从固定改成连击递增" → **status=succeeded、untouchedStable=True、九门 verdict.pass=True、attempts=1、¥0.31、137s、无熔断**;独立 diff 佐证 5 个非目标文件字节相同、仅 game-logic.js 变且真实现连击递增。**M4 plumbing 通**(重写发生 + 写边界守住 + 非目标稳 + 九门跑到 verdict);此次质量也恰好达标(质量归 agent 层、不在 M4 判据)。 - **M4 收口**:两档执行(deterministic + regenerate-module)全落地;断言②地基(非目标 hash 稳)就位。 +**M5 验收闭环 · 三断言机器门(完成、绿,2026-06-29)· 受计费 e2e 评估中** + +- **`cheap_assert.py` 三结构断言**:① change_applied(改动真生效非 no-op:deterministic=manifest 有 found 且 old≠new / regenerate=changed);② nontarget_stable(**base→new 真比对**算内容差,非目标文件变=误伤——**不信执行器自报标记**,防执行器 bug 自证,兑现 Opus P1-2);③ lineage_queryable(baseVersionId 在,worker 侧验必要条件,DB 真可查由后端 e2e + M1 回填)。`three_assertions` 合并 verdict。`test_a11_m5_assertions` 12/12。 +- **集成进 execute_***:两档执行加 `base_version_id` 参 + 计算并附 `assertions` verdict + 收紧 status 判据(succeeded ⟺ 九门 ∧ ① ∧ ②;regenerate 原漏判 ①changed 已补,no-op 重写不再算成功);worker `_process_*_job` 透传 `modify.baseVersionId`。全 **85 测试**独立复跑全绿(含 M3/M4 注入桩接 kwarg 修)。 +- **集成端到端 smoke(主会话独立、真九门)**:ROUND_MS 30000→19000 + base_version_id=4096 → **status=succeeded、a1/a2/a3 全 true、allPass=true、落盘 core.js=19000**。三断言真接进执行器、清洁改动全过。 +- **受计费真后端 e2e(mini-desktop)**:A11 全链路代码已完成 + 本机端到端逐段验证(判意图链路 / 两档执行 + 真九门 / 三断言 / 血缘回填均已一手坐实)。剩最终集成证 = 经真后端(studio `/modify/plan`+`/modify` → aigc → worker → 回调 → 新预览版 + D12 计费 + base_version_id 可查)跑通一次受计费调整任务,需 mini-desktop 全栈起。 + ## 状态 双评审已过(发现全处置)→ 待创始人批 → 执行(M1→M5 顺序,M1 为 plan① 所称"首个里程碑")。