prompt-governance §11 稳定门在 M3 下 flaky 与治法(anyTerms 同义词补全治本+格式硬约束+几何退避;actor cohort 聚合比 judge 宽松) gen-path-parity §6.2 跨语言契约版本接缝(Python 切 acceptance/3 必同步升 Node runner /3 否则真浏览器整拒) staging-ops §2 commit 在途 M 触发 pre-commit 版本漂移闸处置(机械对齐 registry 匹配 frontmatter)+§4 长任务前台串行+孤儿独立复核 ai-development-protocol §4.2 关键 hash 一律主代理独立重算(扩 commit-hash 自验到产物/基线 hash) 均补到既有文件无新建,带本会话实证行号
25 KiB
name, description, node_type
| name | description | node_type |
|---|---|---|
| gen-path-parity-harness | 两条生成路对照验证(parity)harness 范式 + 按品类 ≥80% 达标门(bake-off)——验证「换实现后生成质量没回退」(Node→Python 重写 / 换模型 / 换框架)并判「生成质量够不够格上线」。核心=对照公平铁律(驱动器 held constant)+ 金标 spec 注入 + 两层判据 + 端口池并发 + 自动 spec 期望外生防自证 + 达标门质量口径(编排未收敛剔出分母、单列归编排硬化)。WU-A 后续(cheap-worker/compare_node.py)、M1 达标门(003,bake_off.py)实证。 | skill |
生成路对照验证 harness 范式
何时用
要回答「把生成实现换了一套,质量有没有回退」——Node 重写成 Python、换个模型、换个框架。区别于「每条路各自能不能跑」:对照验证比的是两条路产出的游戏质量是否等价,给「旧路可以退役」这种迁移决策当硬证据。WU-A 框架收敛的 R1 头号风险(把在产主力整条重写)就靠它兜。
对照公平铁律:驱动器必须 held constant
游戏质量用九门过门率衡量,而九门要靠一份 play-spec 驱动游戏去玩。两条路若各用各自自动产的 play-spec,过门率差就掺进了驱动器质量差、不可归因于生成。 所以对照时把每个品类的 play-spec 钉成一份金标、喂两路共用,把驱动器从对照变量里摘掉——这样过门率差才只反映生成质量。
只精化单侧的 play-spec 是最隐蔽的陷阱:它看着是「让对照更准」,实则把驱动器质量偏向了被精化的那一路,制造新的不公平。
金标 spec 只对一部分驱动器家族公平
能不能「一份金标 spec 通吃两路同品类游戏」,取决于驱动器怎么找操作目标:
- tap-targets 族(点击 occupied 目标:点击得分 / 打地鼠 / 经营点客):驱动器从游戏自报的
_forensicsView().state().targets里读坐标再点,坐标与具体游戏实现无关 → 一份金标 spec 公平驱动两路,成立。 - key-cycle 族(方向键:2048 / 躲避):驱动器按 spec 里硬编码的
driver.keys逐键轮发,而按哪个键是生成侧 LLM 写 game-logic 时的自由选择、不在 forensics 契约里。两路独立生成完全可能绑不同键,一份固定按键的金标对绑定一致那路驱得动、对另一路驱不动 → 公平不成立。这类要先立一道「两路绑同一套输入键」的契约(生成侧约束),或让 spec 从每路自报的输入映射派生键集,才能纳入对照。
一句话分界:forensics 契约保证的是可观测性(读得到 targets/score),不是可驱动性(同一份 driver 配置能把两路都开起来)。 别把两者当一回事——这是对照覆盖面的真实边界,硬来会把按键绑定的分叉冤判成生成质量差。
注入机制:把生成与 play 拆开,金标覆写 staged 路径
- 拆步:每路把「生成」和「九门 play」拆成两步。若某条生成路内部已经把 play 一气做完(如
run_studio收口含 play),给它加一个 generation-only 开关(run_gates=False)只生成到 stage、不 play。 - 注入点 = staged 路径:金标 spec 强制覆写到
games/_wg1-gen/<id>/play-spec.json——这是play.cdp.cjs真实读取的位置,不是 evidence 目录(最常踩的错)。 - 覆写时机:在 stage 之后、play 之前写入。gen-only 路靠「自动产 spec 的已存在不覆盖」语义即可让金标留住;已经自动产了 spec 的路(如 Node
gen.mjsdone 门)要强制覆写。 - 再各自单独跑九门 play。
判据:两层 + 逐品类绝对地板 + 双低标红
当基线本身 flaky(如旧路五款只 3/5 真可玩),只用「新路不低于旧路」会把「两路都没真玩起来」或「两路都勉强平庸」判成等价坐实。要四道一起兜:
- 相对层:新路过门率不低于旧路。
- 绝对层:旧路本身过不了可玩门的品类,要新路真过该品类可玩门(driven 且进展门 H 过)才算坐实,不能靠「一起烂」蒙混。
- 逐品类绝对地板:坐实的品类,新路真玩比例要带余量过线(如 ≥0.6),不只是 ≥旧路。
- 双低标红:两路同卡菜单 / 同低分的品类显式标红、不计入坐实。
判据在品类层聚合多局(单局生成 flake 不拖垮整品类,且单局两路同等的 flake 是公平的、不算回退)。
成功定义钉死「金标 spec 下」
对照证据是在手写金标驱动器下采的;生产里新路将用自动产的 play-spec 驱动。两个驱动器不是同一个,而金标通常比自动 spec 更干净。所以对照等价 ≠ 生产退役/切换许可:面向生产的退役授权要另 gate 在「生产自动 spec 达到金标同等过门率」(auto-vs-golden delta)。产物命名直接标「金标 spec 下」,免得被当成生产许可。
自证陷阱:自动 spec 的期望基准必须外生
生产里新路要脱离金标、自动产 play-spec 驱动九门(便宜档 M1 U2)。把自动 spec 加厚到金标同质时,有个比对照更隐蔽的陷阱:某些门的期望字段是语义期望而非可观测状态——典型是 F_wiring 的 expectedEngineCallPrefixes(这个品类该调哪些插件)。它从游戏自报的 forensics state 里推不出来(state 只有 targets/score,没有「该调什么」)。若图省事从该局实际调用反推期望,期望就恒等于实际、F_wiring 退化成自证、必过、假绿——这是 held constant 铁律在单路自动 spec 上的化身:期望基准必须外生(预置「品类→期望前缀」映射表作 held 期望),绝不从被测对象当场反推。可测性:构造一个实际多调一个插件的局,合格自动 spec 的期望前缀不跟着变。
实证:M1(git show 8ea97234:docs/plans/2026-06-26-003-feat-cheap-worker-M1-达标门绿-plan.md)U2,Opus 评审核出自动 spec 与金标唯一实质差异就是这个字段,且它是达标门防假绿的第二层(第一层=按品类判防平均掉卡死品类)。
达标门口径:质量地板 vs 生产真实交付率(M1 U3 实证)
对照验证比的是「两路等价」;达标门(bake-off)换一个问题——「这条路生成出来的游戏,按品类稳不稳定够格上线」,按品类聚合九门过门率、逐品类 ≥80% 判。建这道门时,算过门率的分母怎么定是个会直接翻转结论的口径决策。(2026-07-09 勘误:「按品类九门过门率 ≥80% 判达标」自此只度量机械预筛地板——九门 pass 降为「未见明显死」的预筛,够不够格上线还须过独立模型玩法判定,达标口径随之收窄;下文分母口径〔收敛款为分母、rawPassRate 并报〕本身不受影响。见质量 SoT《游戏质量与爆火能力》裁定三。)
一次生成会以两种性质完全不同的方式「不过」:
- 九门质量挂:游戏产出来了、跑进对局,但某道确定性门没过(如 H_progress 没真进展)。这是真·游戏不可玩。
- 编排未收敛:agent 编排根本没把游戏做出来——M3 撞单步静默超时(
timeout)或工具调用步数硬顶(step_cap),finished=false、staged=false、没有九门 verdict。这不是「游戏不好」,是「这次生成没收敛」。
把两者混进同一个分母,达标率就掺了模型的编排稳定性抖动,不再纯反映生成质量。M1 实测把这点钉死:click-score 合并 14 款,原始口径(未收敛计入失败)= 11/14 = 0.786 卡在 0.8 边界下;剔掉 2 个编排未收敛后质量口径 = 11/12 = 0.917 稳过——同一批样本,口径之差直接决定里程碑收不收口。两批之间的方差(0.714 vs 0.857)也几乎全来自未收敛款的计数,不是质量波动。
口径铁律(创始人 2026-06-27 定):达标门只衡量九门质量地板,分母 = 收敛款(finished=true、有 verdict);编排未收敛剔出分母、单列一个 unconvergedTotal 指标,归生成稳定性(编排硬化)那条线,不混进质量判定。但原始口径(含未收敛的 rawPassRate)必须并报——它贴近生产里用户的真实交付率(用户视角:没拿到能玩的游戏就是失败),是另一层指标,只是不作质量达标判据。converged==0(全未收敛)→ insufficient,质量无从判、不得默判达标。
两个边界别越:① 剔未收敛不是给质量放水——它剔的是「没产出游戏」的样本,不是「产出了但不好」的样本;后者(质量挂)照常计入失败。② 未收敛率本身是真问题(M1 实测 ~7%),只是它的家在编排稳定性、不在达标门;达标门的职责是把它显式量出来单列,不是吸收进质量数字里假装没有。实现见 bake_off.py 的 judge_genre(分母 converged + rawPassRate 并报)/ aggregate(unconvergedTotal 汇总)。
配套的两个生成质量前置(M1 同里程碑、归生成侧不归达标门):U1 护栏——共享 oracle
tools.mjs的check()在打包前堵「返 void 的绘制 API 被误当返回值用」这类能静态判的幻觉(过 check 却运行期卡菜单的子集),救回卡死品类进样本池;U4 预算闸——便宜档复用 tier2CircuitBreakerMiddleware,发起每次模型调用前预估累计 ¥、越 ¥10 硬上限即 fail-closed,成本按 new-api quota 口径落盘(取价不可达降级到次数/token 闸 + 告警,不静默超支也不静默阻断)。
auto-vs-golden 门:同款双驱动 delta(M2 实证)
对照门验「换实现没回退」、达标门验「绝对够格」,还差一道验「生产里那把自动产的驱动器,跟人手写的金标比有没有退化」。生产不会用金标 spec,每款游戏的驱动器是 ensure_play_spec 当场据 smoke state 推断产的、通常比金标薄。所以「金标 spec 下达标 / 等价」不等于「自动 spec 下生产真达标」,后者才是 Node 退役授权的条件。
做法:同一款产物 play 两次、只换驱动器。 生成一次(gen-only)→ 同一份 staged src/ 上先自动 spec(ensure_play_spec)play、后金标(inject_golden)play,逐门比过门率。同款产物两次 play 把生成方差摘掉,delta 纯反映驱动器差。编排序的坑:自动 play 必须在金标注入之前捕获(否则 auto_gates 被金标覆盖);ensure_play_spec 是「已存在不覆盖」语义,跑前要断言 staged 无残留 spec(撞号 / 重跑会静默复用旧 spec、污染 auto_gates)。
方向是单边的:门查「自动有没有比金标驱得差」(delta < 0 退化),不是查「自动有没有比金标松」。这点容易写反——动机一不小心就写成「防自动放过坏局」(更松),但断言同质时自动不可能比金标松(M1 已把自动 spec 断言加厚到与金标逐字段一致),真正会发生的是自动驱不动(薄 spec 把金标能驱起来的可玩局判进不去)。关键门 {E_live,H_progress,G_input} 容差 0(delta≥0)、其余六门容差 -1/N(单款 flake)。double-low 守卫:关键门两路都 <0.5(同挂)→ inconclusive,不被 delta=0 误判等价(同 compare_node.doubleLowRed)。
鉴别力别预判为零。评审一度判这门在 tap-targets 上「自动与金标字段同质、delta≈0 恒过、没鉴别力」。M2 实测证伪:click-score 5 款里 1 款真退化(E_live/G_input auto 0.8 < golden 1.0),3 次复跑确定性复现、非 flake。根因是一类生成契约漂移——M3 合法地用单数 target(对象)暴露当前目标(屏上一次只一个),而整条 tap-targets harness(_build_play_spec 的驱动器推断 + 驱动器 targetsPath:"targets")只认复数 targets(数组):单数款被推成 key-cycle(发方向键)、连「开始」都没点到 → 进不了 live。即门确实能逮住「自动驱动器对某类合法 forensics 形态失效」,delta≈0 只在自动与金标对该款都驱得动时才成立。教训:自动 spec 的驱动器推断键在某个具体字段名 / 形态上时,生成侧任何等价的别样表达都会让它静默误分类——这类漂移要么在生成侧立硬契约(强制复数 targets)、要么让推断与驱动器认全部等价形态。
这门是「不退化确认」、不是决定性独立闸。退役授权的强证据仍是绝对达标(M1)+ 两路等价(002);auto-vs-golden 补「生产自动 spec 没把达标驱虚」这一层。三条齐(M1 达标 ∧ 002 等价 ∧ auto-vs-golden 不退化)才授权切默认路由——任一不绿,授权如实判 NOT authorized 并列明欠哪条(retire_authorization 只读三份既有报告聚合、零真跑零 LLM)。M2 实证:auto-vs-golden 这条因上述契约漂移先判未绿、退役授权正确卡住,精确根因交回生成侧(WU-C 5.4)当场修复(ffe3a5c9:键存在性推断 + 驱动器单数 target 兜底)后转绿、authorized=True——门诚实卡住、修后如实放行,全程没放水。
并发(可选,提速)
并发的真约束不是产物隔离(gameId 前缀已隔离),是九门 play/smoke 的固定端口 + Chrome 实例。
- 端口池:每并发槽一对独立
(server_port, cdp_port),server 段与 cdp 段不重叠。 - 每对一线程:用线程跑整对(线程内生成路自带 event loop),信号量或端口池 size 天然限并发。
- 前台进程内有界并发,不是后台子代理 task / tail 监视 / 自我唤醒重试——后者会失控(撞同名产物、杀进程触发重试死循环、token 泄漏)。
- 前提:引擎工具端口可参数化(
gen.mjs --port/--cdp、play.mjs {port,cdpPort}各自独立 spawn server+Chrome+userDataDir)。
便宜档生成的资源/并发上限画像另见相应容量记录(M3 推理远程、本地瓶颈是 Chrome/esbuild 的 CPU 突发、并发上限 ≤15)。
实现指针
cheap-worker/compare_node.py:judge_genre(两层判据)/inject_golden+_write_spec_atomic(金标覆写 staged)/run_pair_sync(生成→注入→play 整对)/run_multi(端口池 + 线程 + 限并发)/_port_pool。cheap-worker/bake_off.py(达标门):judge_genre(质量口径 = 分母 converged + rawPassRate 并报 + unconverged 单列)/aggregate(按品类逐项 ≥0.8 + unconvergedTotal 汇总 + 缺收敛样本报 insufficient)/run_one_sync(透传 breakerKind 供未收敛诊断)/run_bakeoff(端口池有界并发 + 报告不覆盖)。cheap-worker/auto_vs_golden.py(auto-vs-golden 门 + 退役授权):judge_genre_delta(逐门 delta + 关键门容差 0 / 其余 1/N + double-low 守卫)/aggregate_delta(按品类聚合)/run_pair_dual_sync(同款双驱动 play、auto 先于 golden、ensure 前断言无残留 spec)/run_batch(端口池有界并发 + 报告不覆盖)/retire_authorization(读 M1+002+auto-vs-golden 三报告聚合、零真跑零 LLM)。cheap-worker/fixtures/golden-specs/:每品类一份金标 play-spec。- 九门 harness 本体(驱动器家族 / play.cdp.cjs / 出厂红线)见 game-e2e-cdp-harness;AgentScope 2.0.2 便宜档实现 API 见 agentscope-2.0-facts。
实证
WU-A 后续(2026-06-26,git show 8ea97234:docs/plans/2026-06-26-002-feat-cheap-worker-multi-genre-plan.md):三 tap-targets 品类 n=3 对照 Python 新路 vs Node 旧路全 equivalent,坐实范式未回退。Codex + Opus 双评审揪出的四个真坑都在本范式里固化成了铁律:注入路径错(evidence 而非 staged)、品类族判错(点击得分是 occupied 反应族不是放置族)、判据缺绝对地板(双低被判等价)、只精化单侧 spec 污染对照。
M1 达标门(2026-06-27,git show 8ea97234:docs/plans/2026-06-26-003-feat-cheap-worker-M1-达标门绿-plan.md U3 收口):三 tap-targets 品类各 7 款 M3 实测 + click-score 补跑 7 款确认抖动,质量口径逐品类 ≥0.8 全过——click-score 11/12=0.917、whack-mole 7/7=1.0、shop-serve 6/7=0.857,M1 达标门绿。关键发现 = 上「达标门口径」节的口径铁律:同一批 14 款 click-score,原始 0.786(边界下)与质量 0.917(稳过)之差全在 2 个编排未收敛款(timeout/step_cap),口径决定收口。收口报告 cheap-worker/results/bake-off-M1-final.json(results gitignored、本地证据)。
M2 auto-vs-golden 门(2026-06-27,git show 8ea97234:docs/plans/2026-06-27-001-feat-cheap-worker-M2-node退役授权-plan.md,过双评审):U1 逐门 delta 判据 + U2 同款双驱动 play + U3 三条齐退役授权判定全交付,全套件 106 测绿。full 批 n=5 首测:whack-mole/shop-serve aligned、click-score regressed(E_live/G_input auto 0.8<golden 1.0,3 次复跑确定性复现)。根因 = 单数 target(菜单态 null)vs 复数 targets 契约漂移(见上「auto-vs-golden 门」节)。门当场抓到此生产自动 spec 缺陷 → 当场修(WU-C 5.4,ffe3a5c9:_build_play_spec 改键存在性判定 + 驱动器单数 target 兜底)→ 退化款 auto 0.667→1.0、重放三品类全 aligned → 退役授权三条齐 authorized=True(M1✓+002✓+auto-vs-golden✓)。全程没放水:门诚实卡住、修后如实转绿,证伪「字段同质恒过无鉴别力」的预判。报告 cheap-worker/results/{auto-vs-golden-2/3.json,retire-authorization-M2.json}。
M3a U1(2026-06-27,git show 8ea97234:docs/plans/2026-06-27-002-feat-cheap-worker-M3a-python-route-seam-plan.md,过双评审+复审,创始人 D3=全局 flag 翻转、不做 per-request 灰度):cheap-worker/{worker_service,result_out}.py 交付——§6.1 HTTP worker(202 握手 + 有界串行队列 + 三字段 result-out + HMAC 回调,见下契约节)。27 单测 + 全套 132 测零回归;真生成 e2e 过九门(amgen-m3ae2e1 verdict pass=True、235s)。真 e2e 抓到并修真 bug:回调走系统代理被 fake-ip 拦 502 → 禁代理 opener(502→200)——印证内网外呼必须跑真网络验、单测注入桩会绕过此坑。双评审复审揪出的事实错(幂等字段我误名 n、真名 idempotencyKey 且后端 AigcApiImpl:79 已透传未消费)经对代码核验纠正。
§6.1 worker result-out 契约(M3a · 退役机制接后端)
便宜档退役走「Python HTTP worker(cheap-worker/worker_service.py)经后端默认通道 WorkerDispatchClient 派发」时,worker 产的 result-out 三字段语义严格分离——M3a 双评审两家都点中、初版设计也搞错的 BLOCKER,固化成铁律:
gameConfig= 最小占位 Map{templateId, title, theme:"generic", engineDriven:true}——绝不把源工程塞这里(GamePackage required 故不可空,但 engine 路逻辑不在它里)。engineBundle=amgen-<id>/bundle.iife.js全文(含__GameBundle)= 运行时承重产物——后端resolveEngineBundleText从它落GamePackage.engineBundle入 feed。成功门 =status=succeeded ∧ engineBundle 含 __GameBundle,否则failed+ 七值failureReason。sourceProject= 源工程 2.0 JSON = best-effort 可选(additive,DifyCallbackReqVO.sourceProject,缺则后端源落库旁路、不阻断成功)——profile 三枚举派生不出就整段省略、绝不伪造凑 schema。- 权威范例镜像 =
game-cloud .../saa/SaaGraphDispatcher.buildCallbackReqVO(L601-635)+ Pythonwg1/gen-worker/worker/service.py:build_callback_payload(后者不产 sourceProject)。
串行 worker(Chrome serve/CDP 端口不可并发)治 409 误判:job-in 一律入有界队列(有容量 202、队满 503)、单线程串行消费,执行器 2xx=ok 契约不动(队满 503 仍非 2xx→LLM_ERROR 终态,罕见背压可接受)。回调 HMAC = hmac.sha256(secret, 发送字节)→X-Callback-Signature,签名字节 == 发送字节。回调必须禁系统代理(内网端点经 fake-ip 代理被拦 502)。
§6.1 worker 9d trace + D9 dedup parity(M3b · 换 worker 实现别静默退化 D11/D9)
换便宜档 worker 实现(Node/SAA→cheap-worker Python)时,result-out 不止三字段——9d trace 与 D9 dedup 是 worker 侧产出,换实现就得在新路重新兑现,否则后端 D11/D9 静默失效。M3b 固化:
- 9d trace 必填七项
{pass, repairs, wallS, models, attempts, gameId, stage}(camelCase)+ D11 首局维所需gatespec.driver(键名必须driver、非driverType)与sevenGateVerdict.guards.H_progress.pass(嵌套对象、非裸 bool)+cost.totalRmb+similarity(D9)。镜像 wg1_extract_trace/ SAAextractTraceQuietly输出口径。repairs = max(0, attempts-1)(首轮 0;attempts 是 resume 计数,off-by-one 会让好游戏 stability 永不满分、破 parity)。 - D11 退化判别(retire-detection):worker 只产
trace.cost(M3a 初版)→ 后端ReadinessScorer的 playability/firstPlay/stability 三维全取中性 0.5、只 efficiency 随成本 → readiness 退化成近恒定双峰 45/55。且BUDGET_RMB=0.15与便宜档真实成本 ¥0.4–1.2 错配 → efficiency 维对便宜档恒 0、零区分力。别拿「合成分脱离某区间」作判据(退化态 45 本就在区间外 → 假绿);改逐维断言(三维各自脱离中性 0.5)。 - 反假绿真护栏 = 跨语言切片测反射调真 ReadinessScorer 私有逐维 scorer(
CheapWorkerResultOutContractTest):真 result-out fixture 喂真算分器,逐维断言 + 成功 vs 退化分拉开。三值判别 = 失败 28 / 退化 45 / 成功 100(M3b 真后端实证)。trace是后端开放Map、Jackson 不拒内层未知键 → 键名拼错(seven_gate_verdict/driverType)被静默接收、悄悄落中性;真护栏只有这个切片测。 - 复用旧路代码的 import 边界坑:cheap-worker 的
_bootstrap入 sys.path 的是tier2/gen-worker(其无 dedup/_extract_trace);trace/dedup 参考在wg1/gen-worker(另一棵树、不在 import 路径)。故 trace 在 result_out 内手写镜像口径(输入形态不同、不能直接套_extract_trace);D9 vendor 复制dedup.py进 cheap-worker(纯模块,DEDUP_REGISTRY设自己results/,比跨树 import + monkey-patch 全局常量干净)。 - 两条回调路都落 trace(
DifyCallbackTxService失败路也调persistTraceQuietly)→ trace 落库可经失败 gen 验证;但 D11 success 路高分需 succeeded(注入合法 bundle 验机制、不赌便宜档质量)。aigc.trace.enabled默认 false → 验前必开。 - 玩家试玩边界:玩家 manifest 端点要求 runtime_package published(status=1),preview(0)返「运行包未发布」→
publish→feed→玩家真玩必经reviewProject(APPROVE)(产品/审核台轨、auth + 项目 REVIEWING 生命周期),非生成线;生成线交付 = 落到可发布的预览包(version + status=0 包 + engineBundle 进 package_json + D11 分)。
§6.2 跨语言契约版本接缝:一侧升版本,另一侧校验器必同步升(W-GOLD-LIVE 实证)
便宜档 Python cheap_verify 与 Node runner game-runtime/games/_wg1-gen/_shared/playtest-v3.cdp.cjs 共用一套 acceptance-request/N 来源契约。一侧升了契约版本、另一侧校验器没同步升,真浏览器验收会被整拒,而接线看着是完成的——这与上节「换 worker 实现别静默退化」同型,只不过这里是「换契约版本别静默整拒」。W-GOLD-LIVE 把 Python 切到 acceptance-request/3(v2 字段集之上开口三个可选参照资产字段 designRef/referenceAssetRecordIds/consumerRef,cheap_verify.py:1726/1774,加 consumedReferenceAssets 来源,:1787-1851)后,Node runner 当时只在版本三元里认 /1 /2、/3 落 null → 整条抛 profile_contract_error;且 assertExactArtifactKeys 是严格字段集、不含 v3 三可选字段,真 v3 acceptance 全被整拒。
固化成范式(现已是 runner 现行形态):
- 版本三元必带全分支:
validateProfileProvenance(playtest-v3.cdp.cjs:323)按schemaVersion派生contractVersion,三元逐版本列全/3 ? 3 : /2 ? 2 : /1 ? 1 : null(:333-335);新版本不补分支就落null整拒。 - 严格字段集用 optionalKeys 开口,旧调用方零变:
assertExactArtifactKeys(value, expected, label, optionalKeys=[])(:311)缺省[] = 旧严格行为;v3 把三可选字段与consumedReferenceAssets经optionalKeys白名单放行(:343/352),/1 /2调用方不传该参、行为不变。 - 绊线测试随新字段扩:恢复「篡改 + 重 hash」两段断言(篡改字节 → hash 不一致必拒)并扩到 v3 新字段,防校验器对新增字段静默放行(验法见
cheap-worker/tests/test_acceptance_v3.py)。
消费对账是机器强制,不是 bug:runner 与 full_gate.py 只消费 lifecycleStatus==active 的参照资产;声明消费而无 active 匹配 = verified reject(full_gate.py:187-204,六项闸:存在/激活/role/consumerRef/版本/缺维度)。迁移窗口里清单全是 migration_pending/candidate 时,任何 live 消费当场被拒——这是「迁移完成前不得新增 live 消费」的机器强制。