diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs index 4cc55a173..39a99fb88 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs @@ -3494,7 +3494,7 @@ struct JavascriptImportReferenceSpan { struct JavascriptModuleAnalysis { static_sources: Vec, import_declaration_ranges: Vec>, - imports: Vec<(String, BTreeMap)>, + imports: Vec<(String, Vec<(String, String)>)>, used_import_locals: BTreeSet, import_reference_spans: BTreeMap>, namespace_import_members: BTreeMap>>>, @@ -3567,15 +3567,15 @@ impl<'a> VisitJavascript<'a> for JavascriptModuleAnalysisCollector { self.analysis .import_declaration_ranges .push(declaration.span.start as usize..declaration.span.end as usize); - let mut bindings = BTreeMap::new(); + let mut bindings = Vec::new(); for specifier in declaration.specifiers.iter().flatten() { match specifier { JavascriptImportDeclarationSpecifier::ImportSpecifier(specifier) => { let local = specifier.local.name.to_string().to_ascii_lowercase(); - bindings.insert( + bindings.push(( javascript_module_export_name(&specifier.imported), local.clone(), - ); + )); if let Some(symbol_id) = specifier.local.symbol_id.get() { self.import_symbols.push((local, symbol_id)); self.import_origins.insert( @@ -3589,7 +3589,7 @@ impl<'a> VisitJavascript<'a> for JavascriptModuleAnalysisCollector { } JavascriptImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => { let local = specifier.local.name.to_string().to_ascii_lowercase(); - bindings.insert("default".to_string(), local.clone()); + bindings.push(("default".to_string(), local.clone())); if let Some(symbol_id) = specifier.local.symbol_id.get() { self.import_symbols.push((local, symbol_id)); self.import_origins @@ -3598,7 +3598,7 @@ impl<'a> VisitJavascript<'a> for JavascriptModuleAnalysisCollector { } JavascriptImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => { let local = specifier.local.name.to_string().to_ascii_lowercase(); - bindings.insert("*".to_string(), local.clone()); + bindings.push(("*".to_string(), local.clone())); if let Some(symbol_id) = specifier.local.symbol_id.get() { self.import_symbols.push((local.clone(), symbol_id)); self.import_origins @@ -3607,7 +3607,16 @@ impl<'a> VisitJavascript<'a> for JavascriptModuleAnalysisCollector { } } } - self.analysis.imports.push((source, bindings)); + if let Some((_, existing)) = self + .analysis + .imports + .iter_mut() + .find(|(dependency, _)| dependency == &source) + { + existing.extend(bindings); + } else { + self.analysis.imports.push((source, bindings)); + } } fn visit_export_declaration(&mut self, declaration: &JavascriptExportDeclaration<'a>) { @@ -4281,7 +4290,7 @@ fn validate_javascript_module_links( ) -> Result<(), String> { for (module, analysis) in analyses { for (dependency, bindings) in &analysis.imports { - for exported in bindings.keys() { + for (exported, _) in bindings { if !javascript_module_resolves_export( dependency, exported, @@ -4335,10 +4344,8 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at( let mut pending = std::collections::VecDeque::new(); let mut module_contents = std::collections::BTreeMap::::new(); let mut module_analyses = BTreeMap::::new(); - let mut module_bindings = std::collections::BTreeMap::< - String, - Vec<(String, std::collections::BTreeMap)>, - >::new(); + let mut module_bindings = + std::collections::BTreeMap::)>>::new(); for source in executable_external_script_sources_from_html(html) { let local_path = local_gameplay_script_path_from(None, &source.source) .ok_or_else(|| format!("自主构建外部脚本路径不受支持:{}", source.source))?; @@ -4463,6 +4470,7 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at( .expect("masking parsed JavaScript imports preserves UTF-8"); let mut unit_replacements = Vec::<(std::ops::Range, String)>::new(); let mut added_projection = false; + let mut origins = BTreeMap::)>>::new(); for (dependency, bindings) in dependencies { if bindings.is_empty() { continue; @@ -4486,7 +4494,6 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at( } }) .collect::>(); - let mut origins = BTreeMap::)>>::new(); for (exported, local, namespace) in used_bindings { if let Some((origin, origin_export)) = javascript_module_export_origin( &dependency, @@ -4525,62 +4532,95 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at( } } } - for (origin, bindings) in origins { - let Some(origin_content) = module_contents.get(&origin) else { - continue; - }; - let used_names = bindings - .iter() - .map(|(exported, _, _)| exported.to_ascii_lowercase()) - .collect::>(); - let mut projection = - javascript_module_binding_projection(origin_content, &used_names); - for (exported, local, namespace) in bindings { - let origin_local = module_analyses - .get(&origin) - .and_then(|analysis| analysis.exports.get(&exported.to_ascii_lowercase())) - .and_then(|target| match target { - JavascriptExportTarget::Local(name) => Some(name.as_str()), - JavascriptExportTarget::Reexport { .. } => None, - }) - .unwrap_or(exported.as_str()); - if let Some(namespace) = namespace { - if let Some(ranges) = importer_analysis - .namespace_import_members - .get(&namespace) - .and_then(|members| members.get(&exported)) - { - unit_replacements.extend( - ranges - .iter() - .cloned() - .map(|range| (range, origin_local.to_string())), - ); - } - } else if origin_local == "__agc_default_export__" { - if !rename_javascript_root_binding(&mut projection, origin_local, &local) { - return Err(format!( - "自主构建模块匿名 default 投影绑定无法按符号重命名:{origin}::{exported} -> {local}" - )); - } - } else if origin_local != local { + } + for (origin, bindings) in origins { + let Some(origin_content) = module_contents.get(&origin) else { + continue; + }; + let used_names = bindings + .iter() + .map(|(exported, _, _)| exported.to_ascii_lowercase()) + .collect::>(); + let mut projection = javascript_module_binding_projection(origin_content, &used_names); + let mut synthetic_aliases = BTreeMap::::new(); + for (exported, local, namespace) in &bindings { + let origin_local = module_analyses + .get(&origin) + .and_then(|analysis| analysis.exports.get(&exported.to_ascii_lowercase())) + .and_then(|target| match target { + JavascriptExportTarget::Local(name) => Some(name.as_str()), + JavascriptExportTarget::Reexport { .. } => None, + }) + .unwrap_or(exported.as_str()); + if namespace.is_none() && origin_local == "__agc_default_export__" { + synthetic_aliases + .entry(origin_local.to_string()) + .or_insert_with(|| local.clone()); + } + } + for (synthetic, canonical) in &synthetic_aliases { + if !rename_javascript_root_binding(&mut projection, synthetic, canonical) { + return Err(format!( + "自主构建模块匿名 default 投影绑定无法按符号重命名:{origin}::{synthetic} -> {canonical}" + )); + } + } + for (exported, local, namespace) in bindings { + let origin_local = module_analyses + .get(&origin) + .and_then(|analysis| analysis.exports.get(&exported.to_ascii_lowercase())) + .and_then(|target| match target { + JavascriptExportTarget::Local(name) => Some(name.as_str()), + JavascriptExportTarget::Reexport { .. } => None, + }) + .unwrap_or(exported.as_str()); + let projected_local = synthetic_aliases + .get(origin_local) + .map(String::as_str) + .unwrap_or(origin_local); + if let Some(namespace) = namespace { + if let Some(ranges) = importer_analysis + .namespace_import_members + .get(&namespace) + .and_then(|members| members.get(&exported)) + { + unit_replacements.extend( + ranges + .iter() + .cloned() + .map(|range| (range, projected_local.to_string())), + ); + } + } else if origin_local == "__agc_default_export__" { + if projected_local != local { if let Some(spans) = importer_analysis.import_reference_spans.get(&local) { unit_replacements.extend(spans.iter().map(|span| { let replacement = if span.shorthand { - format!("{local}: {origin_local}") + format!("{local}: {projected_local}") } else { - origin_local.to_string() + projected_local.to_string() }; (span.range.clone(), replacement) })); } } + } else if origin_local != local { + if let Some(spans) = importer_analysis.import_reference_spans.get(&local) { + unit_replacements.extend(spans.iter().map(|span| { + let replacement = if span.shorthand { + format!("{local}: {origin_local}") + } else { + origin_local.to_string() + }; + (span.range.clone(), replacement) + })); + } } - if !projection.is_empty() { - unit.push('\n'); - unit.push_str(&projection); - added_projection = true; - } + } + if !projection.is_empty() { + unit.push('\n'); + unit.push_str(&projection); + added_projection = true; } } if added_projection { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs index eed40af04..ccb8fbad7 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs @@ -2772,6 +2772,153 @@ fn javascript_module_projection_uses_symbols_for_dependencies_and_renames() { ); } +#[test] +fn javascript_module_projection_preserves_duplicate_aliases_without_duplicate_origins() { + let temporary = tempfile::tempdir().expect("create duplicate alias module project"); + let root = temporary.path(); + fs::create_dir_all(root.join("game")).expect("create game directory"); + fs::write( + root.join("game/origin.mjs"), + "export function rotatePiece() { return 'rotated'; }", + ) + .expect("write shared origin module"); + let html = ""; + + for (label, source) in [ + ( + "one declaration with two aliases", + "import { rotatePiece as turnLeft, rotatePiece as turnRight } from './origin.mjs'; turnLeft(); turnRight();", + ), + ( + "two declarations from one dependency", + "import { rotatePiece as turnLeft } from './origin.mjs'; import { rotatePiece as turnRight } from './origin.mjs'; turnLeft(); turnRight();", + ), + ] { + fs::write(root.join("game/main.mjs"), source).expect("write duplicate alias importer"); + let modules = read_external_gameplay_javascript_at(root, html) + .unwrap_or_else(|error| panic!("read {label}: {error}")); + let projected = modules + .module_units() + .iter() + .find(|unit| { + unit.matches("rotatepiece();").count() == 2 + && unit.contains("function rotatepiece()") + }) + .unwrap_or_else(|| panic!("both aliases must remain projected for {label}")); + assert_eq!( + projected.matches("function rotatepiece()").count(), + 1, + "one origin must be projected once for {label}", + ); + } + + fs::write( + root.join("game/bridge-a.mjs"), + "export { rotatePiece } from './origin.mjs';", + ) + .expect("write first re-export bridge"); + fs::write( + root.join("game/bridge-b.mjs"), + "export { rotatePiece } from './origin.mjs';", + ) + .expect("write second re-export bridge"); + fs::write( + root.join("game/main.mjs"), + "import { rotatePiece as turnLeft } from './bridge-a.mjs'; import { rotatePiece as turnRight } from './bridge-b.mjs'; turnLeft(); turnRight();", + ) + .expect("write importer with two bridges to one origin"); + let modules = read_external_gameplay_javascript_at(root, html) + .expect("read two bridges to one origin module"); + let projected = modules + .module_units() + .iter() + .find(|unit| { + unit.matches("rotatepiece();").count() == 2 && unit.contains("function rotatepiece()") + }) + .expect("both bridged aliases must resolve to the shared origin"); + assert_eq!( + projected.matches("function rotatepiece()").count(), + 1, + "one origin reached through multiple bridges must be projected once", + ); + + fs::write( + root.join("game/origin.mjs"), + "export default function() { return 'rotated'; }", + ) + .expect("write anonymous default origin module"); + for (label, source) in [ + ( + "anonymous default aliases", + "import turnLeft from './origin.mjs'; import turnRight from './origin.mjs'; turnLeft(); turnRight();", + ), + ( + "anonymous default namespace and alias", + "import * as gameplay from './origin.mjs'; import turnRight from './origin.mjs'; gameplay.default(); turnRight();", + ), + ] { + fs::write(root.join("game/main.mjs"), source).expect("write anonymous default importer"); + let modules = read_external_gameplay_javascript_at(root, html) + .unwrap_or_else(|error| panic!("read {label}: {error}")); + let projected = modules + .module_units() + .iter() + .find(|unit| unit.matches("turnleft();").count() == 2) + .or_else(|| { + modules + .module_units() + .iter() + .find(|unit| unit.matches("turnright();").count() == 2) + }) + .unwrap_or_else(|| panic!("all anonymous default references must share one alias for {label}")); + assert_eq!( + projected.matches("const turnleft =").count() + + projected.matches("const turnright =").count(), + 1, + "an anonymous default origin must receive one canonical declaration for {label}", + ); + assert!( + !projected.contains("__agc_default_export__"), + "the synthetic anonymous default name must not leak into {label}", + ); + } + + fs::write( + root.join("game/bridge-a.mjs"), + "export { default } from './origin.mjs';", + ) + .expect("write first default bridge"); + fs::write( + root.join("game/bridge-b.mjs"), + "export { default } from './origin.mjs';", + ) + .expect("write second default bridge"); + fs::write( + root.join("game/main.mjs"), + "import turnLeft from './bridge-a.mjs'; import turnRight from './bridge-b.mjs'; turnLeft(); turnRight();", + ) + .expect("write anonymous default importer through two bridges"); + let modules = read_external_gameplay_javascript_at(root, html) + .expect("read anonymous default through two bridges"); + let projected = modules + .module_units() + .iter() + .find(|unit| unit.matches("turnleft();").count() == 2) + .or_else(|| { + modules + .module_units() + .iter() + .find(|unit| unit.matches("turnright();").count() == 2) + }) + .expect("bridged anonymous default aliases must share one declaration"); + assert_eq!( + projected.matches("const turnleft =").count() + + projected.matches("const turnright =").count(), + 1, + "an anonymous default reached through two bridges must be projected once", + ); +} + #[test] fn game_chat_pure_continue_does_not_inherit_across_sessions() { let (_temporary, root, original_state, original_contract) = diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index c6a857383..80240d960 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -5977,6 +5977,6 @@ ## 2026-08-04 图集事务与 Tetris 完成门使用句柄和 AST 收口 - 图集事务:九文件旧合同在写 `prepared` 前必须全部持有可信源句柄并整体复读;Unix 事务控制文件统一通过锚定目录句柄的 `openat / unlinkat + O_NOFOLLOW + O_NONBLOCK` 操作,FIFO 等非普通文件必须在读取前失败关闭,恢复前态也必须在同一叶子句柄上稳定双读并复核前后元数据与当前 inode。Windows 祖先 pin 只请求读访问并拒绝 delete sharing,可重复持有;只有事务叶子句柄请求删除访问。事务捕获与恢复 CAS 从 canonical 项目根句柄逐组件打开或创建父目录,staging、no-replace link/move 与 unlink 均相对固定父目录句柄执行;清理事务证据前再次复核整组安装结果。安装后的任何清理错误都按实际 canonical 状态把当前项纳入逆序回滚,不能留下新旧混合合同。 -- JavaScript / ESM:Tetris 静态连续性检查以 Oxc parser、semantic 与 AST visitor 为权威。无效语法、ASI、template interpolation、正则 / 注释、表达式体箭头、参数和词法遮蔽、export alias、import 后再 export 的 bridge、re-export 与缺失导出链接不再由字符串扫描猜测;跨模块 alias 保留 origin 根绑定名称,只按解析到 import symbol 的 reference span 改写 importer,object shorthand 展开后保留原键;namespace 只改写绑定到 import symbol 的完整 member span,同文本属性和局部遮蔽均不得连带改写。HTML `type` 存在时优先于 legacy `language`。源码投影仍只是静态语义门,最终完成继续要求绑定当前 revision 的真实 Chromium 固定试玩回执。 +- JavaScript / ESM:Tetris 静态连续性检查以 Oxc parser、semantic 与 AST visitor 为权威。无效语法、ASI、template interpolation、正则 / 注释、表达式体箭头、参数和词法遮蔽、export alias、import 后再 export 的 bridge、re-export 与缺失导出链接不再由字符串扫描猜测;跨模块 alias 保留 origin 根绑定名称,只按解析到 import symbol 的 reference span 改写 importer。同一 export 的多个本地 alias 保序保留,同一 dependency 的多条 import declaration 合并绑定;投影在 importer 内按最终 origin 聚合,因此同一 origin 经不同 dependency 或 bridge 到达时也只生成一次根声明。object shorthand 展开后保留原键;namespace 只改写绑定到 import symbol 的完整 member span,同文本属性和局部遮蔽均不得连带改写。HTML `type` 存在时优先于 legacy `language`。源码投影仍只是静态语义门,最终完成继续要求绑定当前 revision 的真实 Chromium 固定试玩回执。 - 浏览器因果:状态证据仍只冻结 trusted input listener 及其点击派生微任务内的变化;完整手势身份改由宿主在成功完成 Chromium 元素鼠标输入后调用隔离世界 finish。更早注册的 `window` capture listener 即使调用 `stopImmediatePropagation()` 也不能阻断探针自身的完成身份,页面脚本不能伪造 host finish,RAF / timer 继续不计入动作结果。 - 验证边界:Linux 定向回归覆盖目录相对读写与清理、祖先 symlink、CAS 安装后错误、九文件混合快照、Tetris AST 反例和七项真实 Chrome generic 试玩。Windows cfg 代码必须继续在真实 Windows CI / 发布构建验证;本地缺少 MinGW C compiler 时,安装了 Rust target 也不能把交叉 `cargo check` 失败误报为源码失败。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 2bb4d84b5..766d31974 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -829,7 +829,7 @@ game-project/ - `assets/art-spec.png` 的唯一语义是视觉规范与派生参考,不是运行时背景、角色、目标或图集。game-chat 的核心玩家、方块/目标、障碍/场景和反馈必须来自独立派生的透明 `assets/art-spritesheet.png` 及其服务端 `iconImageSrcs` 本地切片;Runtime 以 `sourceResourceId` 把切片清单绑定到当前图集,并要求活动 Canvas 分别绘制四类不同切片。纯代码核心实体、猜测图集等分坐标、单个裁切冒充全部类别、整图展示、隐藏引用、微小水印和诱饵路径均不构成真实美术使用。`playable-web-game-state.v1.sequence` 只在真实输入、状态迁移或模拟状态变化时递增,不得由纯渲染帧推进。 - game-chat canonical 图集进一步要求主图与四个切片都有非空且互不复用的 Canvas `assetObjectId`;同一对象在顶层、resource 与 asset 中重复返回的 `assetObjectId` 和 `taskId` 必须分别一致,冲突时失败关闭。公开 `assets/art-spritesheet-slices/manifest.json` 与私有 `.agent/runtime/art-spritesheet-contract.json` 必须同时绑定 `sourceResourceId / sourceAssetObjectId / sourceTaskId / sourceCanvasProjectId / sourceReferenceResourceIds`,并对四种 usage 的 `name / path / width / height / resourceId / assetObjectId / contentSha256 / pixelSha256` 做完整一致性比较。旧项目缺私有回执时不得从公开文件反向生成回执;只允许同一 game-chat root 下处于 running 的 scheduled `art-asset-plan` 对固定主图执行受限 `replaceExisting=true` repair,普通 pending、其它 Agent、其它路径或有效合同均拒绝。九个固定合同文件在任何 canonical 改动前必须先全部打开可信源句柄,完成有界双读,并在 journal 持久化后、发布 `prepared` marker 前再次整体复读与身份校验;任一文件在九文件捕获窗口变化都失败关闭,不能形成跨版本混合快照。事务控制文件在 Unix 通过锚定目录句柄的 `openat / unlinkat + O_NOFOLLOW + O_NONBLOCK` 读取和清理,FIFO 等非普通文件不得阻塞恢复;恢复前态在同一叶子句柄稳定双读并复核元数据、内容和当前 inode。Windows 祖先目录 pin 只请求读访问并拒绝 delete sharing,允许九文件捕获重复持有同一 root;只有事务叶子句柄请求删除访问。事务目录创建、合同源捕获与恢复 CAS 必须从 canonical 项目根句柄逐组件打开或创建父目录,staging、hard-link、no-replace move 与 unlink 全部相对固定父目录句柄执行,不能退回 pathname 预检后操作。CAS 安装先把目标字节持久化到同目录私有 staging,再以不覆盖切换安装;即使 canonical 已安装后清理 backup/staging 才报错,也必须把当前项纳入同轮逆序回滚。Canvas 资产登记成功并写 `committed` marker 后才可清理;恢复继续在同一项目写锁内完整验证全部 journal 条目和快照、形成内存恢复计划并缓存全部 canonical 的恢复前状态,逐项安装完成后、清理事务证据前还必须再次复核整组 installed 状态,再按 transaction id 整组回滚或幂等清理。回滚前必须 CAS 证明目标仍是本轮安装结果,外部修改不得被覆盖,冲突进入 reconciliation。远端下载阶段不得长期持锁,也不得在无锁 request 阶段修改 canonical 文件。 - 图集本地提交以主图 staging 为线性化前置:任何新主图先写随机私有 staging 文件,替换时保留 previous,canonical 主图完整安装后才写四切片、公开清单、私有回执和项目资产登记。进程若在 backup/install 窗口退出,同一 accepted External generation 恢复先识别唯一同 suffix 的 previous/replacement 对并恢复旧主图,再按远端结果完成替换;若 canonical 已等于远端摘要,则不再要求替换授权,直接补齐其余合同。成功后清理主图、四切片、公开清单、私有回执和项目 manifest 的全部遗留 staging/backup。首次生成也禁止直接流式写 canonical 路径,避免部分 PNG 被误认为已安装结果。 -- 俄罗斯方块任务固定使用 `BrowserPlaytestScenario::TetrisV1`。`playable-web-game-state.v1.gameplay` 必须持续提供 `kind=tetris`、`activePieceId`、`rotation`、`row`、`lockedPieces`、`lineClearChecks`、`clearedLines` 和 `occupiedCells`;任何采样点删除字段都立即失败,但允许 `score / nextPieceId` 等额外 telemetry。静态连续性检查按 HTML 规定的五种空白解析标签,并在 `type` 存在时忽略 legacy `language`,再结合 `nomodule` 判断可执行脚本。JavaScript / ESM 必须先通过 Oxc parser 与 semantic;无效语法失败关闭,import/export production、ASI、default / namespace / alias / import 后再 export 的 bridge / re-export 链接、template `${...}` 内表达式、注释与正则边界都以 AST 为权威,不得跨换行猜测 `from` 或把未链接模块当完成证据。跨模块 alias 投影保留 origin 根绑定,只改写解析到 import symbol 的 importer reference span;object shorthand 展开为显式键值以保留原键,namespace 只改写完整 member span。同文本对象属性和局部遮蔽不得连带改写,投影依赖也只由 semantic 未解析根引用递归纳入。函数定义、表达式体箭头、调用可达性和参数 / 局部遮蔽按 semantic symbol identity 判断;guard return / throw 只终止其真实控制流分支,不能截断后续可达玩法。字符串、注释、HTML raw-text/RCDATA 与其它非执行容器、带 `src` 脚本的内联正文、非 JavaScript script、短路动态 import、恒真分支的 else、顶层无条件 return / throw 后正文和 `if(false)` / 明显恒假分支诱饵继续不构成证据;filter / splice 消行仍必须由满行判断真实控制,并作用于正式棋盘。本地 `.js / .mjs`、inline module 与传递依赖统一限制在 `game/`,文件按去重数量并受 256 文件、累计 2 MiB 上限约束。浏览器因果探针运行于 Chromium 隔离执行上下文,Promise 闭包保存点击前基线,MutationObserver 只冻结 trusted 输入 listener 及其点击派生微任务产生的最后状态;宿主只有在 Chromium 元素鼠标输入成功完成后才调用隔离世界 finish,把该 CDP 结果作为完整手势证据,页面无法伪造。这样更早注册的 `window` capture listener 即使 `stopImmediatePropagation()`,以及后注册的同步 click listener,都不会造成假阴性;RAF / timer 仍不会污染证据。探针 fingerprint 覆盖 install、ready 与 finish 的真实脚本。其余同方块旋转、重力/锁定、四格落盘或消行、`lineClearChecks` 和 restart 归零约束保持不变。旧/续跑合同及 game-chat 快车道在回执读取前按有效原任务迁移到该场景、重算 fingerprint 并回读一致,旧 generic-v1 回执视为 stale,不能交付完成。 +- 俄罗斯方块任务固定使用 `BrowserPlaytestScenario::TetrisV1`。`playable-web-game-state.v1.gameplay` 必须持续提供 `kind=tetris`、`activePieceId`、`rotation`、`row`、`lockedPieces`、`lineClearChecks`、`clearedLines` 和 `occupiedCells`;任何采样点删除字段都立即失败,但允许 `score / nextPieceId` 等额外 telemetry。静态连续性检查按 HTML 规定的五种空白解析标签,并在 `type` 存在时忽略 legacy `language`,再结合 `nomodule` 判断可执行脚本。JavaScript / ESM 必须先通过 Oxc parser 与 semantic;无效语法失败关闭,import/export production、ASI、default / namespace / alias / import 后再 export 的 bridge / re-export 链接、template `${...}` 内表达式、注释与正则边界都以 AST 为权威,不得跨换行猜测 `from` 或把未链接模块当完成证据。跨模块 alias 投影保留 origin 根绑定,只改写解析到 import symbol 的 importer reference span;同一 export 的多个本地 alias 必须保序保留,同一 dependency 的多条 import declaration 合并绑定,同一 origin 即使经不同 dependency 或 bridge 到达也只投影一次。object shorthand 展开为显式键值以保留原键,namespace 只改写完整 member span。同文本对象属性和局部遮蔽不得连带改写,投影依赖也只由 semantic 未解析根引用递归纳入。函数定义、表达式体箭头、调用可达性和参数 / 局部遮蔽按 semantic symbol identity 判断;guard return / throw 只终止其真实控制流分支,不能截断后续可达玩法。字符串、注释、HTML raw-text/RCDATA 与其它非执行容器、带 `src` 脚本的内联正文、非 JavaScript script、短路动态 import、恒真分支的 else、顶层无条件 return / throw 后正文和 `if(false)` / 明显恒假分支诱饵继续不构成证据;filter / splice 消行仍必须由满行判断真实控制,并作用于正式棋盘。本地 `.js / .mjs`、inline module 与传递依赖统一限制在 `game/`,文件按去重数量并受 256 文件、累计 2 MiB 上限约束。浏览器因果探针运行于 Chromium 隔离执行上下文,Promise 闭包保存点击前基线,MutationObserver 只冻结 trusted 输入 listener 及其点击派生微任务产生的最后状态;宿主只有在 Chromium 元素鼠标输入成功完成后才调用隔离世界 finish,把该 CDP 结果作为完整手势证据,页面无法伪造。这样更早注册的 `window` capture listener 即使 `stopImmediatePropagation()`,以及后注册的同步 click listener,都不会造成假阴性;RAF / timer 仍不会污染证据。探针 fingerprint 覆盖 install、ready 与 finish 的真实脚本。其余同方块旋转、重力/锁定、四格落盘或消行、`lineClearChecks` 和 restart 归零约束保持不变。旧/续跑合同及 game-chat 快车道在回执读取前按有效原任务迁移到该场景、重算 fingerprint 并回读一致,旧 generic-v1 回执视为 stale,不能交付完成。 - 泥点不足是确定性业务中断,不是瞬态 Provider 故障或未知副作用。钱包的 `泥点余额不足` 与 `可消费泥点不足:...` 两种领域文案统一映射为稳定原因 `mud-points-insufficient`,不得自动重试;即使 External Generation durable ledger 已存在,也必须落为 `failed`,不能误入 `needs-reconciliation`。game-chat 顶部状态、持久失败对话与 `【Supervisor 阶段记录】` 统一显示“泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。”,并禁止透传 operationId、URL、路径、密钥或任意上游正文。 - tool-plan 成功响应落账前,对内置 Runtime 原生函数与 legacy wrapper 的合法、无重复 key JSON arguments 按工具 schema 的精确位置做项目路径 canonicalization:`file.*.path`、`project.patchset.changes[*].path`、`project.git_commit.paths[*]`、`command.*.cwd`、`image.inspect.paths[*]` 与 `canvas.asset_generate.outputPath` 若是当前项目根目录内的完整绝对路径,转换为 `/` 分隔的项目相对路径后再校验、持久化并执行;源码/叙述字段、任务产物描述、动态 MCP arguments 和项目外绝对路径不得改写,后两者继续由绝对路径门禁失败关闭。项目根只允许搜索/列举范围与命令 cwd 规范化为 `.`,不能成为文件目标。当前进程与重启恢复都必须从同一份规范化 handoff 重放,禁止分别执行原响应和持久响应。