contracts/play-loop/ —— 判分闭环两契约(PlaySpec 考卷 · VerdictFeedback 判卷反馈)
这一目录登记生成引擎判分闭环上的两份契约,它们是 2026-07-03 一次性裁决(Δ1)从散在代码里的隐形约定钉出来的显式契约。闭环是这样一条线:一局游戏生成出来后,harness 按一份考卷(PlaySpec)真玩它、九门判分;没过门就把一份结构化判卷反馈(VerdictFeedback)喂回续修 agent 让它接着修;修完再玩再判,直到门绿才放行。两份契约分别钉住这条线的两端——考卷说清「怎么玩我、以便据此判我」,反馈说清「哪道门没过、卡在哪、往哪修」。
| 契约文件 | 设计代号 | 是什么 | 消费方 | 状态 |
|---|---|---|---|---|
play-spec.schema.json |
C5 | PlaySpec 考卷:游戏声明起局仪式、驱动器族、输入指令、赢/输可观测量、与源工程的派生绑定 | W-S1 修复三单(接 startRitual / derivedFrom 进 harness 与 ensure_play_spec)· tier2 F-1 对齐 | schema 已立、校验器与负样本齐;生产接线在途 |
verdict-feedback.schema.json |
C6 | VerdictFeedback 判卷反馈:每次未过门回喂续修 agent 的结构化载荷(哪道门 / 卡在哪个 phase / 判卷驱动器 / 证据指针 / 疑似失败面 / 修复方向) | W-S1 修复三单(gate_judge 产结构化反馈、middleware 注入续修)· tier2 F-1 对齐 | schema 已立、校验器与负样本齐;生产接线在途 |
为什么现在钉这两份
考卷(C5)至今是隐形的。驱动器怎么选,靠的是 cheap_run.py:241-242 那句「state 里有没有 targets 键」——有就用点目标的 tap-targets 族、没有就用循环按键的 key-cycle 族。这条约定只活在一个函数里,读代码才知道。更要命的是考卷和源工程之间没有任何绑定:cheap_run.py:278 用「play-spec.json 已存在就不覆盖」来兜底,可源工程一旦改过,旧考卷还在,就会拿上一版的考卷去判新工程——要么假绿、要么错判。C5 把驱动器族、起局仪式、赢/输可观测量都升为显式字段,并加一段 derivedFrom.sourceHash 把考卷绑到源工程内容上:hash 不一致即算陈旧、必须重生。陈旧从此可判定,而不是靠「不覆盖」听天由命。
**反馈(C6)**至今是一整段拼出来的中文字符串(gate_judge.py:109 的 _cheap_verdict_feedback、run.py:1070 的 verdict_feedback)。字符串拼装里丢字段是无声的,最典型的一处就是 gate_judge.py:100-102:它拼 H 门的 latch 细节时只读了 latch.after,没读 latch.phaseNow。可 latch 有两条失败支——驻留校验支带 after(play.cdp.cjs:1089),而 advisory 失败支只带 phaseNow、不带 after(play.cdp.cjs:1083)。于是当游戏卡在 playing 玩不到终局、走 advisory 支失败时,回喂里就成了 after=None,续修 agent 根本不知道游戏卡在哪个阶段,只能盲修。C6 把「卡在哪个阶段」升为一等字段 phaseNow,并规定「只要报 latch 失败就必须带 phaseNow」——这类丢字段从结构上不再可能发生。
不编码 tier,用能力字段与扩展段表达档位差异(Δ9③)
两份 schema 都要同时承载便宜档与 tier2 富游戏档,但任何字段里都不会出现 tier: cheap | tier2 这样的枚举。做法沿用 contracts/trace/ 已立的「接口对称、内容不对称」纪律:核心字段两档同名同义(考卷的 driver/observables/derivedFrom,反馈的 gate/driverType/failedGates),档位差异一律靠两种方式表达——
- 能力字段的「在不在」:考卷里出现
economy段=这局有经济胜负语义,出现controlCheck=这局有可跟手的控制体,出现firstPlay=要判首玩核心闭环时限。它们表达的是「这局具备什么能力」,而不是「这局属于哪档」。便宜档轻游戏不写这些段,tier2 富游戏档写,同一份 schema 都校验得过。 ext扩展段:某一档独有、不进对称核心的东西塞ext,各写各的、没有的字段绝不编造。tier2 的经济三个数、findings 严重度就落在反馈的ext里;便宜档一般不写ext。
样本 play-spec/valid/03-tier2-paddle-economy.json 与 verdict-feedback/valid/03-tier2-economy-ext.json 就是用来证明这一点的:tier2 的富能力字段和 ext 富证据全部通过同一份 schema,全程没有 tier 枚举。
和另外两处容易混的东西划清界限
- C6 反馈 ≠
contracts/trace/的 trace 事件。 trace 记录是生成轨迹里一步的观测事件,写进台账供成本对账和排障回看,是旁路的、给人和监控看的。C6 反馈是判分没过时喂回 agent 让它接着修的载荷,是闭环里的一环、给模型读的。两者都带traceId(反馈的evidence.traceId正是用来 join 到 trace 台账),但用途和生命周期完全不同:trace 每步都产,反馈只在未过门时产。 - 这两份 ≠
contracts/agent-loop/里的 v1 verdict。contracts/agent-loop/verdict.schema.json是 Tier0/1 clicker 世代的终判记录(designId / playReport / decision 三值),是终判本身,且是上一代模板闭环的产物。C5/C6 属于当前引擎无关九门这一代,且 C6 是终判之后产的反馈、不是终判。别把两代、也别把「终判」和「终判后的反馈」混为一谈。
校验器与样本
校验器 validate.py 零依赖(只用 Python 标准库),自带一个 JSON-Schema Draft-2020-12 子集实现——本仓没装 jsonschema、也没 vendor ajv,而红线要求零新依赖;两份契约的消费方(cheap-worker、tier2)本就是 Python,落一份 stdlib 校验器与消费侧同栈。正负样本套件即校验器自身的回归证明:某关键字实现错了,正样本会被误拦或负样本会漏过,跑套件即暴露。
# 套件模式:正样本应全过、负样本应全拦,打印每契约小计与总计
python3 validate.py --suite
# 单点模式:拿任意样本对某 schema 校验(可多文件)
python3 validate.py play-spec.schema.json samples/play-spec/valid/01-t2048-key-cycle.json
样本目录 samples/<契约>/{valid,invalid}/,目录名对应同名 schema。每个负样本文件头部的 _note 写明它复现的是哪条真实失败模式、对应生产代码哪一行、预期被哪条规则拦下——负样本都是「真实会发生的错法」,不是假想错法:
- C5 负样本:①无 sourceHash 绑定(复现
cheap_run.py:278缺绑定的陈旧兜底)②无任何驱动方式(driver/inputs 皆缺)③驱动器族缺必填参数(key-cycle 无 keys)④进展断言用了checkAssertion不认的算子(会无声判否)。 - C6 负样本:①报 latch 失败却丢 phaseNow(复现
gate_judge.py:100-102)②逐门反馈缺 driverType ③声称未过门却给空 items(复现gate_judge.py:125-126的盲修缺口)④门全绿却仍产反馈(passed=true 矛盾态)。
取样来源
正样本的 gameplay/verdict 字段取自现网真实产物,derivedFrom 等 C5 新增绑定段按契约目标形态补齐(现网 play-spec.json 尚无此段,接线在 W-S1 在途),每个样本 _note 如实记录:
play-spec/valid/01←game-runtime/games/_wg1-gen/t2048/play-spec.json(key-cycle)play-spec/valid/02←cheap-worker/fixtures/golden-specs/shop-serve.play-spec.json(tap-targets)play-spec/valid/03←game-cloud/.../saa-golden/play-spec.json(paddle-intercept + controlCheck)+run.py:1017-1024的 economy 形态play-spec/valid/04←game-runtime/games/_wg1-gen/_goldenpath-ref/play-spec.json(inputs 固定序列)verdict-feedback/valid/01←game-runtime/games/_wg1-gen/t2048/evidence/verdict.json(H_progress latch.phaseNow='playing' 真实失败)verdict-feedback/valid/02←game-runtime/games/_wg1-gen/_goldenpath-ref/evidence/verdict.json(F_wiring callCount=0 真实失败)verdict-feedback/valid/03←run.py:1000-1070verdict_feedback/_economy_evidence +gate_judge.py:42-53前缀口径 +tier2-verdict.schema.jsonrichGameGates/findings 形态(tier2 富证据示例值)