games-development-ai/contracts/play-loop/test_reference_asset_and_acceptance_v3.py
lili cbfd4d871b
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(acceptance): 闭合 playtest v3 与 A+ 可信消费链
固化 Match-3 生产者、视觉、音频与双 Judge 证据闭包。

将《山海行纪》r1.1 绑定新的不可变 release,并以生产预检现场核验 bundle、Registry/2 和 25 项 Writer 快照。

同步地图1平衡锁值、跨游戏回归修复、验收契约与 SoT 证据。
2026-07-28 20:16:13 -07:00

205 lines
12 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.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""W-GOLD-LIVE 检查点 1 离线验证:参照资产契约 + acceptance v3 开口向后兼容。
零依赖确定性测试,不烧真模型、不触 live 注入:复用 validate.py 内置的 stdlib 子集校验器
(与 `python3 validate.py --suite` 同一机制),验证四件事:
① 四个新 schemaacceptance-request-v3 / acceptance-provenance-v3 /
reference-asset-record / reference-asset-registry自洽正样本过、负样本拦
② 迁移清单初始数据 reference-asset-registry.initial.json 符合 registry schema
(结构层 + recordId 唯一/migration-list 禁 active 语义层);
③ acceptance-request/3 与 acceptance-provenance/3 带/不带新字段都 valid向后兼容
新字段全部 optionalv2 字段集原样可过 v3
④ 旧 v2 acceptance 样本在 v2 schema 下仍 valid、负样本仍被拦v2 契约未被破坏)。
另加金标 SoT §7 对账断言:迁移清单编码的条目数/角色/生命周期与 SoT 表一致,未激活记录的
占位 hash 全部在 migrationNotes 标注,正式 game_content_gold 当前仅《山海行纪》1 款。
运行python3 contracts/play-loop/test_reference_asset_and_acceptance_v3.py
全绿 exit 0任一失败 exit 1 并打印失败证据。
"""
from __future__ import annotations
import json
import hashlib
import re
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import validate as V # noqa: E402 —— 同目录 stdlib 校验器,唯一依赖
_PASS: list = []
_FAIL: list = []
def check(name: str, ok: bool, detail: str = "") -> None:
"""记录并打印单条断言;失败时附原始证据,便于复核而非只看红绿。"""
(_PASS if ok else _FAIL).append((name, detail))
line = ("PASS " if ok else "FAIL ") + name
if not ok and detail:
line += f"\n 证据:{detail}"
print(line)
def errors_of(schema_file: str, instance) -> list:
"""结构层 + 语义层完整校验,返回错误列表(空=合规)。与 _validate_file 同口径。"""
schema = V._load((HERE / schema_file).resolve())
errs = V.validate(schema, instance, schema)
if not errs:
errs += V._semantic_validate(schema, instance)
return errs
def sample(rel_path: str):
"""按套件口径读取样本(支持 _base/_set/_delete 声明式派生)。"""
return V._load_sample_instance((HERE / rel_path).resolve())
def main() -> int:
# ── ① 新 schema 自洽:正样本过、负样本拦 ──────────────────────────────
for schema_file, contract in [
("acceptance-request-v3.schema.json", "acceptance-request-v3"),
("acceptance-provenance-v3.schema.json", "acceptance-provenance-v3"),
("reference-asset-record.schema.json", "reference-asset-record"),
]:
valid_dir = HERE / "samples" / contract / "valid"
invalid_dir = HERE / "samples" / contract / "invalid"
for f in sorted(valid_dir.glob("*.json")):
errs = errors_of(schema_file, sample(f"samples/{contract}/valid/{f.name}"))
check(f"{contract} 正样本通过:{f.name}", not errs, "; ".join(errs))
for f in sorted(invalid_dir.glob("*.json")):
errs = errors_of(schema_file, sample(f"samples/{contract}/invalid/{f.name}"))
check(f"{contract} 负样本被拦:{f.name}", bool(errs), "负样本竟然通过")
# registry 正样本 = 迁移清单初始数据canonical套件同口径
registry = json.loads((HERE / "reference-asset-registry.initial.json").read_text(encoding="utf-8"))
registry_errs = errors_of("reference-asset-registry.schema.json", registry)
check("② 迁移清单初始数据符合 registry schema结构+语义)", not registry_errs,
"; ".join(registry_errs))
for f in sorted((HERE / "samples/reference-asset-registry/invalid").glob("*.json")):
instance = sample(f"samples/reference-asset-registry/invalid/{f.name}")
errs = errors_of("reference-asset-registry.schema.json", instance)
check(f"② registry 负样本被拦:{f.name}", bool(errs), "负样本竟然通过")
# ── ③ v3 向后兼容:同身份数据不带新字段也过 v3v2 原样数据不过 v3版本线独立──
v2_base = json.loads(
(HERE / "samples/acceptance-request-v2/valid/01-absent-puzzle.json").read_text(encoding="utf-8"))
as_v3 = {**v2_base, "schemaVersion": "acceptance-request/3"}
errs = errors_of("acceptance-request-v3.schema.json", as_v3)
check("③ v2 身份数据仅改 schemaVersion=3 即过 v3新字段全 optional", not errs,
"; ".join(errs))
errs = errors_of("acceptance-request-v3.schema.json", dict(v2_base))
check("③ v2 原样数据schemaVersion/2被 v3 const 拒绝(版本线独立、非就地放松)",
bool(errs), "v2 原样数据竟通过 v3")
v2_prov = json.loads(
(HERE / "samples/acceptance-provenance-v2/valid/01-absent-puzzle.json").read_text(encoding="utf-8"))
errs = errors_of("acceptance-provenance-v3.schema.json",
{**v2_prov, "schemaVersion": "acceptance-provenance/3"})
check("③ v2 provenance 身份数据仅改 schemaVersion=3 即过 v3", not errs, "; ".join(errs))
# ── ④ 旧 v2 契约不被破坏v2 样本在 v2 schema 下仍过/仍拦 ──────────────
for f in sorted((HERE / "samples/acceptance-request-v2/valid").glob("*.json")):
errs = errors_of("acceptance-request-v2.schema.json",
sample(f"samples/acceptance-request-v2/valid/{f.name}"))
check(f"④ 旧 v2 request 正样本仍 valid{f.name}", not errs, "; ".join(errs))
for f in sorted((HERE / "samples/acceptance-request-v2/invalid").glob("*.json")):
errs = errors_of("acceptance-request-v2.schema.json",
sample(f"samples/acceptance-request-v2/invalid/{f.name}"))
check(f"④ 旧 v2 request 负样本仍被拦:{f.name}", bool(errs), "负样本竟然通过")
for f in sorted((HERE / "samples/acceptance-provenance-v2/valid").glob("*.json")):
errs = errors_of("acceptance-provenance-v2.schema.json",
sample(f"samples/acceptance-provenance-v2/valid/{f.name}"))
check(f"④ 旧 v2 provenance 正样本仍 valid{f.name}", not errs, "; ".join(errs))
# ── ⑤ 迁移清单编码对账金标 SoT §7 ────────────────────────────────────
records = registry["records"]
by_id = {r["recordId"]: r for r in records}
check("⑤ 迁移清单共 15 条且 recordId 唯一", len(records) == 15 and len(by_id) == 15,
f"records={len(records)} unique={len(by_id)}")
gold_m3 = [f"gold-m3-{kind}-r3" for kind in ("gem", "candy", "fruit", "porcelain", "rune")]
ok = all(by_id[rid]["role"] == "harness_fixture"
and by_id[rid]["lifecycleStatus"] == "migration_pending" for rid in gold_m3)
check("⑤ 五款 gold-m3-*-r3 → harness_fixture/migration_pending", ok)
exemplars = [f"_fewshot-{k}" for k in ("feiyi", "puzzle")] + \
[f"_template-{k}" for k in ("feiyi", "puzzle", "shop", "story", "trpg")]
ok = all(by_id[rid]["role"] == "generation_exemplar"
and by_id[rid]["lifecycleStatus"] == "migration_pending" for rid in exemplars)
check("⑤ 2 款 _fewshot + 5 款 _template → generation_exemplar/migration_pending", ok)
fable_candidates = ["gac-shanhai-xunyi-lu", "gac-yeshi-yitiaojie"]
ok = all(by_id[rid]["role"] == "game_content_gold"
and by_id[rid]["lifecycleStatus"] == "candidate" for rid in fable_candidates)
check("⑤ Fable 其余两款 → game_content_gold/candidate", ok)
# 设计引用必须逐字锁定已批准的两份 designIntent不能只以“恰有两份”放过替换。
expected_xingji_refs = [
"docs/agent-specs/2026-07-06-北极星顶级线-肉鸽割草-开发设计书.md",
"docs/agent-specs/2026-07-06-山海宇宙设定与美术音频管线-选型材料.md",
]
xingji_refs = by_id["gac-shanhai-xingji"]["designRef"] or []
ok = (xingji_refs == expected_xingji_refs
and all((HERE.parent.parent / ref).is_file() for ref in xingji_refs))
check("⑤ 《山海行纪》designRef 指向 2026-07-23 追认的两份 designIntent 且文件真实存在", ok,
f"designRef={xingji_refs}")
xingji = by_id["gac-shanhai-xingji"]
expected_hash = "17b9073c767faf7990e0bf4563a86d55ffa81121f11e7c8ad37c8b8ce72e8cbd"
# 当前 bundle 的浏览器证据必须进入 active 注册记录,避免只引用旧轮次截图。
expected_browser_evidence = {
"game-runtime/games/shanhai-xingji/evidence/round-17-gold-lock/qa-report.md",
"game-runtime/games/shanhai-xingji/evidence/round-17-gold-lock/browser-evidence.json",
"game-runtime/games/shanhai-xingji/evidence/round-18-host-refresh/audio-listening-evidence.md",
"game-runtime/games/shanhai-xingji/evidence/round-18-host-refresh/qa-report.md",
"game-runtime/games/shanhai-xingji/evidence/round-18-host-refresh/browser-evidence.json",
}
bundle = HERE.parent.parent / xingji["assetRef"] / "dist/shanhai-bundle.js"
actual_hash = hashlib.sha256(bundle.read_bytes()).hexdigest() if bundle.is_file() else None
ok = (xingji["role"] == "game_content_gold"
and xingji["lifecycleStatus"] == "active"
and xingji["assetVersion"] == "map1-vertical-slice-r1.1"
and xingji["artifactHash"] == expected_hash
and actual_hash == expected_hash
and xingji["consumerRef"] == "generation-runtime@reference-assets/1"
and xingji["signedBy"] == "创始人"
and bool(xingji["signedAt"])
and expected_browser_evidence.issubset(set(xingji["evidenceRefs"]))
and all((HERE.parent.parent / ref).is_file() for ref in xingji["evidenceRefs"]))
check("⑤ 《山海行纪》地图1纵切版 active 记录身份、bundle 真 hash、消费方、证据与人工签认齐全", ok,
f"bundle={bundle} actualHash={actual_hash} record={xingji}")
active_gold = [r for r in records
if r["role"] == "game_content_gold" and r["lifecycleStatus"] == "active"]
check("⑤ 正式签认 game_content_gold 当前恰 1 款《山海行纪》SoT §7 表)",
[r["recordId"] for r in active_gold] == ["gac-shanhai-xingji"],
f"active={[r['recordId'] for r in active_gold]}")
# ── ⑥ 占位 hash 红线:格式合法 + 逐条注释标注,不伪装 active ────────────
hash_pattern = re.compile(r"^[0-9a-f]{64}$")
notes = registry.get("migrationNotes") or {}
ok = all(hash_pattern.match(r["artifactHash"]) for r in records)
check("⑥ 全部 artifactHash 符合 64 位小写十六进制格式", ok)
pending_records = [r for r in records if r["lifecycleStatus"] != "active"]
missing_note = [r["recordId"] for r in pending_records
if "占位" not in notes.get(r["recordId"], "")]
check("⑥ 每条未激活记录的 artifactHash 占位均在 migrationNotes 显式标注", not missing_note,
f"缺标注={missing_note}")
ok = all(r["lifecycleStatus"] in ("candidate", "migration_pending") for r in pending_records)
check("⑥ 除已签认《山海行纪》外,迁移项仍无伪装 active", ok)
# ── 汇总 ─────────────────────────────────────────────────────────────
print(f"\n══ 汇总:{len(_PASS)} 过 / {len(_FAIL)} 败 ══")
if _FAIL:
for name, detail in _FAIL:
print(f"{name}")
return 1
print("全部通过:新 schema 自洽 + 迁移清单符合 schema + v3 向后兼容 + 旧 v2 未破坏。")
return 0
if __name__ == "__main__":
sys.exit(main())