判例库补两类被门证实的证据落位,门判据改动前的全量回归多两组锚:
- 卡 menu 族 amgen-fx4c-{1,3,4}(复现窗 2026-07-03)从 pending 升为 product_fixtures:
每份固化 src 源码(卡 menu 现场:menuStartBtn 仅 render 副作用赋值 + 非方向键起局)+
evidence 摘要;bundle/index.html 构建产物与约 1MB 生成 trace(verdict 全 null)不入仓,
附 tools.mjs build-saa 重建命令。裁决两防线:案 1/3 驱动器约定漂移(误杀)、案 4 并发
flaky(脆弱写法),细化归卡 menu 归因单。
- 剧情品类金标正例 p11a-s1「深渊回声」(丰富度 7/11、九门全绿)落
cheap-worker/fixtures/golden-samples/narrative/,给策划知识包 v2 正例复验当靶子;
narrative.json 的 _note 指针从「产物未入仓」翻为已固化。
manifest 新增 product_fixtures 键表达「有载体、真跑需九门 CDP 环境」的第三态(区别 cases
可运行 / pending 无载体);产物型判例不进 run.mjs 零依赖 stage。run.mjs 全量回归绿
(node 三段 29 过、python 段无 pytest 合法 SKIP)。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
门金标判例库(gate-fixtures)
验收门是本项目的护城河:能生成代码不等于能玩,一款游戏要过九门真玩取证、形状/check 静态门、latch 终态契约才算 pass。门越硬,越怕两类事故——误杀(把合法游戏判死)和漏放(把坏游戏放行)。过去每次被证实的误杀或漏放,修复都固化进了判据,但案例本身散落在各次提交叙述与 results 目录里,没有一个集中位,也就没有"改判据前必须先过这些案例"的机器约束。
这个目录就是那个集中位。每一个被证实的误杀/漏放永久变成门自己的一条判例;门判据任何改动,都必须先让本库全量回归转绿,再动手。
为什么落在 contracts/ 而不是 harness 树内
判据不在一处,而是散在四个子系统、两种语言:
| 判据 | 位置 | 语言 |
|---|---|---|
| 九门 CDP 真玩取证(含 H 门 latch) | game-runtime/games/_wg1-gen/_shared/play.cdp.cjs |
Node |
| 形状静态门 + API 静态门 | game-runtime/tools/amodel-gen/tools.mjs |
Node |
| 未定义标识符 + import 断链扫描 | game-runtime/tools/amodel-gen/check-undef.mjs |
Node |
| verdict→反馈映射 | tier2/gen-worker/worker/gate_judge.py |
Python |
| cheap 门 | cheap-worker/cheap_gates.py |
Python |
没有任何一棵判据子树"离所有判据都近"。而"判例集中位 + 一个运行入口跑全量"本身是跨子系统的事:塞进 _wg1-gen/_shared/ 会让它反手去够 tier2 与 cheap-worker,还把 harness 的模板/运行时职责和一份跨子系统登记表搅在一起。contracts/ 是本仓既定的跨端单一事实源(八类契约都在这里),与每一棵判据子树平级、等距;而"每个被证实的误杀/漏放永久变成门的 fixture、判据改动必过全量"这句话,本身就是一条关于门行为的契约。所以判例库放这里,和 API/DB/事件/SDK 那些契约并列。
这不违背"判例跟着判据走、谁近放谁":已经有家的判例原地不动——undef-scan 的坏样本仍在 amodel-gen/tests/fixtures/ 挨着 check-undef,W-GENRE 四品类金标 play-spec 仍在 cheap-worker/fixtures/ 挨着 cheap 门,形状门/check 门的单测仍在 amodel-gen/tests/。本库不搬它们、不重抄它们(单一事实源),只做两件事:把它们登记进一张清单、用一个入口统一驱动;以及给那些眼下无家可归的判例(latch 终态同义词误杀)安一个家。
目录里有什么
manifest.json—— 判例清单(机器可读):每条判例登记它约束哪道门、判例真身在哪(carrier)、判例形态、期望裁决、以及来源(哪次误杀/漏放、哪个提交语境)。判例真身随判据就近而居,清单里只有指针,不重抄。cases/latch-terminal-synonyms.test.mjs—— 本库唯一新增的判例真身,收编 latch 终态同义词误杀(见下)。它无处可归(play.cdp.cjs是 CLI、无导出、无既有测试位),故安家于此。products/—— 产物型判例的复现产物(见下「产物型判例」)。目前收编卡 menu 族amgen-fx4c-{1,3,4}:每份固化 src 源码(卡 menu 证据)+ evidence 摘要,构建产物与生成 trace 不入仓。run.mjs—— 全量回归统一入口,零新依赖(只用 Node 内置node:test/node:child_process)。
判例形态
一条判例 = 可复跑输入 + 期望裁决 + 来源注记。输入是喂给真判据的东西(游戏源片段、跨文件 import 图、取证 state 的 phase 值),期望裁决是"该拦/该放 + 哪道门",来源注记说清它源自哪次误杀或漏放。判例一律跑真判据:形状门/check 门的判据函数本就导出(_shapeGateErrors、scanUndefinedIdentifiers、scanImportChain),单测直接调它们;latch 谓词内联在 play.cdp.cjs 无法 import,判例改为从判据源码抽取该谓词的比较字面量集、重建等价谓词再断言——复现的是判据实际比较的相位名,既不 eval 源码、也不另写一份同义词表,更不碰判据本体。删掉判据里任一同义词,抽出的集合随之丢项,判例即红。
latch 终态同义词误杀(本库收编的标志性案例)
2026-06-25,五款真实基线游戏真玩到终局、score 已经上升,却被判"终态不可达"而误杀。根因是 latch 只认 phase === 'gameover',而这几款游戏的官方终态相位叫 over / win / lose。修复把终态判定扩成同义词集(play.cdp.cjs:1074-1075 的 isTerminal)。cases/latch-terminal-synonyms.test.mjs 就是这次误杀的永久回归:断言 gameover/over/win/lose 都判终态(删任一个即复现误杀),同时守住反向——play/menu/booting 等非终态不得被判终态(防把同义词集放宽到误收、放过"秒进终局"的空壳)。
运行
node contracts/gate-fixtures/run.mjs
逐 stage 打印判据、覆盖、过/挂/跳,末尾给总计与退出码(有真实失败→退出 1,否则 0)。能跑的必须绿,跑不动的诚实跳过、不算失败:本环境缺 esbuild 时 check-undef 的扫描零依赖降级、其单测整体 skip;缺 pytest 时 Python 门那段整段 skip。在装齐 esbuild + pytest 的完整 CI 里,四段全跑;在裸环境里 Node 核心两段仍跑。两种环境都可挂载。
单独跑某道门的判例也行,例如:
node --test game-runtime/tools/amodel-gen/tests/check.test.mjs # 形状门
node --test contracts/gate-fixtures/cases/latch-terminal-synonyms.test.mjs # latch 终态同义词
挂载方案(留给下一单,本单不改 CI 配置)
判据改动必过全量判例,要靠机器门兜住,和文档治理的 docs-gate.sh 同一套挂法:
- pre-commit:当本次提交触及任一判据文件(
play.cdp.cjs/tools.mjs/check-undef.mjs/gate_judge.py/cheap_gates.py)时,跑node contracts/gate-fixtures/run.mjs,非零即拦。判据没动的提交不必跑,省时间。 - Gitea Actions:CI 流水线加一步
node contracts/gate-fixtures/run.mjs,并预装 esbuild + pytest,让四段全量真跑(不再有环境性 skip)。 - wave-close:波次收口清单加一条——判据有改动的波次,必须留一份本回归的全绿输出留证。
三处挂点具体接线(改 .githooks/ / .gitea/workflows/ / wave-close 脚本)是独立动作,按红线留给挂载单执行。
产物型判例(products/)
有些误杀/漏放已有复现产物、也钉死了归因,但真跑要九门 CDP 浏览器环境(driver 起局 + latch 驻留取证),没法塞进 run.mjs 这趟零依赖回归。这类判例落 products/:固化判例核心证据(src 源码 + evidence 摘要),在 manifest.json 的 product_fixtures 登记期望裁决与重建命令,作文档基线;等 harness 托管化能离线回放,再升为端到端 stage。它们既不是 cases(可运行、被 stage 驱动),也不是 pending(只有叙述、无载体),是中间的第三态。
- 卡 menu 族
amgen-fx4c-{1,3,4}(已固化,复现窗 2026-07-03):三款游戏都真玩起不了局、卡在 menu。归因已钉两类——案 1/3(接水果、太空躲避)是驱动器约定漂移:起局靠点击非方向键的menuStartBtn,而runKeyCycle驱动器无起局逻辑就点不到、把合法游戏误判卡 menu(误杀防线);案 4(点击命中类)是并发 flaky:menuStartBtn仅在render()副作用里赋值,rAF 饥饿下恒 null(脆弱写法防线)。每份只固化src/(卡 menu 源码证据)与evidence/(成本/步数摘要);bundle、index.html 等构建产物与约 1MB 的生成 trace(verdict 全 null)不入仓,重建命令见manifest.json。裁决细化归卡 menu 归因单。
待补产物
有些误杀/漏放目前只有叙述、没有可复跑载体,如实记在 manifest.json 的 pending,不硬造:
- base5 五款完整 CDP 真玩复跑:完整端到端(含 latch 驻留)需浏览器 + 五款持久化取证快照,快照未入仓;当前以 latch 谓词行为回归覆盖其核心,端到端复跑待补。
- wg1 verdict 基线:
wg1/gen-worker/results/*.json约 28 份是产物级金标,但重现需跑完整 gen+play,无零依赖复跑载体,登记为参照基线待 harness 托管化。
加一条新判例
再发现一次误杀或漏放,先看它约束的判据近旁有没有测试位:有(如 amodel-gen 的门)就把判例加进那里的 *.test.mjs,再到 manifest.json 登记一条指针;判据无既有测试位、判例无家可归,就在 cases/ 新增判例真身。要么有可复跑载体、进 cases 或既有测试;要么只有叙述、进 pending——不硬造。加完让 run.mjs 全绿。