feat(planB-p2): U9 M0 core 联合门 turnkey(003-U3)——shell U8 + 三契约结构核

M0 core = 一句话真生成可玩游戏 + 三契约对齐:
- u9_m0_core_gate.py:shell U8 e2e + 运行时读真契约做结构核(API 信封 code==0 / dispatcher=source-project.schema 4键 / manifest=game-package.schema 8键);selftest 5/5
- 读契约 required 防硬编码漂移;缺 artifact 如实记 gap(不伪报)
- README 补「gen-done 接线时须补」4 项(play 路 / 遥测查询 / U8→U9 artifacts / 抽样源)
turnkey 三件齐(U7 rubric + U8 e2e + U9 M0 门),gen-done 落地即跑。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-20 01:38:39 -07:00
parent 4261a638a0
commit 7c2a35888d
2 changed files with 174 additions and 1 deletions

View File

@ -10,7 +10,7 @@
|---|---|---|---|
| **U7 rubric 评分(R9)** | `rubric_score.py` + `rubric-protocol.md` | ✅ 建好(selftest 7/7) | `--template --batch <批manifest> -o sheet.json` → 人工填分 → `--score sheet.json` 判门 |
| **U8 feed真玩 e2e(R4+R11)** | `u8_feed_play_e2e.py`(接 backend_gw 全链 + play.cdp.cjs/前端 player_cdp + 资产对位断言) | ✅ 建好(selftest 8/8 纯逻辑断言;编排步 gated on gen-done) | `--run --prompt "…" --cdp <端点>`:一句话→SAA 生成→发布→feed→试玩→断言(published/in-feed/资产对位/九门/终局遥测)。play 路二选一见文件注(产品前端 :4173 / 本地 bundle 九门) |
| **U9 M0 core 联合门(003-U3)** | `u9_m0_core_gate.*`(组合 U8 + 三契约核) | ⏳ 待建 | 跑 U8 e2e + 核 API/dispatcher/manifest 三契约对齐 |
| **U9 M0 core 联合门(003-U3)** | `u9_m0_core_gate.py`(shell U8 + 运行时读契约结构核) | ✅ 建好(selftest 5/5;读真契约 dispatcher=4键/manifest=8键) | `--run`:跑 U8 e2e + 三契约核(API 信封 code==0 / dispatcher=source-project.schema / manifest=game-package.schema) |
## 门禁判据(plan 003 对齐)
- **U7**:N=8 评分表 + 总均分 ≥ 1.4 + 资产对位项无 0(一票否决)。
@ -22,3 +22,10 @@ harness 的**逻辑/断言/抽样/聚合**与具体生成产物无关——只
## 串行约束
U8/U9 的 mini-desktop CDP 真跑与 Plan A 的 e2e **串行排队**(同机端口 4320/9222、flock)——见 `.agents/skills/game-e2e-cdp-harness.md`。
## gen-done 接线时须补(诚实标注·非现编)
本目录 harness 的**逻辑/断言/抽样/三契约核**已建 + selftest;下列须**待 gen-done 产出形态明确后**接线(现无法验,故标 deferred 不伪报):
1. **U8 play 路二选一**:Plan A 产「staging 发布游戏」→ 走产品前端 :4173 GamePlayer(player_cdp,同 u5u2_walk);产「本地 gameDefinition」→ 走 play.cdp.cjs 本地 bundle 九门。二者断言口径同。
2. **U8 遥测查询接口**:`backend_gw` 现无 `get_telemetry`,终局断言暂记 gap——gen-done 接线时补遥测查询(或经 DB/日志取 `game_end`)。
3. **U8→U9 artifacts 传递**:U9 三契约核需 U8 evidence 带 `api_envelope/source_project/package` 三件 artifact;现 U8 未输出(形态待 gen-done 实物定),U9 已对此记 gap。接线时在 `run_e2e` 补 `ev["artifacts"]`。
4. **抽样源**:U7 `--batch` 须喂 Plan A ≥80% 批 manifest(形态:list[gameId|对象])。

View File

@ -0,0 +1,166 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
U9 M0 core 联合验收门(Plan B Phase-2 / origin 003-U3)—— turnkey 门。
M0 core = 「一句话真生成可玩游戏 + 三契约对齐」:
1. 跑 U8 e2e(create→生成→发布→feed→真玩,u8_feed_play_e2e.run)= 一句话端到端可玩;
2. **三契约结构核**(运行时读契约 required,防漂移):
- API:CommonResult 信封(code==0 + data 非空)—— runtime/package · feed · aigc 关键响应;
- dispatcher:SAA 产出 = source-project.schema.json(schemaVersion/sourceHash/profile/gameDefinition);
- manifest:游戏包 = game-package.schema.json(schemaVersion/gameId/versionId/templateId/gameConfig/assets/manifest/meta)。
门:U8 PASS ∧ 三契约全对齐。
**gated on gen-done + U8**:编排步须 Plan A 产可玩游戏后跑(mini-desktop)。本文件先建 + 三契约结构核 selftest 现验。
"""
import argparse
import json
import os
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.abspath(os.path.join(HERE, "..", "..", "..")) # 仓根(harness 在 docs/agent-specs/<dir>/)
MANIFEST_SCHEMA = os.path.join(REPO, "contracts", "game-package.schema.json")
DISPATCHER_SCHEMA = os.path.join(REPO, "contracts", "agent-loop", "source-project.schema.json")
U8 = os.path.join(HERE, "u8_feed_play_e2e.py")
def _required(schema_path):
"""运行时读契约 required(防硬编码漂移);缺文件抛错。"""
with open(schema_path, encoding="utf-8") as f:
return json.load(f).get("required") or []
def check_required(obj, required):
"""结构核:required 键齐则 ([], True),否则 (缺键列表, False)。"""
obj = obj or {}
missing = [k for k in required if k not in obj]
return missing, len(missing) == 0
# ============== 三契约结构核(gen-done 前可 selftest)==============
def assert_api(envelope):
"""API 契约:CommonResult 信封 code==0 + data 非空。"""
e = envelope or {}
ok = (e.get("code") == 0) and (e.get("data") is not None)
return ok, {"code": e.get("code"), "has_data": e.get("data") is not None}
def assert_dispatcher(source_project):
"""dispatcher 契约:SAA 产出对齐 source-project.schema required。"""
missing, ok = check_required(source_project, _required(DISPATCHER_SCHEMA))
return ok, {"missing": missing}
def assert_manifest(package):
"""manifest 契约:游戏包对齐 game-package.schema required。"""
missing, ok = check_required(package, _required(MANIFEST_SCHEMA))
return ok, {"missing": missing}
def three_contracts(api_envelope, source_project, package):
"""三契约联合核。返回 (passed, detail)。"""
a_ok, a = assert_api(api_envelope)
d_ok, d = assert_dispatcher(source_project)
m_ok, m = assert_manifest(package)
return (a_ok and d_ok and m_ok), {"api": {"ok": a_ok, **a},
"dispatcher": {"ok": d_ok, **d},
"manifest": {"ok": m_ok, **m}}
# ============== 联合门(gated on gen-done + U8)==============
def run_m0_core(prompt, cdp_endpoint, out_dir=None):
"""跑 U8 e2e + 三契约核。返回 verdict dict。"""
# 1. U8 e2e(一句话端到端可玩)—— shell u8,读其 JSON 输出
cmd = [sys.executable, U8, "--run", "--prompt", prompt, "--cdp", cdp_endpoint]
if out_dir:
cmd += ["--out", out_dir]
p = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
try:
# u8 末行打 U8_VERDICT,前面是 JSON;取首个 { 到末个对应 }
txt = p.stdout
u8_ev = json.loads(txt[txt.index("{"):txt.rindex("}") + 1])
except Exception:
u8_ev = {"passed": False, "_parse_error": (p.stdout or p.stderr)[-400:]}
u8_pass = bool(u8_ev.get("passed"))
# 2. 三契约核 —— 从 U8 evidence 取 artifacts(api 信封 / source-project / manifest)
# u8 在 steps 里留有 manifest/gameDef;此处按其 evidence 形态取,缺则记 gap(不伪报)。
api_env = (u8_ev.get("artifacts") or {}).get("api_envelope")
src_proj = (u8_ev.get("artifacts") or {}).get("source_project")
pkg = (u8_ev.get("artifacts") or {}).get("package")
tc_pass, tc = three_contracts(api_env, src_proj, pkg)
artifacts_present = all(x is not None for x in (api_env, src_proj, pkg))
passed = u8_pass and tc_pass and artifacts_present
return {
"passed": passed,
"u8_passed": u8_pass,
"three_contracts_passed": tc_pass,
"artifacts_present": artifacts_present,
"three_contracts": tc,
"u8": u8_ev,
"gap": None if artifacts_present else "U8 evidence 未带 api_envelope/source_project/package 三件 artifact——gen-done 接线时补 u8 artifacts 输出",
}
# ============== selftest(三契约结构核)==============
def _selftest():
# 用真契约文件读 required,构造 好/缺键 fixture
req_disp = _required(DISPATCHER_SCHEMA)
req_mani = _required(MANIFEST_SCHEMA)
assert req_disp and req_mani, "契约 required 读取为空"
good_disp = {k: "x" for k in req_disp}
good_mani = {k: "x" for k in req_mani}
good_api = {"code": 0, "data": {"x": 1}}
# 全好 → 过
ok, tc = three_contracts(good_api, good_disp, good_mani); assert ok, tc
# API code!=0 → 不过
ok, tc = three_contracts({"code": 500, "data": None}, good_disp, good_mani)
assert (not ok) and (not tc["api"]["ok"]), tc
# dispatcher 缺键 → 不过 + 列缺
bad_disp = {k: "x" for k in req_disp[1:]} # 缺第一个
ok, tc = three_contracts(good_api, bad_disp, good_mani)
assert (not ok) and tc["dispatcher"]["missing"] == [req_disp[0]], tc
# manifest 缺键 → 不过
bad_mani = {k: "x" for k in req_mani[:-1]} # 缺最后一个
ok, tc = three_contracts(good_api, good_disp, bad_mani)
assert (not ok) and tc["manifest"]["missing"] == [req_mani[-1]], tc
# 空对象 → 全缺
ok, tc = three_contracts({}, {}, {})
assert (not ok) and len(tc["dispatcher"]["missing"]) == len(req_disp), tc
print(f"u9_m0_core_gate selftest: 5/5 PASS(三契约结构核;读真契约 dispatcher={len(req_disp)}键 manifest={len(req_mani)}键)")
def _print(v):
print("=== U9 M0 core 联合门 ===")
print(f" U8 e2e(一句话端到端可玩): {'✓' if v['u8_passed'] else '✗'}")
print(f" artifacts 齐(api/dispatcher/manifest 三件): {'✓' if v['artifacts_present'] else '✗ ' + (v.get('gap') or '')}")
tc = v["three_contracts"]
print(f" 三契约: API {'✓' if tc['api']['ok'] else '✗'} | dispatcher {'✓' if tc['dispatcher']['ok'] else '✗ 缺' + str(tc['dispatcher'].get('missing'))} | manifest {'✓' if tc['manifest']['ok'] else '✗ 缺' + str(tc['manifest'].get('missing'))}")
print(f" M0_CORE_VERDICT={'PASS' if v['passed'] else 'FAIL'}")
def main():
ap = argparse.ArgumentParser(description="U9 M0 core 联合门(gated on gen-done + U8)")
ap.add_argument("--selftest", action="store_true", help="三契约结构核自测(读真契约;gen-done 前可跑)")
ap.add_argument("--run", action="store_true", help="真跑 M0 core(须 gen-done + mini-desktop CDP)")
ap.add_argument("--prompt", default="一个躲避陨石的小游戏")
ap.add_argument("--cdp", default="http://localhost:9222")
ap.add_argument("--out")
args = ap.parse_args()
if args.selftest:
_selftest(); return 0
if args.run:
v = run_m0_core(args.prompt, args.cdp, args.out)
_print(v)
return 0 if v["passed"] else 1
ap.print_help(); return 2
if __name__ == "__main__":
sys.exit(main())