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 fb12e8785..b077d53be 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 @@ -6048,8 +6048,12 @@ impl<'a> VisitJavascript<'a> for JavascriptImportMemberCallCollector<'_> { fn javascript_string_import_source( expression: &JavascriptExpression<'_>, ) -> Option<(String, usize)> { - let JavascriptExpression::ImportExpression(import) = expression else { - return None; + let import = match expression { + JavascriptExpression::ImportExpression(import) => import, + JavascriptExpression::ParenthesizedExpression(parenthesized) => { + return javascript_string_import_source(&parenthesized.expression); + } + _ => return None, }; let JavascriptExpression::StringLiteral(source) = &import.source else { return None; @@ -6057,6 +6061,20 @@ fn javascript_string_import_source( Some((source.value.to_string(), import.span.start as usize)) } +fn javascript_awaited_import_source( + expression: &JavascriptExpression<'_>, +) -> Option<(String, usize)> { + match expression { + JavascriptExpression::AwaitExpression(awaited) => { + javascript_string_import_source(&awaited.argument) + } + JavascriptExpression::ParenthesizedExpression(parenthesized) => { + javascript_awaited_import_source(&parenthesized.expression) + } + _ => None, + } +} + impl JavascriptDynamicImportUsageCollector<'_> { fn record_namespace_source( &mut self, @@ -6130,6 +6148,12 @@ impl JavascriptDynamicImportUsageCollector<'_> { .collect(); } expression => { + if let Some((source, import_position)) = + javascript_awaited_import_source(expression) + { + path.reverse(); + return vec![(source, import_position, path)]; + } let Some(next) = expression.as_member_expression() else { return Vec::new(); }; @@ -6998,6 +7022,206 @@ fn javascript_projected_dynamic_import_position_is_reachable( const JAVASCRIPT_STATIC_MEMBER_DEMAND: &str = "\0agc-static-member"; const JAVASCRIPT_INSTANCE_MEMBER_DEMAND: &str = "\0agc-instance-member"; +fn javascript_expression_root_symbol_and_member_path( + expression: &JavascriptExpression<'_>, + scoping: &JavascriptScoping, +) -> Option<(JavascriptSymbolId, Vec)> { + match expression { + JavascriptExpression::Identifier(identifier) => identifier + .reference_id + .get() + .and_then(|reference_id| scoping.get_reference(reference_id).symbol_id()) + .map(|symbol_id| (symbol_id, Vec::new())), + JavascriptExpression::ParenthesizedExpression(parenthesized) => { + javascript_expression_root_symbol_and_member_path(&parenthesized.expression, scoping) + } + _ => { + let member = expression.as_member_expression()?; + let property = member.static_property_name()?.to_string(); + let (symbol_id, mut path) = + javascript_expression_root_symbol_and_member_path(member.object(), scoping)?; + path.push(property); + Some((symbol_id, path)) + } + } +} + +fn javascript_assignment_target_root_symbol_and_member_path( + target: &oxc_ast::ast::AssignmentTarget<'_>, + scoping: &JavascriptScoping, +) -> Option<(JavascriptSymbolId, Vec)> { + match target { + oxc_ast::ast::AssignmentTarget::AssignmentTargetIdentifier(identifier) => identifier + .reference_id + .get() + .and_then(|reference_id| scoping.get_reference(reference_id).symbol_id()) + .map(|symbol_id| (symbol_id, Vec::new())), + _ => { + let member = target.as_member_expression()?; + let property = member.static_property_name()?.to_string(); + let (symbol_id, mut path) = + javascript_expression_root_symbol_and_member_path(member.object(), scoping)?; + path.push(property); + Some((symbol_id, path)) + } + } +} + +fn javascript_callable_expression_function_indices( + expression: &JavascriptExpression<'_>, + ranges: &[NamedJavascriptFunctionRange], + scoping: &JavascriptScoping, +) -> BTreeSet { + match expression { + JavascriptExpression::FunctionExpression(_) + | JavascriptExpression::ArrowFunctionExpression(_) => { + let span = expression.span(); + ranges + .iter() + .enumerate() + .filter(|(_, range)| { + range.start == span.start as usize && range.end == span.end as usize + }) + .map(|(index, _)| index) + .collect() + } + JavascriptExpression::Identifier(identifier) => identifier + .reference_id + .get() + .and_then(|reference_id| scoping.get_reference(reference_id).symbol_id()) + .map(|symbol_id| scoping.symbol_span(symbol_id).start as usize) + .into_iter() + .flat_map(|binding_start| { + ranges + .iter() + .enumerate() + .filter(move |(_, range)| range.binding_start == Some(binding_start)) + .map(|(index, _)| index) + }) + .collect(), + JavascriptExpression::ParenthesizedExpression(parenthesized) => { + javascript_callable_expression_function_indices( + &parenthesized.expression, + ranges, + scoping, + ) + } + JavascriptExpression::ConditionalExpression(conditional) => { + let mut indices = javascript_callable_expression_function_indices( + &conditional.consequent, + ranges, + scoping, + ); + indices.extend(javascript_callable_expression_function_indices( + &conditional.alternate, + ranges, + scoping, + )); + indices + } + _ => BTreeSet::new(), + } +} + +fn javascript_projected_assignment_root_function_indices( + content: &str, + ranges: &[NamedJavascriptFunctionRange], + root_binding: &str, + required_static: Option, + member_path: &[String], +) -> BTreeSet { + let allocator = JavascriptAllocator::default(); + let parsed = JavascriptParser::new(&allocator, content, javascript_source_type(true)).parse(); + if parsed.panicked || !parsed.diagnostics.is_empty() { + return BTreeSet::new(); + } + let semantic = JavascriptSemanticBuilder::new_compiler().build(&parsed.program); + if !semantic.diagnostics.is_empty() { + return BTreeSet::new(); + } + let scoping = semantic.semantic.scoping(); + let Some(root_symbol) = scoping + .get_bindings(scoping.root_scope_id()) + .get(root_binding) + .copied() + else { + return BTreeSet::new(); + }; + let method_ranges = javascript_member_method_ranges(content); + let mut roots = BTreeSet::new(); + for statement in &parsed.program.body { + let JavascriptStatement::ExpressionStatement(statement) = statement else { + continue; + }; + let JavascriptExpression::AssignmentExpression(assignment) = &statement.expression else { + continue; + }; + if !assignment.operator.is_assign() { + continue; + } + let Some((target_symbol, mut target_path)) = + javascript_assignment_target_root_symbol_and_member_path(&assignment.left, scoping) + else { + continue; + }; + if target_symbol != root_symbol { + continue; + } + match required_static { + Some(false) => { + if target_path.first().map(String::as_str) != Some("prototype") { + continue; + } + target_path.remove(0); + } + Some(true) if target_path.first().map(String::as_str) == Some("prototype") => { + continue; + } + _ => {} + } + if target_path == member_path { + roots.extend(javascript_callable_expression_function_indices( + &assignment.right, + ranges, + scoping, + )); + continue; + } + let Some(suffix) = member_path.strip_prefix(target_path.as_slice()) else { + continue; + }; + if suffix.is_empty() { + continue; + } + let assignment_span = assignment.right.span(); + roots.extend( + method_ranges + .get(suffix) + .into_iter() + .flat_map(|methods| methods.iter()) + .filter(|method| { + assignment_span.start as usize <= method.range.start + && method.range.end <= assignment_span.end as usize + && required_static.is_none_or(|required| { + method + .is_static + .is_none_or(|is_static| is_static == required) + }) + }) + .flat_map(|method| { + ranges + .iter() + .enumerate() + .filter(move |(_, range)| { + method.range.start == range.start && method.range.end == range.end + }) + .map(|(index, _)| index) + }), + ); + } + roots +} + fn javascript_projected_root_function_indices( content: &str, ranges: &[NamedJavascriptFunctionRange], @@ -7016,7 +7240,7 @@ fn javascript_projected_root_function_indices( _ => (None, member_path), }; let methods = javascript_member_method_ranges(content); - return methods + let mut roots = methods .get(actual_member_path) .into_iter() .flat_map(|method_ranges| method_ranges.iter()) @@ -7038,7 +7262,15 @@ fn javascript_projected_root_function_indices( }) .map(|(index, _)| index) }) - .collect(); + .collect::>(); + roots.extend(javascript_projected_assignment_root_function_indices( + content, + ranges, + root_binding, + required_static, + actual_member_path, + )); + return roots; } let top_level = ranges .iter() @@ -7058,11 +7290,19 @@ fn javascript_projected_root_function_indices( }) .map(|(index, _)| index) .collect::>(); - top_level + let mut roots = top_level .iter() .copied() .filter(|index| ranges[*index].name == root_binding) - .collect() + .collect::>(); + roots.extend(javascript_projected_assignment_root_function_indices( + content, + ranges, + root_binding, + None, + &[], + )); + roots } #[derive(Default)] @@ -10968,6 +11208,136 @@ mod javascript_projection_reachability_tests { ); } + #[test] + fn projected_export_assignment_callables_are_reachability_roots() { + for (content, root, member_path, source) in [ + ( + "export let start; start = () => import('./assigned-root.mjs');", + "start", + Vec::new(), + "./assigned-root.mjs", + ), + ( + "export const api = {}; api.run = function() { return import('./assigned-member.mjs'); };", + "api", + vec![ + JAVASCRIPT_STATIC_MEMBER_DEMAND.to_string(), + "run".to_string(), + ], + "./assigned-member.mjs", + ), + ( + "export class Game {} Game.prototype.run = () => import('./assigned-prototype.mjs');", + "Game", + vec![ + JAVASCRIPT_INSTANCE_MEMBER_DEMAND.to_string(), + "run".to_string(), + ], + "./assigned-prototype.mjs", + ), + ( + "function implementation() { return import('./assigned-function-alias.mjs'); } export let start; start = implementation;", + "start", + Vec::new(), + "./assigned-function-alias.mjs", + ), + ( + "const implementation = () => import('./assigned-member-alias.mjs'); export const api = {}; api.run = implementation;", + "api", + vec![ + JAVASCRIPT_STATIC_MEMBER_DEMAND.to_string(), + "run".to_string(), + ], + "./assigned-member-alias.mjs", + ), + ] { + let ranges = named_javascript_function_ranges(content); + let roots = javascript_projected_root_function_indices( + content, + &ranges, + root, + &member_path, + ); + let position = content.find(source).expect("find assigned dynamic import"); + assert!( + javascript_projected_dynamic_import_position_is_reachable( + content, &ranges, &roots, position, + ), + "a projected exported assignment must execute its selected callable: {content}", + ); + } + + let class_members = "export class Game {} Game.run = () => import('./assigned-static.mjs'); Game.prototype.run = () => import('./assigned-instance.mjs');"; + let ranges = named_javascript_function_ranges(class_members); + let static_roots = javascript_projected_root_function_indices( + class_members, + &ranges, + "Game", + &[ + JAVASCRIPT_STATIC_MEMBER_DEMAND.to_string(), + "run".to_string(), + ], + ); + let instance_roots = javascript_projected_root_function_indices( + class_members, + &ranges, + "Game", + &[ + JAVASCRIPT_INSTANCE_MEMBER_DEMAND.to_string(), + "run".to_string(), + ], + ); + let static_import = class_members + .find("./assigned-static.mjs") + .expect("find static assignment import"); + let instance_import = class_members + .find("./assigned-instance.mjs") + .expect("find instance assignment import"); + assert!(javascript_projected_dynamic_import_position_is_reachable( + class_members, + &ranges, + &static_roots, + static_import, + )); + assert!(!javascript_projected_dynamic_import_position_is_reachable( + class_members, + &ranges, + &static_roots, + instance_import, + )); + assert!(javascript_projected_dynamic_import_position_is_reachable( + class_members, + &ranges, + &instance_roots, + instance_import, + )); + assert!(!javascript_projected_dynamic_import_position_is_reachable( + class_members, + &ranges, + &instance_roots, + static_import, + )); + } + + #[test] + fn awaited_dynamic_namespace_member_records_its_export_demand() { + let content = + "export async function start() { return (await import('./dependency.mjs')).run(); }"; + let analysis = javascript_module_analysis(content, true).expect("analyze awaited import"); + let demand = analysis + .dynamic_import_demands + .iter() + .find(|((source, _), _)| source == "./dependency.mjs") + .map(|(_, demands)| demands); + + assert!( + demand + .is_some_and(|demands| { demands.contains_key(&("run".to_string(), Vec::new())) }), + "a direct awaited namespace call must demand dependency::run: {:#?}", + analysis.dynamic_import_demands, + ); + } + #[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/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 4df446c52..a2b9f790d 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 @@ -3722,6 +3722,66 @@ fn javascript_dynamic_projection_keeps_occurrence_member_and_constructor_identit .any(|unit| unit.contains("function finishLocalAlias()"))); } +#[test] +fn javascript_dynamic_projection_reaches_export_assignments_and_direct_await_members() { + let html = ""; + for (label, origin, importer, dependency, final_source, final_binding) in [ + ( + "exported live binding", + "export let start; start = () => import('./dependency.mjs').then(({ run }) => run());", + "import { start } from './origin.mjs'; start();", + "export function run() { return import('./final.mjs').then(({ finish }) => finish()); } export function poison() { return import('./missing-poison.mjs'); }", + "export function finish() { return 'live-binding'; }", + "finish", + ), + ( + "exported object member installation", + "export const api = {}; api.start = function() { return import('./dependency.mjs').then(({ runMember }) => runMember()); };", + "import { api } from './origin.mjs'; api.start();", + "export function runMember() { return import('./final.mjs').then(({ finishMember }) => finishMember()); } export function poison() { return import('./missing-poison.mjs'); }", + "export function finishMember() { return 'member'; }", + "finishMember", + ), + ( + "exported prototype member installation", + "export class Game {} Game.prototype.start = () => import('./dependency.mjs').then(({ runPrototype }) => runPrototype());", + "import { Game } from './origin.mjs'; new Game().start();", + "export function runPrototype() { return import('./final.mjs').then(({ finishPrototype }) => finishPrototype()); } export function poison() { return import('./missing-poison.mjs'); }", + "export function finishPrototype() { return 'prototype'; }", + "finishPrototype", + ), + ( + "direct awaited namespace member", + "export async function start() { if (false) (await import('./missing-false.mjs')).run(); return (await import('./dependency.mjs')).run(); }", + "import { start } from './origin.mjs'; start();", + "export function run() { return import('./final.mjs').then(({ finishAwaited }) => finishAwaited()); } export function poison() { return import('./missing-poison.mjs'); }", + "export function finishAwaited() { return 'awaited'; }", + "finishAwaited", + ), + ] { + let temporary = tempfile::tempdir().expect("create dynamic live binding project"); + let root = temporary.path(); + fs::create_dir_all(root.join("game")).expect("create game directory"); + fs::write(root.join("game/origin.mjs"), origin).expect("write projected origin"); + fs::write(root.join("game/main.mjs"), importer).expect("write projected importer"); + fs::write(root.join("game/dependency.mjs"), dependency) + .expect("write selected dynamic dependency"); + fs::write(root.join("game/final.mjs"), final_source) + .expect("write selected transitive dependency"); + + let modules = read_external_gameplay_javascript_at(root, html) + .unwrap_or_else(|error| panic!("project {label}: {error}")); + assert!( + modules + .module_units() + .iter() + .any(|unit| unit.contains(&format!("function {final_binding}()"))), + "{label} must demand the selected dynamic export and its closure: {:#?}", + modules.module_units(), + ); + } +} + #[test] fn javascript_projection_preserves_nested_names_shorthand_keys_and_cycles() { let temporary = tempfile::tempdir().expect("create projection identity project"); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index a4c4b562d..71e6c105f 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -5994,6 +5994,7 @@ - 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。 - JavaScript callback 时序、内建覆盖与 class expression owner 补充:受控异步 callback 的注册位置只建立可达调用边,闭包读取的外层 alias 状态选取注册所在同步作用域收尾点,不能冻结在注册点;函数体写副作用仍不得同步提交到注册调用末尾。数组字面量上的已知迭代 callback 继续按同步执行传播外层 alias 变化。函数对象自有 `.call / .apply / .bind` assignment 按函数对象身份形成成员 callable 状态,普通函数别名共享同一对象覆盖,`.bind()` 结果保持独立对象身份;存在覆盖时禁止回退到 Function.prototype 语义。`new (class { ... })` 赋给局部变量时冻结 class expression 的 instance owner,使后续实例方法调用保持可达。 +- JavaScript / ESM assignment root 与直接动态 namespace 补充:被选 export 的顶层 live-binding function / arrow assignment、对象成员安装和 class prototype 安装同时成为对应 root / member 的 projected reachability root,class static 与 instance assignment 不得串线;`(await import('./dep.mjs')).run()` 及等价静态 computed member 直接记录 occurrence-scoped `run` export demand,恒假分支、未调用函数与其它既有可达性边界继续生效。 ## 2026-08-04 静态视觉门脚本与 ESM 求值顺序 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index a6914126b..2fceaea3e 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -832,8 +832,8 @@ 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 或在每个位置查询时复制区间、重扫整份源码。 +- 被选 export root 的直接顶层 assignment 必须与声明一起进入 projection,并沿 RHS semantic dependency 继续闭包,覆盖声明后 live binding 初始化、导出对象成员安装与 class prototype 安装。assignment target 必须解析到相同 root symbol;函数体内、嵌套控制流或其它 root 的写入不随文本同名混入。importer 外部调用被选 export 时,assignment RHS 的 function / arrow、对象成员安装与 prototype 成员安装必须成为对应 root / member 的 projected reachability root,且 class static 与 instance 成员严格分离。全部依赖声明先完成投影,再按原始源码位置输出延后的初始化写入,避免 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;`(await import('./dep.mjs')).run()` 及等价静态 computed member 还必须直接把 `run` 记录为该 occurrence 的 export demand,继续服从恒假分支与函数可达性过滤。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` 不能触发参数执行推断。 - `new` 必须覆盖显式 `super()`、隐式 derived constructor 和直接 class expression;class expression 经 `new` 赋给局部变量时必须冻结 instance owner,未 override 的 instance / static 方法沿冻结的 `extends` 链查找。普通嵌套 function 拥有独立 `this`,只有 arrow 词法继承外层 owner。声明后通过 assignment 安装的 function / arrow、本地 function alias 和 `.bind()` 结果都必须保留 callable identity;只有未被函数对象自有成员覆盖的 Function.prototype `.call()` / `.apply()` / `.bind()` 采用内建 callable 语义,自有覆盖和业务对象同名方法都按实际成员 callable 解析。函数对象自有 `.call / .apply / .bind` 覆盖必须按对象身份传播到全部普通别名,`.bind()` 生成的新函数则保持独立对象身份。callable conditional expression 必须合并 consequent / alternate 的全部可调用身份;声明式和后续赋值式 `api.run.bind(api)` 都在绑定时冻结成员 owner 与方法名。受控 callback 识别穿透 callee 与 receiver 外层括号;Promise `.then` 的 fulfilled / rejected 两个参数都是 callback,`.catch` / `.finally` 仍仅取第一个。timer、RAF、microtask、Promise 与事件监听 callback 的注册点只建立异步可达调用边,闭包读取外层 alias 时必须选取当前同步作用域收尾后的状态,不能冻结在注册点;callback 函数体对外层 alias 的写副作用仍不得同步提交到注册调用末尾。数组字面量的已知迭代 API 才按同步 callback 传播函数体副作用。视觉资产启发式允许使用 ASCII 小写副本查找路径、`drawImage` 与图片变量,但函数调用图、semantic binding 和可达性必须始终解析原始大小写 JavaScript,禁止把 `MainLoop` 与 `mainloop` 合并。Canvas 视觉门必须复用同一组受 256 文件、累计源码 `2 MiB` 与投影处理 `32 MiB` 限制的 inline / external module 加载、链接和投影结果,并逐个原始大小写 unit 独立关联资产路径、图片变量与可达 `drawImage`;`