让 static-smoke 夹具满足收紧后的入口合同
Project CI / Repository checks (pull_request) Successful in 3m16s
Project CI / Backend tests (pull_request) Successful in 4m9s
Project CI / Frontend tests (pull_request) Successful in 4m10s
Project CI / Native shell tests (pull_request) Failing after 12m10s

9a7a79951 给 finish_agent_runtime_project_verification_locked 加了一道复核:记录
game.static_smoke 通过时,按当前磁盘内容重跑 validate_game_html_smoke。五个夹具
仍停在收紧前的形状——它们各自只为视觉门、模块可达性或 owner 产物判据构造入口页,
没有目标说明、主循环或输入监听,于是伪造的 smoke 通过一律被拒,用例从未绿过。

生产代码不动:draft_validation.rs 与 master 逐字一致,project_gates.rs 里那段复核
逻辑也和 master 相同。只把夹具补到合同要求的形状:

- 数值常量作用域、内联/外部模块图集三处改用仓库已有的 with_static_smoke_contract
  包一层,被探测的常量引用与模块结构原样保留;
- 终态投影用例的入口页原本只有一个空实现回调,空回调本身触发另一条判据,去掉后
  同样包上合同层,写入次数不变、revision 断言不受影响;
- owner 产物用例是真实新项目,入口是无画布占位页。它在断言过「占位页过不了真实
  smoke」之后才需要一份过期 smoke 凭证,故在 owner 产物之前先落一份合规入口,
  verified_revision 仍取最后一次 owner 产物;
- 自主 manifest 用例只写了 package.json,直接用普通文件写入落一份合规入口,不触碰
  revision 记账。

顺带修一处仿造 gate:清 static_smoke_verified_revision 时必须同时清入口摘要,
两者是一对,否则 gate 自身的一致性校验会先于被测行为拒收。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 13:00:09 +00:00
parent e54ebb8227
commit c1dba09049
2 changed files with 30 additions and 4 deletions
@@ -7044,7 +7044,9 @@ fn canvas_visual_gate_resolves_numeric_constants_by_symbol_scope() {
&queue_autonomous_manifest_child_fixture(&root, &parent_state, "code-prototype"),
);
let html = "<!doctype html><html><body><canvas id='game' width='360' height='600'></canvas><script>(()=>{const COLS=10;const ROWS=20;const CELL=28;const BOARD_X=40;const BOARD_Y=20;const canvas=document.getElementById('game');const context=canvas.getContext('2d');const playerArt=new Image();playerArt.src='../assets/art-spritesheet-slices/player.png';const targetArt=new Image();targetArt.src='../assets/art-spritesheet-slices/blocks-and-targets.png';const sceneArt=new Image();sceneArt.src='../assets/art-spritesheet-slices/obstacles-and-scene.png';const feedbackArt=new Image();feedbackArt.src='../assets/art-spritesheet-slices/feedback-effects.png';function unrelated(){const CELL=1000;return CELL}function drawPiece(x,y){context.drawImage(playerArt,BOARD_X+x*CELL+2,BOARD_Y+y*CELL+2,CELL-4,CELL-4);context.drawImage(targetArt,BOARD_X+x*CELL+2,BOARD_Y+y*CELL+2,CELL-4,CELL-4);context.drawImage(sceneArt,BOARD_X+x*CELL+2,BOARD_Y+y*CELL+2,CELL-4,CELL-4);context.drawImage(feedbackArt,BOARD_X+x*CELL+2,BOARD_Y+y*CELL+2,CELL-4,CELL-4)}for(let y=0;y<ROWS;y++){for(let x=0;x<COLS;x++){drawPiece(x,y)}}})();</script></body></html>";
advance_game_index_revision(&root, &code_state, html);
// 夹具只为数值常量作用域判据构造,缺完整 static-smoke 合同要的目标说明、
// 主循环与输入监听;补上这层再落盘,被探测的常量引用形状不变。
advance_game_index_revision(&root, &code_state, &with_static_smoke_contract(html));
mark_verification_passed(&root, &code_state, "game.static_smoke");
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none());
}
@@ -7123,7 +7125,7 @@ fn cli_code_prototype_accepts_linked_inline_and_external_modules_for_canvas_atla
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><canvas width=320 height=180></canvas><script type='module'>const context=document.querySelector('canvas').getContext('2d');const sheet=new Image();sheet.src='../assets/art-spritesheet.png';function renderAtlas(){context.drawImage(sheet,0,0,32,32,0,0,64,64)}renderAtlas();</script></body></html>",
&with_static_smoke_contract("<!doctype html><html><body><canvas width=320 height=180></canvas><script type='module'>const context=document.querySelector('canvas').getContext('2d');const sheet=new Image();sheet.src='../assets/art-spritesheet.png';function renderAtlas(){context.drawImage(sheet,0,0,32,32,0,0,64,64)}renderAtlas();</script></body></html>"),
);
mark_verification_passed(&root, &code_state, "game.static_smoke");
assert!(
@@ -7139,7 +7141,9 @@ fn cli_code_prototype_accepts_linked_inline_and_external_modules_for_canvas_atla
advance_game_index_revision(
&root,
&code_state,
"<!doctype html><html><body><canvas width=320 height=180></canvas><script type='module' src='./render.mjs'></script></body></html>",
&with_static_smoke_contract(
"<!doctype html><html><body><canvas width=320 height=180></canvas><script type='module' src='./render.mjs'></script></body></html>",
),
);
mark_verification_passed(&root, &code_state, "game.static_smoke");
assert!(
@@ -8105,6 +8109,12 @@ fn autonomous_owner_artifact_runtime_validation_unblocks_real_new_project_withou
append_game_creator_agent_runtime_task(&root, &state)
.expect("persist running design foundation child");
// 上面刚断言过占位入口过不了真实 smoke。后面要伪造一次 static-smoke 通过来制造
// 「过期凭证」,而收口时会按当前磁盘内容复核入口页——占位页必然被拒。先把入口
// 换成合规页面,再走 owner 产物;verified_revision 仍取最后一次 owner 产物,
// 后续 revision 断言不受影响。
advance_game_index_revision(&root, &state, cropped_spritesheet_game_html());
advance_owner_artifact_revision(
&root,
&state,
@@ -8410,7 +8420,11 @@ fn autonomous_gui_cli_code_prototype_terminal_projection_requires_own_static_smo
let mutation_revision = advance_game_index_revision(
&root,
&state,
"<!doctype html><html><body><canvas width='320' height='180'></canvas><script>requestAnimationFrame(()=>{});</script></body></html>",
// 同一份入口后面要被记为 static-smoke 通过,落盘时就得满足完整合同;
// 此处只写一次、revision 不变,前面对 mutation/verified revision 的断言不受影响。
&with_static_smoke_contract(
"<!doctype html><html><body><canvas width='320' height='180'></canvas></body></html>",
),
);
mark_verification_passed(&root, &state, "project.verify");
let project_verified_gate = read_game_creator_agent_runtime_verification_gate(
@@ -8495,6 +8509,9 @@ fn autonomous_gui_cli_code_prototype_terminal_projection_requires_own_static_smo
legacy_gate.last_verification_status =
Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED.to_string());
legacy_gate.static_smoke_verified_revision = None;
// 入口摘要与 revision 是一对:仿造 legacy gate 时必须一起清,否则 gate
// 自身的一致性校验会先于被测行为拒收。
legacy_gate.static_smoke_verified_game_index_sha256 = None;
write_game_creator_agent_runtime_verification_gate(&root, &legacy_gate)
.expect("persist legacy project.verify-only completed code gate");
let root_blocker = autonomous_game_build_completion_blocker_at_locked(&root, &parent_state)
@@ -1063,6 +1063,15 @@ async fn autonomous_manifest_code_prototype_requires_its_own_static_smoke_after_
.collect::<BTreeSet<_>>()
);
// 收口 static-smoke 时会按磁盘内容复核 game/index.html。该夹具只写了
// package.json,入口仍是初始化留下的无画布占位页,复核必然拒收。落一份满足
// 完整合同的入口;纯文件写入不触碰 revision 记账,上面对 mutation / verified
// revision 的断言不受影响。
fs::write(
root.join("game/index.html"),
"<!doctype html><html><body><p>目标:移动角色收集全部目标并获得胜利;碰到危险即失败,按 R 重新开始。</p><canvas width=320 height=180></canvas><script>const canvas=document.querySelector('canvas');const context=canvas.getContext('2d');let playerX=0;window.addEventListener('keydown',(event)=>{if(event.key==='ArrowRight'){playerX+=1;}if(event.key==='r'||event.key==='R'){playerX=0;}});function draw(){context.clearRect(0,0,canvas.width,canvas.height);context.fillStyle='#ffffff';context.fillRect(playerX,0,8,8);context.fillText(String(playerX),8,172);requestAnimationFrame(draw);}requestAnimationFrame(draw);</script></body></html>",
)
.expect("write a smoke-contract compliant game entry before the static smoke credential");
persist_project_verification_for_test(
&root,
"code-prototype",