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 5785733c9..117f3635d 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 @@ -18,6 +18,7 @@ use oxc_ast::ast::{ RegExpLiteral as JavascriptRegExpLiteral, Statement as JavascriptStatement, StaticMemberExpression as JavascriptStaticMemberExpression, StringLiteral as JavascriptStringLiteral, TemplateElement as JavascriptTemplateElement, + UnaryExpression as JavascriptUnaryExpression, UnaryOperator as JavascriptUnaryOperator, VariableDeclarator as JavascriptVariableDeclarator, }; use oxc_ast_visit::Visit as VisitJavascript; @@ -1114,8 +1115,9 @@ struct NamedJavascriptFunctionRange { member_static: bool, externally_callable: bool, invocations: Vec, - // (registration/call site used for reachability, execution point used for outer state reads) - invocation_state_positions: Vec<(usize, usize)>, + // (registration/call site used for reachability, execution point used for outer state reads, + // whether the invocation runs after the current synchronous job) + invocation_state_positions: Vec<(usize, usize, bool)>, synchronous_invocations: Vec, } @@ -1408,6 +1410,8 @@ enum JavascriptCallableValue { captured_at: usize, object_span: (usize, usize), }, + NonCallableMember, + DeletedMember, } #[derive(Clone)] @@ -1415,6 +1419,7 @@ struct JavascriptMemberCallableEvent { // Member receiver evaluation precedes the assignment RHS and must keep that object identity. receiver_captured_at: usize, event: JavascriptAliasEvent, + deleted: bool, } impl JavascriptMethodReceiverOwnerCollector<'_> { @@ -1560,7 +1565,19 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { .collect() }) .collect(), - _ => Vec::new(), + _ => expression + .as_member_expression() + .and_then(|member| { + self.referenced_symbol(member.object()) + .zip(member.static_property_name().map(str::to_string)) + }) + .map(|(receiver, name)| JavascriptCallableValue::Member { + receiver, + name, + captured_at, + }) + .into_iter() + .collect(), } } @@ -1629,9 +1646,12 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { event: JavascriptAliasEvent { position, scope: javascript_alias_scope_at(self.ranges, position), - value: None, + // A present but non-callable own member still disables Function.prototype + // lookup; it is distinct from deleting the own member. + value: Some(JavascriptCallableValue::NonCallableMember), conditional, }, + deleted: false, }); } else { events.extend( @@ -1645,11 +1665,46 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { value: Some(value), conditional, }, + deleted: false, }), ); } } + fn record_member_callable_deletion( + &mut self, + receiver: JavascriptSymbolId, + name: String, + receiver_captured_at: usize, + position: usize, + ) { + if javascript_position_is_in_literal_false_block( + self.content, + self.literal_false_ranges, + position, + ) { + return; + } + self.member_callable_events + .entry((receiver, name)) + .or_default() + .push(JavascriptMemberCallableEvent { + receiver_captured_at, + event: JavascriptAliasEvent { + position, + scope: javascript_alias_scope_at(self.ranges, position), + // Keep deletion as a selected transition so a conditional delete can + // represent both the inherited intrinsic and the retained own property. + value: Some(JavascriptCallableValue::DeletedMember), + conditional: javascript_position_is_conditionally_executed( + self.conditional_ranges, + position, + ), + }, + deleted: true, + }); + } + fn record_callable_assignment( &mut self, symbol_id: JavascriptSymbolId, @@ -1847,6 +1902,25 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> { } oxc_ast_visit::walk::walk_assignment_expression(self, assignment); } + + fn visit_unary_expression(&mut self, expression: &JavascriptUnaryExpression<'a>) { + if expression.operator == JavascriptUnaryOperator::Delete { + if let Some(member) = expression.argument.as_member_expression() { + if let Some((receiver, name)) = self + .referenced_symbol(member.object()) + .zip(member.static_property_name().map(str::to_string)) + { + self.record_member_callable_deletion( + receiver, + name, + expression.span.start as usize, + expression.span.end as usize, + ); + } + } + } + oxc_ast_visit::walk::walk_unary_expression(self, expression); + } } struct JavascriptFunctionInvocationCollector<'a, 'b> { @@ -2036,16 +2110,28 @@ fn javascript_invocation_effect_position(span: oxc_span::Span) -> usize { (span.end as usize).saturating_sub(1) } +fn javascript_asynchronous_state_position( + ranges: &[NamedJavascriptFunctionRange], + content_len: usize, + registration_at: usize, +) -> usize { + ranges + .iter() + .filter(|range| range.start <= registration_at && registration_at < range.end) + .min_by_key(|range| range.end - range.start) + .map_or(content_len, |range| range.end.saturating_sub(1)) +} + impl JavascriptFunctionInvocationCollector<'_, '_> { fn asynchronous_state_position(&self, registration_at: usize) -> usize { // Browser callbacks run after the current synchronous job/scope completes. Keeping this // separate from the registration call site prevents both registration-time closure // freezing and synchronous propagation of callback writes. - self.context_ranges - .iter() - .filter(|range| range.start <= registration_at && registration_at < range.end) - .min_by_key(|range| range.end - range.start) - .map_or(self.content.len(), |range| range.end.saturating_sub(1)) + javascript_asynchronous_state_position( + self.context_ranges, + self.content.len(), + registration_at, + ) } fn push_invocations(&mut self, indices: Vec, at: usize, synchronous: bool) { @@ -2058,7 +2144,7 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { self.ranges[index].invocations.push(at); self.ranges[index] .invocation_state_positions - .push((at, state_at)); + .push((at, state_at, !synchronous)); if synchronous { self.ranges[index].synchronous_invocations.push(at); } @@ -2108,6 +2194,9 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { Vec::new() } } + JavascriptCallableValue::NonCallableMember | JavascriptCallableValue::DeletedMember => { + Vec::new() + } } } @@ -2135,6 +2224,7 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { self.conditional_ranges, at, &mut BTreeSet::new(), + false, ); if selection.resolved { if !selection.conditional { @@ -2425,6 +2515,9 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { JavascriptCallableValue::Bound { object_span, .. } => { [*object_span].into_iter().collect() } + JavascriptCallableValue::NonCallableMember | JavascriptCallableValue::DeletedMember => { + BTreeSet::new() + } } } @@ -2454,6 +2547,7 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { self.conditional_ranges, at, &mut BTreeSet::new(), + false, ); if selection.resolved { if !selection.conditional { @@ -2504,34 +2598,47 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { .iter() .any(|identity| receiver_identities.contains(identity)) }; - matches_receiver.then(|| member_event.event.clone()) + matches_receiver.then(|| member_event.clone()) }) }) .collect::>(); if events.is_empty() { return None; } + let alias_events = events + .iter() + .map(|member_event| member_event.event.clone()) + .collect::>(); let selection = javascript_alias_event_indices_at( - &events, + &alias_events, self.context_ranges, self.content, self.literal_false_ranges, self.conditional_ranges, at, &mut BTreeSet::new(), + false, ); if !selection.resolved { return None; } + let deleted = selection.indices.iter().any(|index| events[*index].deleted); + let has_own_property = selection + .indices + .iter() + .any(|index| !events[*index].deleted); + if !has_own_property { + return None; + } let mut indices = Vec::new(); for index in selection.indices { - if let Some(value) = events[index].value.as_ref() { + if let Some(value) = alias_events[index].value.as_ref() { indices.extend(self.callable_indices_for_value(value, &mut BTreeSet::new())); } } indices.sort_unstable(); indices.dedup(); - Some((indices, selection.conditional)) + Some((indices, selection.conditional || deleted)) } fn super_constructor_indices(&self, at: usize) -> Vec { @@ -2567,6 +2674,28 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { synchronous, ); } + JavascriptExpression::ConditionalExpression(conditional) => { + self.record_expression( + &conditional.consequent, + conditional.consequent.span().start as usize, + invoke_at, + synchronous, + ); + self.record_expression( + &conditional.alternate, + conditional.alternate.span().start as usize, + invoke_at, + synchronous, + ); + } + JavascriptExpression::AssignmentExpression(assignment) => { + self.record_expression( + &assignment.right, + assignment.right.span().start as usize, + invoke_at, + synchronous, + ); + } JavascriptExpression::FunctionExpression(_) | JavascriptExpression::ArrowFunctionExpression(_) => { let span = expression.span(); @@ -2694,6 +2823,7 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionInvocationCollector<'a, '_> { indices.sort_unstable(); indices.dedup(); self.push_invocations(indices, invoke_at, true); + self.record_expression(&expression.callee, resolve_at, invoke_at, true); oxc_ast_visit::walk::walk_new_expression(self, expression); } } @@ -5669,6 +5799,7 @@ fn javascript_alias_event_indices_at( conditional_ranges: &[std::ops::Range], use_position: usize, visiting: &mut BTreeSet<(usize, usize)>, + after_synchronous_job: bool, ) -> JavascriptAliasSelection { let use_scope = javascript_alias_scope_at(ranges, use_position); let mut state = JavascriptAliasSelection::default(); @@ -5680,7 +5811,7 @@ fn javascript_alias_event_indices_at( .iter() .find(|range| range.start == scope.0 && range.end == scope.1) { - for (invocation, state_at) in &range.invocation_state_positions { + for (invocation, state_at, asynchronous) in &range.invocation_state_positions { if (range.start..range.end).contains(invocation) || javascript_position_is_in_literal_false_block( content, @@ -5690,14 +5821,21 @@ fn javascript_alias_event_indices_at( { continue; } + let after_synchronous_job = after_synchronous_job || *asynchronous; + let state_at = if after_synchronous_job && !*asynchronous { + javascript_asynchronous_state_position(ranges, content.len(), *invocation) + } else { + *state_at + }; let outer = javascript_alias_event_indices_at( events, ranges, content, literal_false_ranges, conditional_ranges, - *state_at, + state_at, visiting, + after_synchronous_job, ); state.indices.extend(outer.indices); state.resolved |= outer.resolved; @@ -5711,6 +5849,7 @@ fn javascript_alias_event_indices_at( conditional_ranges, content.len(), visiting, + after_synchronous_job, ); state.indices.extend(outer.indices); state.resolved |= outer.resolved; @@ -5723,6 +5862,7 @@ fn javascript_alias_event_indices_at( conditional_ranges, range.start.saturating_sub(1), visiting, + after_synchronous_job, ); state.indices.extend(outer.indices); state.resolved |= outer.resolved; @@ -5760,6 +5900,7 @@ fn javascript_alias_event_values<'a, T>( conditional_ranges, use_position, &mut BTreeSet::new(), + false, ); selection.resolved.then(|| { selection @@ -11994,6 +12135,45 @@ mod javascript_projection_reachability_tests { } } + #[test] + fn asynchronous_callbacks_observe_completed_closures_and_the_current_job() { + for (label, registration) in [ + ("timer", "setTimeout(callback, 0);"), + ("promise", "import('./seed.mjs').then(callback);"), + ] { + let content = format!( + "function oldRun() {{ return import('./{label}-old-outer.mjs'); }} function nextRun() {{ return import('./{label}-next-outer.mjs'); }} function oldLocal() {{ return import('./{label}-old-local.mjs'); }} function nextLocal() {{ return import('./{label}-next-local.mjs'); }} let run = oldRun; function setup() {{ let local = oldLocal; function callback() {{ run(); local(); }} {registration} local = nextLocal; }} setup(); run = nextRun;" + ); + let ranges = named_javascript_function_ranges(&content); + for source in [ + format!("./{label}-next-outer.mjs"), + format!("./{label}-next-local.mjs"), + ] { + assert!( + javascript_position_is_reachable( + &content, + &ranges, + content.find(&source).expect("find final async callable"), + ), + "an async callback must observe the final callable for {source}", + ); + } + for source in [ + format!("./{label}-old-outer.mjs"), + format!("./{label}-old-local.mjs"), + ] { + assert!( + !javascript_position_is_reachable( + &content, + &ranges, + content.find(&source).expect("find stale async callable"), + ), + "an async callback must not retain the stale callable for {source}", + ); + } + } + } + #[test] fn function_object_overrides_disable_call_apply_and_bind_intrinsics() { for member in ["call", "apply", "bind"] { @@ -12114,6 +12294,138 @@ mod javascript_projection_reachability_tests { ); } + #[test] + fn function_object_overrides_accept_ordinary_object_members() { + for member in ["call", "apply", "bind"] { + let content = format!( + "function target() {{ return import('./member-rhs-intrinsic-{member}.mjs'); }} const overrides = {{ run() {{ return import('./member-rhs-override-{member}.mjs'); }} }}; target.{member} = overrides.run; target.{member}(null);" + ); + let ranges = named_javascript_function_ranges(&content); + let intrinsic = content + .find(&format!("./member-rhs-intrinsic-{member}.mjs")) + .expect("find member RHS intrinsic decoy"); + let replacement = content + .find(&format!("./member-rhs-override-{member}.mjs")) + .expect("find ordinary member override"); + assert!( + !javascript_position_is_reachable(&content, &ranges, intrinsic), + "an ordinary object member override must disable intrinsic {member}", + ); + assert!( + javascript_position_is_reachable(&content, &ranges, replacement), + "an ordinary object member must remain callable as an own {member} override", + ); + } + } + + #[test] + fn compound_callees_preserve_call_edges_and_evaluation_order() { + let conditional = "function oldRun() { return import('./conditional-callee-old.mjs'); } function nextRun() { return import('./conditional-callee-next.mjs'); } let run = oldRun; function swap() { run = nextRun; return flag; } (swap() ? run : run)();"; + let ranges = named_javascript_function_ranges(conditional); + assert!(!javascript_position_is_reachable( + conditional, + &ranges, + conditional.find("./conditional-callee-old.mjs").unwrap(), + )); + assert!(javascript_position_is_reachable( + conditional, + &ranges, + conditional.find("./conditional-callee-next.mjs").unwrap(), + )); + + let assignment = "function oldRun() { return import('./assignment-callee-old.mjs'); } function nextRun() { return import('./assignment-callee-next.mjs'); } let run = oldRun; (run = nextRun)();"; + let ranges = named_javascript_function_ranges(assignment); + assert!(!javascript_position_is_reachable( + assignment, + &ranges, + assignment.find("./assignment-callee-old.mjs").unwrap(), + )); + assert!(javascript_position_is_reachable( + assignment, + &ranges, + assignment.find("./assignment-callee-next.mjs").unwrap(), + )); + } + + #[test] + fn new_invokes_ordinary_function_constructors() { + for content in [ + "function Game() { return import('./function-constructor.mjs'); } new Game();", + "function Game() { return import('./aliased-function-constructor.mjs'); } const Constructor = Game; new Constructor();", + ] { + let ranges = named_javascript_function_ranges(content); + let position = content.find("import(").expect("find function constructor import"); + assert!( + javascript_position_is_reachable(content, &ranges, position), + "new must invoke an ordinary function constructor: {content}", + ); + } + } + + #[test] + fn deleting_function_object_overrides_restores_intrinsics() { + for member in ["call", "apply", "bind"] { + let invocation = if member == "bind" { + "const bound = target.bind(null); bound();".to_string() + } else { + format!("target.{member}(null);") + }; + let content = format!( + "function target() {{ return import('./deleted-intrinsic-{member}.mjs'); }} function overrideMethod() {{ return import('./deleted-override-{member}.mjs'); }} const alias = target; target.{member} = overrideMethod; delete alias.{member}; {invocation}" + ); + let ranges = named_javascript_function_ranges(&content); + assert!( + javascript_position_is_reachable( + &content, + &ranges, + content + .find(&format!("./deleted-intrinsic-{member}.mjs")) + .unwrap(), + ), + "delete must restore Function.prototype.{member}", + ); + assert!( + !javascript_position_is_reachable( + &content, + &ranges, + content + .find(&format!("./deleted-override-{member}.mjs")) + .unwrap(), + ), + "a deleted own {member} override must stop being callable", + ); + } + + let conditional = "function target() { return import('./conditional-delete-intrinsic.mjs'); } function overrideMethod() { return import('./conditional-delete-override.mjs'); } target.call = overrideMethod; if (flag) delete target.call; target.call(null);"; + let ranges = named_javascript_function_ranges(conditional); + for source in [ + "./conditional-delete-intrinsic.mjs", + "./conditional-delete-override.mjs", + ] { + assert!( + javascript_position_is_reachable( + conditional, + &ranges, + conditional.find(source).unwrap(), + ), + "a conditional delete must preserve both possible targets: {source}", + ); + } + + let non_callable = "function target() { return import('./non-callable-own-member-decoy.mjs'); } target.call = 42; target.call(null);"; + let ranges = named_javascript_function_ranges(non_callable); + assert!( + !javascript_position_is_reachable( + non_callable, + &ranges, + non_callable + .find("./non-callable-own-member-decoy.mjs") + .unwrap(), + ), + "a present non-callable own member must not restore Function.prototype.call", + ); + } + #[test] fn class_expression_instance_owner_survives_new_assignment() { let content = "const game = new (class { run() { return import('./class-expression-instance.mjs'); } })(); game.run();"; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index f2bfb7407..fcd810038 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -6002,3 +6002,8 @@ - classic script 分析单元把 inline 与无 `defer / async` 的本地 external 正文按 `game/index.html` 标签顺序交错组成 parser-blocking 段,再把 classic external `defer` 按文档顺序放到解析完成后的 deferred 段;不得把 defer-before-inline 误投影为外链先执行。classic external `async` 的下载完成顺序不可静态证明,当前静态门直接失败关闭。带 `src` 标签的 inline body 继续忽略;外部文件仍执行可信普通文件、`game/` 边界、文件数与累计体积门禁,重复标签按浏览器出现次数保留求值位置。 - Canvas 尺寸、可见性、元素绑定和 stylesheet 选择器扫描只消费浏览器可渲染标记;`template / textarea / noscript / title / style / xmp / iframe / noembed / plaintext` 内的 Canvas、标签和样式诱饵全部跳过。活动顶层 stylesheet 与可见标记分开提取,既允许真实 CSS 参与隐藏/尺寸判断,也不把 CSS raw-text 中的伪标签当作 DOM。 - ESM 组合单元按 dependency 初始化先于 importer 顶层求值排列。import reference 的 span replacement 仍基于原 importer 完成,随后把已闭包的 dependency projection 放在 importer 前并对最终单元重跑 parser、semantic、单元 `2 MiB` 与累计投影 `32 MiB` 门禁;循环模块继续按 `(origin module, original root binding)` canonical identity 去重并要求有界固定点收敛。 + +## 2026-08-04 JavaScript 延迟状态与复合调用边 + +- 受控异步 callback 的 alias 读取按完整 enclosing invocation 链延迟到各层函数同步收尾,最外层再延迟到当前 job 末尾;callback 写入仍不在注册点同步提交。conditional / assignment expression callee 分别在 test / RHS 求值后建立调用边,`new` 同时执行普通 function constructor 及 alias。 +- 函数对象自有 `.call / .apply / .bind` 覆盖允许以普通对象静态 member callable 作为 RHS,并继续按函数对象身份跨普通 alias 共享。`delete` 自有覆盖后恢复 Function.prototype intrinsic;条件删除合并覆盖与 intrinsic,非 callable 自有值仍视为属性存在并禁止 intrinsic 回退。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index c3dd28dad..c9a33bb26 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -838,6 +838,7 @@ game-project/ - 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`;`