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 140cdc7b4..fa100abe1 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 @@ -6944,6 +6944,107 @@ fn javascript_top_level_declarations(content: &str) -> std::collections::BTreeMa declarations } +fn javascript_expression_root_symbol( + expression: &JavascriptExpression<'_>, + scoping: &JavascriptScoping, +) -> Option { + match expression { + JavascriptExpression::Identifier(identifier) => identifier + .reference_id + .get() + .and_then(|reference_id| scoping.get_reference(reference_id).symbol_id()), + JavascriptExpression::ParenthesizedExpression(parenthesized) => { + javascript_expression_root_symbol(&parenthesized.expression, scoping) + } + _ => expression + .as_member_expression() + .and_then(|member| javascript_expression_root_symbol(member.object(), scoping)), + } +} + +fn javascript_assignment_target_root_symbol( + target: &oxc_ast::ast::AssignmentTarget<'_>, + scoping: &JavascriptScoping, +) -> Option { + match target { + oxc_ast::ast::AssignmentTarget::AssignmentTargetIdentifier(identifier) => identifier + .reference_id + .get() + .and_then(|reference_id| scoping.get_reference(reference_id).symbol_id()), + _ => target + .as_member_expression() + .and_then(|member| javascript_expression_root_symbol(member.object(), scoping)), + } +} + +fn javascript_top_level_assignment_writes(content: &str) -> BTreeMap> { + let allocator = JavascriptAllocator::default(); + let parsed = JavascriptParser::new(&allocator, content, javascript_source_type(true)).parse(); + if parsed.panicked || !parsed.diagnostics.is_empty() { + return BTreeMap::new(); + } + let semantic = JavascriptSemanticBuilder::new_compiler().build(&parsed.program); + if !semantic.diagnostics.is_empty() { + return BTreeMap::new(); + } + let scoping = semantic.semantic.scoping(); + let root_names = scoping + .get_bindings(scoping.root_scope_id()) + .iter() + .map(|(name, symbol_id)| (*symbol_id, name.to_string())) + .collect::>(); + let mut writes = BTreeMap::>::new(); + for statement in &parsed.program.body { + let JavascriptStatement::ExpressionStatement(statement) = statement else { + continue; + }; + let JavascriptExpression::AssignmentExpression(assignment) = &statement.expression else { + continue; + }; + let Some(symbol_id) = javascript_assignment_target_root_symbol(&assignment.left, scoping) + else { + continue; + }; + let Some(name) = root_names.get(&symbol_id) else { + continue; + }; + let span = statement.span; + let Some(source) = content.get(span.start as usize..span.end as usize) else { + continue; + }; + writes + .entry(name.clone()) + .or_default() + .push((span.start as usize, source.to_string())); + } + writes +} + +fn javascript_projection_fragment_dependencies( + fragment: &str, + declarations: &BTreeMap, +) -> Option> { + let allocator = JavascriptAllocator::default(); + let parsed = JavascriptParser::new(&allocator, fragment, javascript_source_type(true)).parse(); + if parsed.panicked || !parsed.diagnostics.is_empty() { + return None; + } + let semantic = JavascriptSemanticBuilder::new_compiler().build(&parsed.program); + if !semantic.diagnostics.is_empty() { + return None; + } + Some( + semantic + .semantic + .scoping() + .root_unresolved_references() + .keys() + .map(|dependency| dependency.as_str().to_string()) + .filter(|dependency| declarations.contains_key(dependency)) + .collect(), + ) +} + pub(in crate::agent) fn javascript_module_binding_projection( content: &str, imported: &BTreeSet, @@ -6952,6 +7053,7 @@ pub(in crate::agent) fn javascript_module_binding_projection( return String::new(); }; let mut declarations = javascript_top_level_declarations(content); + let assignment_writes = javascript_top_level_assignment_writes(content); declarations.extend( analysis .synthetic_declarations @@ -6971,6 +7073,8 @@ pub(in crate::agent) fn javascript_module_binding_projection( .collect::>(); let mut included = BTreeSet::new(); let mut included_declarations = BTreeSet::new(); + let mut included_write_bindings = BTreeSet::new(); + let mut selected_writes = BTreeMap::::new(); let mut projection = String::new(); while let Some(name) = pending.pop() { if !included.insert(name.clone()) { @@ -6979,33 +7083,42 @@ pub(in crate::agent) fn javascript_module_binding_projection( let Some(declaration) = declarations.get(&name) else { continue; }; - if !included_declarations.insert(declaration.clone()) { - continue; + if included_declarations.insert(declaration.clone()) { + projection.push_str(declaration); + projection.push('\n'); + let Some(dependencies) = + javascript_projection_fragment_dependencies(declaration, &declarations) + else { + return String::new(); + }; + pending.extend( + dependencies + .into_iter() + .filter(|dependency| !included.contains(dependency)), + ); } - projection.push_str(declaration); - projection.push('\n'); - let allocator = JavascriptAllocator::default(); - let parsed = - JavascriptParser::new(&allocator, declaration, javascript_source_type(true)).parse(); - if parsed.panicked || !parsed.diagnostics.is_empty() { - return String::new(); - } - let semantic = JavascriptSemanticBuilder::new_compiler().build(&parsed.program); - if !semantic.diagnostics.is_empty() { - return String::new(); - } - for dependency in semantic - .semantic - .scoping() - .root_unresolved_references() - .keys() - { - let dependency = dependency.as_str().to_string(); - if declarations.contains_key(&dependency) && !included.contains(&dependency) { - pending.push(dependency); + if included_write_bindings.insert(name.clone()) { + for (position, write) in assignment_writes.get(&name).into_iter().flatten() { + selected_writes + .entry(*position) + .or_insert_with(|| write.clone()); + let Some(dependencies) = + javascript_projection_fragment_dependencies(write, &declarations) + else { + return String::new(); + }; + pending.extend( + dependencies + .into_iter() + .filter(|dependency| !included.contains(dependency)), + ); } } } + for write in selected_writes.into_values() { + projection.push_str(&write); + projection.push('\n'); + } projection } @@ -10259,6 +10372,57 @@ mod javascript_projection_reachability_tests { ); } + #[test] + fn exported_roots_keep_direct_top_level_initialization_writes() { + for (content, exported, assignment, dependency) in [ + ( + "const implementation = () => rotateBoard(); function rotateBoard() { return import('./live-binding.mjs'); } export let rotatePiece; rotatePiece = implementation; const decoy = {}; decoy.rotatePiece = () => import('./decoy.mjs');", + "rotatePiece", + "rotatePiece = implementation", + "const implementation =", + ), + ( + "const implementation = () => import('./object-member.mjs'); export const api = {}; api.rotatePiece = implementation;", + "api", + "api.rotatePiece = implementation", + "const implementation =", + ), + ( + "const implementation = function() { return import('./prototype-member.mjs'); }; export class Game {} Game.prototype.rotatePiece = implementation;", + "Game", + "Game.prototype.rotatePiece = implementation", + "const implementation =", + ), + ] { + let projection = javascript_module_binding_projection( + content, + &BTreeSet::from([exported.to_string()]), + ); + assert!( + projection.contains(assignment), + "an exported root must retain its direct module-initialization write: {projection}", + ); + assert!( + projection.contains(dependency), + "the initialization RHS dependency must join the projection: {projection}", + ); + assert!( + projection.find(dependency) < projection.find(assignment), + "dependencies must be declared before deferred initialization writes: {projection}", + ); + assert!(javascript_is_syntactically_valid(&projection, true)); + } + + let projection = javascript_module_binding_projection( + "export const api = {}; const decoy = {}; decoy.rotatePiece = () => import('./decoy.mjs');", + &BTreeSet::from(["api".to_string()]), + ); + assert!( + !projection.contains("decoy.rotatePiece"), + "writes rooted at an unrelated binding must remain outside the projection", + ); + } + #[test] fn imported_custom_map_does_not_execute_a_local_callback() { let content = "import { scheduler } from './scheduler.mjs'; function decoy() { return import('./missing-decoy.mjs'); } scheduler.map(decoy);"; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 3a709610c..af7b0fd71 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -5992,3 +5992,4 @@ - JavaScript / ESM alias 求值顺序补充:receiver alias 保存赋值完成时刻并在该时刻解析 source owner,后续 source 重赋值不得倒灌。调用事件按内层参数 / RHS 先于外层调用 / assignment 生效;`switch case/default` 赋值一律保留跳过与各分支可能状态,普通函数内无条件 `return / throw` 截断之后的 alias 副作用。恒真 / 恒假关键字大小写敏感,可能被局部或参数遮蔽的 `undefined` 不再作为文本恒假值。 - JavaScript / ESM callee 与终止顺序补充:identifier callee 和 `new C(args)` 的 constructor / instance owner 在实参前冻结,invocation effect 保留在实参之后;callable assignment 到 RHS 完成后才生效,`start = start()` 继续调用旧值。`return / throw` 表达式中的 assignment / call 先执行,截断点取表达式之后的 AST statement end;函数体使用 Oxc body span,不从默认参数或解构参数中的首个 `{` 猜测。未知 guard clause 后续与 `catch` 体一律按 conditional effect 合并旧状态。 - JavaScript callable 分支与参数快照补充:conditional expression 必须在 test 求值完成后,分别于 consequent / alternate 自身起点冻结 callable identity;受控 callback 参数按该参数自身起点解析,前置参数产生的 alias 副作用先于后续 callback identity 生效,callback 的执行边仍保留在注册调用完成位置。 +- JavaScript / ESM live binding 投影补充:被选 export root 的直接顶层 assignment 及其 RHS 依赖必须与原声明共同投影,覆盖 `export let x; x = impl`、导出对象成员安装和 class prototype 安装;assignment target 以 semantic root symbol 归属,函数体写入、嵌套控制流和无关 root 写入不得因同名文本进入投影。共享 declaration 的写入按原始源码位置合并,继续参与 canonical 重命名、循环去重和既有 `2 MiB / 32 MiB` 门禁;全部依赖声明必须先于延后的初始化写入输出,不能因 projection traversal 产生 TDZ。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 119590536..baf7e8d23 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -832,6 +832,7 @@ game-project/ - 图集本地提交以主图 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;同一 export 的多个本地 alias 必须按大小写敏感的 symbol identity 保序保留,同一 dependency 的多条 import declaration 合并绑定,同一 origin 即使经不同 dependency 或 bridge 到达也只投影一次。组合 importer 与 origin 前,全部投影根绑定都必须分配跨 importer、同源私有绑定和其它 origin 无冲突的确定性名称,重命名后重新通过 parser 与 semantic;只有最终交给静态启发式扫描的文本允许统一小写。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,不能交付完成。 - ESM 投影中的顶层 function/class/variable 声明必须直接使用 Oxc statement span 提取,不能用首个分号或换行截断箭头函数、多行 initializer 或多 declarator;对象、数组、默认值与 rest 解构声明必须递归收集全部 binding,并保证同一声明只投影一次。import symbol 与 importer 自有 root binding 分开保存,即使名称仅大小写不同也不得在组合前折叠;投影根分配 canonical 名时必须避让 importer 与 origin 的全部非 import semantic binding,不能被嵌套局部捕获。模块组合必须按依赖深度迭代到稳定闭包,把被导出函数继续依赖的 imported origin 带入最终 consumer 单元;循环 ESM 以原始声明 identity 去重回流,不能不断生成重命名副本,并以最终 span replacement 后的单 unit `2 MiB`、累计投影处理 `32 MiB` 为失败关闭上限。固定字符串 dynamic import 同样由下游实际使用的 export 反向驱动加载;未使用 export、未调用嵌套函数和恒假分支中的 dynamic source 不得进入模块单元,同名 dynamic export 不能反向选择未引用的本地声明。只有被选声明中的 `await import` 解构、namespace member 或 `.then(...)` 静态 binding 才能进入组合投影;callback 参数、解构 alias 和 namespace member 必须按 semantic symbol span 改接到投影根,dynamic object shorthand 改名时还必须显式保留原属性键,不能靠追加同名文本跨过局部遮蔽。span replacement 完成后还要对完整组合 unit 重跑 parser 与 semantic,启发式扫描只能在原始源码完成 AST 解析和掩码后再统一小写。匿名 default function / arrow 必须在原始 AST 中以 collision-safe synthetic binding 注册可外调 root span,使 wrapper 内 imported member 的传递依赖继续传播;synthetic binding 必须避开用户真实根名,改名只能更新 default target,不能污染同名命名导出。namespace 经过 renamed re-export 时,同时保留 importer 使用的 member 名和最终 origin export 名:前者定位 importer member span,后者选择 origin declaration,不能混用。named import、namespace import 和 namespace 解构 alias 的成员调用必须保留完整静态成员路径,并按调用 span 排除恒假分支后再把 demand 传播到上游;对象 / class 直接成员、对象解构 alias、实例 alias 与下游 wrapper 都使用同一条可达性链。对象 method shorthand、函数表达式值、箭头函数值以及 class function-valued field 必须按精确函数 span 注册成员根,只加载被实际调用成员中的 dynamic dependency,不能因声明写法遗漏,也不能把属性内未调用的嵌套函数升级为根。 +- 被选 export root 的直接顶层 assignment 必须与声明一起进入 projection,并沿 RHS semantic dependency 继续闭包,覆盖声明后 live binding 初始化、导出对象成员安装与 class prototype 安装。assignment target 必须解析到相同 root symbol;函数体内、嵌套控制流或其它 root 的写入不随文本同名混入。全部依赖声明先完成投影,再按原始源码位置输出延后的初始化写入,避免 projection traversal 引入 TDZ;组合结果继续受 canonical 重命名、循环 identity 去重及既有体积门禁约束。 - dynamic import demand 必须以 source 与 import occurrence position 联合作为身份,同 source 的可达裸 import 不得继承不可达 occurrence 的 export。动态 namespace 在首段 export 之后继续保留完整 member path,声明 initializer 与后续赋值式 `await import` 都必须绑定 semantic symbol。named / namespace 成员作为回调参数、对象解构 alias、实例 alias 与下游 wrapper 时继续按真实调用 span 传播 demand。constructor、`new Game().method()` 和实例 alias 分别形成精确成员根,`this.method()` 只匹配相同 class / object owner;顶层声明位置只使用 Oxc span,禁止以源码文本 `find` 反查。受控 callback API 名严格大小写敏感,调用图只读取真实 callback 参数位:timer、microtask、RAF、Promise 与数组迭代 API 取第一个参数,`addEventListener` 取第二个参数,delay、initial value、event type、options 和其它额外参数都不得建立执行边。恒假分支区间由单次函数可达性 analysis 预计算、排序合并并借用二分索引,不得使用 thread-local 完整源码 key 或在每个位置查询时复制区间、重扫整份源码。 - imported member 只有作为受控 callback API 的参数时才建立执行 demand;日志、注册元数据等普通传参不能被当作调用。普通 member alias 必须同时支持声明 initializer 与后续赋值,`new ns.Game()` 的完整 constructor path 继续传播到实例 alias。局部对象、class、实例、`this` 和 `super` 的方法调用统一按 semantic receiver owner 匹配,禁止按末级方法名跨 owner 选中同名 decoy。 - member alias、局部 receiver 与动态 namespace 的 assignment 必须保留赋值位置和 enclosing function scope;函数体内的使用必须按真实 invocation position 选择当时已生效的事件,多次调用跨过赋值边界时合并全部可能 owner,恒假分支、未调用函数或调用之后的赋值不能倒灌覆盖更早使用点。`receiver = source` 必须冻结赋值完成时的 source owner,source 后续重赋值不能反向改写 receiver;identifier callee 与 `new C(args)` 的 constructor / instance owner 在参数求值前冻结,真实 invocation effect 仍在参数求值后生效,callable assignment 只在 RHS 完成后替换旧值,因此 `start = start()` 必须调用旧 callable。`return / throw` 的表达式先完整执行,随后才截断后续事件;函数体和截断位置统一使用 Oxc AST body / statement span,不能被默认参数或解构参数中的 `{}` 误导。未知 guard clause 后的语句与 `catch` 体赋值都保留执行和跳过状态;调用事件继续按 JavaScript 的 callee、参数 / RHS、内层调用、外层调用 / 赋值顺序生效,`outer(inner())` 与 `receiver = mutate()` 不得按 AST 先序颠倒副作用。未知条件、循环以及 `switch case/default` 内赋值保留执行与跳过状态,conditional expression 保留全部可解析 owner,无法解析的确定重赋值显式 invalidation;已调用函数对外层 alias 的最终副作用按调用位置回写,但普通函数体内无条件 `return / throw` 之后的事件必须截断。被导出的 function、class/object member 还必须把模块初始化完成视为潜在外部调用时点,使声明之后生效的顶层赋值进入 demand,同时保留此前本地调用的旧状态。动态依赖传播继续以原始 owner 模块 AST span 为权威,不得因投影重排声明或省略独立赋值语句而重算出相反 alias。class owner 必须继续区分 static / instance,class expression 与实例化 alias 进入相同 receiver 图,`super` 的 parent owner 在 class 定义位置冻结。constructor、`.call()` 与 `.apply()` 都是实际 invocation;具名 function expression 只保留外层 binding 对应的单一 range,未被受控 API 执行的 inline function / arrow 保持不可达。恒真 / 恒假关键字只按 JavaScript 大小写识别,局部或参数遮蔽的 `undefined` 不得当作恒假;恒假分支识别必须先用 parser trivia 屏蔽注释和非执行 literal。callback API 只接受 semantic 未解析的已知全局调度函数或 AST 可证明的 literal array / dynamic import 调用;用户定义、导入或遮蔽的同名 `setTimeout / map / then` 不能触发参数执行推断。