58 lines
8.2 KiB
Markdown
Raw 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.

# 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 校验器与消费侧同栈。正负样本套件即校验器自身的回归证明:某关键字实现错了,正样本会被误拦或负样本会漏过,跑套件即暴露。
```bash
# 套件模式:正样本应全过、负样本应全拦,打印每契约小计与总计
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-1070` verdict_feedback/_economy_evidence + `gate_judge.py:42-53` 前缀口径 + `tier2-verdict.schema.json` richGameGates/findings 形态(tier2 富证据示例值)