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 b338f36b7..d04a26035 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 @@ -1100,7 +1100,7 @@ impl std::ops::Deref for NamedJavascriptFunctionRanges { #[derive(Default)] struct JavascriptFunctionDefinitionCollector { ranges: Vec, - owners: Vec>, + owners: Vec>>, static_context: Vec, } @@ -1118,13 +1118,32 @@ impl JavascriptFunctionDefinitionCollector { end: usize, binding_start: Option, member_static: bool, + ) { + self.push_with_owner( + name, + start, + end, + binding_start, + self.owners.last().cloned().flatten(), + member_static, + ); + } + + fn push_with_owner( + &mut self, + name: &str, + start: usize, + end: usize, + binding_start: Option, + owner: Option>, + member_static: bool, ) { self.ranges.push(NamedJavascriptFunctionRange { name: name.to_string(), start, end, binding_start, - owner: self.owners.last().cloned(), + owner, member_static, externally_callable: false, invocations: Vec::new(), @@ -1135,14 +1154,14 @@ impl JavascriptFunctionDefinitionCollector { impl<'a> VisitJavascript<'a> for JavascriptFunctionDefinitionCollector { fn visit_class(&mut self, class: &JavascriptClass<'a>) { self.owners - .push(class.span.start as usize..class.span.end as usize); + .push(Some(class.span.start as usize..class.span.end as usize)); oxc_ast_visit::walk::walk_class(self, class); self.owners.pop(); } fn visit_object_expression(&mut self, object: &JavascriptObjectExpression<'a>) { self.owners - .push(object.span.start as usize..object.span.end as usize); + .push(Some(object.span.start as usize..object.span.end as usize)); oxc_ast_visit::walk::walk_object_expression(self, object); self.owners.pop(); } @@ -1150,7 +1169,8 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionDefinitionCollector { fn visit_function(&mut self, function: &JavascriptFunction<'a>, flags: JavascriptScopeFlags) { let start = function.span.start as usize; let end = function.span.end as usize; - if !self.already_registered_expression(start, end) { + let already_registered = self.already_registered_expression(start, end); + if !already_registered { let (name, binding_start) = function.id.as_ref().map_or_else( || (format!("\0anonymous-function-{start}-{end}"), None), |identifier| { @@ -1163,15 +1183,24 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionDefinitionCollector { ) }, ); - self.push( + self.push_with_owner( &name, start, end, binding_start, + None, self.static_context.last().copied().unwrap_or(false), ); } + let this_owner = self + .ranges + .iter() + .find(|range| range.start <= start && range.end == end) + .filter(|range| range.binding_start.is_none()) + .and_then(|range| range.owner.clone()); + self.owners.push(this_owner); oxc_ast_visit::walk::walk_function(self, function, flags); + self.owners.pop(); } fn visit_arrow_function_expression( @@ -1200,7 +1229,13 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionDefinitionCollector { JavascriptExpression::FunctionExpression(_) | JavascriptExpression::ArrowFunctionExpression(_) ) { - self.push( + let owner = matches!( + initializer, + JavascriptExpression::ArrowFunctionExpression(_) + ) + .then(|| self.owners.last().cloned().flatten()) + .flatten(); + self.push_with_owner( identifier.name.as_str(), identifier.span.start as usize, initializer.span().end as usize, @@ -1208,6 +1243,7 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionDefinitionCollector { .symbol_id .get() .map(|_| identifier.span.start as usize), + owner, self.static_context.last().copied().unwrap_or(false), ); } @@ -1300,6 +1336,8 @@ struct JavascriptMethodReceiverOwnerCollector<'a> { conditional_ranges: &'a [std::ops::Range], owner_events: BTreeMap>>, + callable_events: + BTreeMap>>, super_aliases: Vec<((usize, usize), JavascriptSymbolId, usize)>, } @@ -1313,6 +1351,15 @@ enum JavascriptReceiverOwnerValue { }, } +#[derive(Clone)] +enum JavascriptCallableValue { + Direct((usize, usize)), + Alias { + source: JavascriptSymbolId, + captured_at: usize, + }, +} + impl JavascriptMethodReceiverOwnerCollector<'_> { fn referenced_symbol( &self, @@ -1398,6 +1445,71 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { } } + fn callable_value( + &self, + expression: &JavascriptExpression<'_>, + position: usize, + ) -> Option { + match expression { + JavascriptExpression::FunctionExpression(_) + | JavascriptExpression::ArrowFunctionExpression(_) => { + let span = expression.span(); + Some(JavascriptCallableValue::Direct(( + span.start as usize, + span.end as usize, + ))) + } + JavascriptExpression::Identifier(_) => { + self.referenced_symbol(expression) + .map(|source| JavascriptCallableValue::Alias { + source, + captured_at: position, + }) + } + JavascriptExpression::ParenthesizedExpression(parenthesized) => { + self.callable_value(&parenthesized.expression, position) + } + JavascriptExpression::CallExpression(call) => call + .callee + .as_member_expression() + .filter(|member| member.static_property_name() == Some("bind")) + .and_then(|member| self.referenced_symbol(member.object())) + .map(|source| JavascriptCallableValue::Alias { + source, + captured_at: position, + }), + _ => None, + } + } + + fn record_callable_assignment( + &mut self, + symbol_id: JavascriptSymbolId, + expression: &JavascriptExpression<'_>, + position: usize, + ) { + if javascript_position_is_in_literal_false_block( + self.content, + self.literal_false_ranges, + position, + ) { + return; + } + let value = self.callable_value(expression, position); + self.callable_events + .entry(symbol_id) + .or_default() + .push(JavascriptAliasEvent { + position, + scope: javascript_alias_scope_at(self.ranges, position), + value, + conditional: javascript_position_is_conditionally_executed( + self.conditional_ranges, + position, + ), + }); + } + fn receiver_values( &self, initializer: &JavascriptExpression<'_>, @@ -1449,10 +1561,11 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { self, ) -> ( BTreeMap>>, + BTreeMap>>, Vec<((usize, usize), JavascriptSymbolId, usize)>, ) { let super_owners = self.super_aliases.into_iter().collect(); - (self.owner_events, super_owners) + (self.owner_events, self.callable_events, super_owners) } } @@ -1496,6 +1609,20 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> { initializer, declarator.span.end as usize, ); + if !matches!( + initializer, + JavascriptExpression::FunctionExpression(_) + | JavascriptExpression::ArrowFunctionExpression(_) + ) && self + .callable_value(initializer, declarator.span.start as usize) + .is_some() + { + self.record_callable_assignment( + symbol_id, + initializer, + declarator.span.start as usize, + ); + } } } oxc_ast_visit::walk::walk_variable_declarator(self, declarator); @@ -1516,6 +1643,11 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> { &assignment.right, assignment.span.end as usize, ); + self.record_callable_assignment( + symbol_id, + &assignment.right, + assignment.span.start as usize, + ); } } } @@ -1535,6 +1667,8 @@ struct JavascriptFunctionInvocationCollector<'a, 'b> { expression_ranges: BTreeMap<(usize, usize), Vec>, receiver_owner_events: BTreeMap>>, + callable_events: + BTreeMap>>, super_aliases: Vec<((usize, usize), JavascriptSymbolId, usize)>, marker: std::marker::PhantomData<&'a ()>, } @@ -1669,19 +1803,113 @@ fn javascript_invocation_effect_position(span: oxc_span::Span) -> usize { } impl JavascriptFunctionInvocationCollector<'_, '_> { - fn record_identifier(&mut self, identifier: &oxc_ast::ast::IdentifierReference<'_>, at: usize) { - let Some(reference_id) = identifier.reference_id.get() else { - return; - }; - let Some(symbol_id) = self.scoping.get_reference(reference_id).symbol_id() else { - return; - }; + fn callable_indices_for_symbol( + &self, + symbol_id: JavascriptSymbolId, + at: usize, + visiting: &mut BTreeSet, + ) -> Vec { + if !visiting.insert(symbol_id) { + return Vec::new(); + } let binding_start = self.scoping.symbol_span(symbol_id).start as usize; - if let Some(indices) = self.binding_ranges.get(&binding_start) { - for index in indices { - self.ranges[*index].invocations.push(at); + let mut indices = self + .binding_ranges + .get(&binding_start) + .cloned() + .unwrap_or_default(); + if let Some(events) = self.callable_events.get(&symbol_id) { + let selection = javascript_alias_event_indices_at( + events, + self.context_ranges, + self.content, + self.literal_false_ranges, + self.conditional_ranges, + at, + &mut BTreeSet::new(), + ); + if selection.resolved { + if !selection.conditional { + indices.clear(); + } + for index in selection.indices { + let Some(value) = events[index].value.as_ref() else { + continue; + }; + match value { + JavascriptCallableValue::Direct(span) => { + indices.extend( + self.expression_ranges + .get(span) + .into_iter() + .flatten() + .copied(), + ); + } + JavascriptCallableValue::Alias { + source, + captured_at, + } => indices.extend(self.callable_indices_for_symbol( + *source, + *captured_at, + visiting, + )), + } + } } } + visiting.remove(&symbol_id); + indices.sort_unstable(); + indices.dedup(); + indices + } + + fn callable_indices_for_expression( + &self, + expression: &JavascriptExpression<'_>, + at: usize, + ) -> Vec { + let mut indices = match expression { + JavascriptExpression::Identifier(identifier) => identifier + .reference_id + .get() + .and_then(|reference_id| self.scoping.get_reference(reference_id).symbol_id()) + .map(|symbol_id| { + self.callable_indices_for_symbol(symbol_id, at, &mut BTreeSet::new()) + }) + .unwrap_or_default(), + JavascriptExpression::ParenthesizedExpression(parenthesized) => { + self.callable_indices_for_expression(&parenthesized.expression, at) + } + JavascriptExpression::FunctionExpression(_) + | JavascriptExpression::ArrowFunctionExpression(_) => { + let span = expression.span(); + self.expression_ranges + .get(&(span.start as usize, span.end as usize)) + .cloned() + .unwrap_or_default() + } + _ => expression + .as_member_expression() + .map(|member| self.method_indices_for_member(member, at)) + .unwrap_or_default(), + }; + indices.sort_unstable(); + indices.dedup(); + indices + } + + fn record_identifier(&mut self, identifier: &oxc_ast::ast::IdentifierReference<'_>, at: usize) { + let Some(symbol_id) = identifier + .reference_id + .get() + .and_then(|reference_id| self.scoping.get_reference(reference_id).symbol_id()) + else { + return; + }; + for index in self.callable_indices_for_symbol(symbol_id, at, &mut BTreeSet::new()) { + self.ranges[index].invocations.push(at); + } } fn current_owner(&self, at: usize) -> Option<(usize, usize, bool)> { @@ -1751,6 +1979,12 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { at: usize, ) -> Vec<(usize, usize, bool)> { match expression { + JavascriptExpression::ClassExpression(class) => { + vec![(class.span.start as usize, class.span.end as usize, true)] + } + JavascriptExpression::ParenthesizedExpression(parenthesized) => { + self.receiver_owner(&parenthesized.expression, at) + } JavascriptExpression::ThisExpression(_) => self.current_owner(at).into_iter().collect(), JavascriptExpression::Super(_) => self .current_owner(at) @@ -1791,6 +2025,52 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { } } + fn parent_owners(&self, owner: (usize, usize, bool)) -> Vec<(usize, usize, bool)> { + let (start, end, is_static) = owner; + let mut parents = self + .super_aliases + .iter() + .filter(|(child, _, _)| *child == (start, end)) + .flat_map(|(_, parent_symbol, definition_position)| { + self.receiver_owners_for_symbol( + *parent_symbol, + *definition_position, + &mut BTreeSet::new(), + ) + .into_iter() + .map(move |(parent_start, parent_end, _)| (parent_start, parent_end, is_static)) + }) + .collect::>(); + parents.sort_unstable(); + parents.dedup(); + parents + } + + fn method_indices_for_owner( + &self, + owner: (usize, usize, bool), + name: &str, + visiting: &mut BTreeSet<(usize, usize, bool)>, + ) -> Vec { + if !visiting.insert(owner) { + return Vec::new(); + } + let mut indices = self + .method_ranges + .get(&(Some(owner), name.to_string())) + .cloned() + .unwrap_or_default(); + if indices.is_empty() { + for parent in self.parent_owners(owner) { + indices.extend(self.method_indices_for_owner(parent, name, visiting)); + } + } + visiting.remove(&owner); + indices.sort_unstable(); + indices.dedup(); + indices + } + fn method_indices_for_member( &self, member: &oxc_ast::ast::MemberExpression<'_>, @@ -1803,11 +2083,21 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { .receiver_owner(member.object(), at) .into_iter() .flat_map(|owner| { - self.method_ranges - .get(&(Some(owner), name.to_string())) - .into_iter() - .flatten() - .copied() + self.method_indices_for_owner(owner, name.as_ref(), &mut BTreeSet::new()) + }) + .collect::>(); + indices.sort_unstable(); + indices.dedup(); + indices + } + + fn super_constructor_indices(&self, at: usize) -> Vec { + let mut indices = self + .current_owner(at) + .into_iter() + .flat_map(|owner| self.parent_owners(owner)) + .flat_map(|parent| { + self.method_indices_for_owner(parent, "constructor", &mut BTreeSet::new()) }) .collect::>(); indices.sort_unstable(); @@ -1860,18 +2150,20 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionInvocationCollector<'a, '_> { if let JavascriptBindingPattern::BindingIdentifier(identifier) = &declarator.id { if let Some(JavascriptExpression::CallExpression(call)) = &declarator.init { if let Some(bind_member) = call.callee.as_member_expression() { - if bind_member.static_property_name() == Some("bind") { - if let Some(target_member) = bind_member.object().as_member_expression() { - let indices = self.method_indices_for_member( - target_member, - declarator.span.start as usize, - ); - if !indices.is_empty() { - self.binding_ranges - .entry(identifier.span.start as usize) - .or_default() - .extend(indices); - } + if bind_member.static_property_name() == Some("bind") + && self + .method_indices_for_member(bind_member, declarator.span.start as usize) + .is_empty() + { + let indices = self.callable_indices_for_expression( + bind_member.object(), + declarator.span.start as usize, + ); + if !indices.is_empty() { + self.binding_ranges + .entry(identifier.span.start as usize) + .or_default() + .extend(indices); } } } @@ -1883,8 +2175,15 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionInvocationCollector<'a, '_> { fn visit_call_expression(&mut self, call: &JavascriptCallExpression<'a>) { let resolve_at = call.span.start as usize; let invoke_at = javascript_invocation_effect_position(call.span); - if let Some(member) = call.callee.as_member_expression() { - if matches!(member.static_property_name(), Some("call" | "apply")) { + if matches!(call.callee, JavascriptExpression::Super(_)) { + for index in self.super_constructor_indices(resolve_at) { + self.ranges[index].invocations.push(invoke_at); + } + } else if let Some(member) = call.callee.as_member_expression() { + let business_method = self.method_indices_for_member(member, resolve_at); + if matches!(member.static_property_name(), Some("call" | "apply")) + && business_method.is_empty() + { self.record_expression(member.object(), resolve_at, invoke_at); } else { self.record_expression(&call.callee, resolve_at, invoke_at); @@ -1911,11 +2210,11 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionInvocationCollector<'a, '_> { .receiver_owner(&expression.callee, resolve_at) .into_iter() .flat_map(|(start, end, _)| { - self.method_ranges - .get(&(Some((start, end, false)), "constructor".to_string())) - .into_iter() - .flatten() - .copied() + self.method_indices_for_owner( + (start, end, false), + "constructor", + &mut BTreeSet::new(), + ) }) .collect::>(); indices.sort_unstable(); @@ -2059,10 +2358,11 @@ fn named_javascript_function_ranges(content: &str) -> NamedJavascriptFunctionRan literal_false_ranges: &literal_false_ranges, conditional_ranges: &conditional_ranges, owner_events: BTreeMap::new(), + callable_events: BTreeMap::new(), super_aliases: Vec::new(), }; receiver_owner_collector.visit_program(&parsed.program); - let (receiver_owner_events, super_aliases) = receiver_owner_collector.finish(); + let (receiver_owner_events, callable_events, super_aliases) = receiver_owner_collector.finish(); let mut context_ranges = definitions.ranges.clone(); for _ in 0..16 { for range in &mut definitions.ranges { @@ -2079,6 +2379,7 @@ fn named_javascript_function_ranges(content: &str) -> NamedJavascriptFunctionRan method_ranges: method_ranges.clone(), expression_ranges: expression_ranges.clone(), receiver_owner_events: receiver_owner_events.clone(), + callable_events: callable_events.clone(), super_aliases: super_aliases.clone(), marker: std::marker::PhantomData, }; @@ -9768,6 +10069,171 @@ mod javascript_projection_reachability_tests { assert!(javascript_position_is_reachable(content, &ranges, position)); } + #[test] + fn derived_and_direct_class_construction_reaches_runtime_constructors() { + let explicit = "class Base { constructor() { return import('./explicit-base.mjs'); } } class Game extends Base { constructor() { super(); return import('./explicit-game.mjs'); } } new Game();"; + let ranges = named_javascript_function_ranges(explicit); + for source in ["./explicit-base.mjs", "./explicit-game.mjs"] { + let position = explicit + .find(source) + .expect("find explicit constructor import"); + assert!( + javascript_position_is_reachable(explicit, &ranges, position), + "new must execute the explicit derived constructor chain: {source}", + ); + } + + let implicit = "class Base { constructor() { return import('./implicit-base.mjs'); } } class Game extends Base {} new Game();"; + let ranges = named_javascript_function_ranges(implicit); + let position = implicit + .find("./implicit-base.mjs") + .expect("find implicit parent constructor import"); + assert!( + javascript_position_is_reachable(implicit, &ranges, position), + "an implicit derived constructor must forward construction to its parent", + ); + + let direct = "new (class { constructor() { return import('./direct-class.mjs'); } })();"; + let ranges = named_javascript_function_ranges(direct); + let position = direct + .find("./direct-class.mjs") + .expect("find direct class expression constructor import"); + assert!( + javascript_position_is_reachable(direct, &ranges, position), + "a directly constructed class expression must execute its constructor", + ); + } + + #[test] + fn only_arrows_lexically_inherit_a_class_this_owner() { + let ordinary = "class Game { start() { function nested() { return this.run(); } nested(); } run() { return import('./ordinary-decoy.mjs'); } } new Game().start();"; + let ranges = named_javascript_function_ranges(ordinary); + let position = ordinary + .find("./ordinary-decoy.mjs") + .expect("find ordinary nested function import"); + assert!( + !javascript_position_is_reachable(ordinary, &ranges, position), + "an ordinary nested function has its own this and must not inherit the class owner", + ); + + let arrow = "class Game { start() { const nested = () => this.run(); nested(); } run() { return import('./arrow-owner.mjs'); } } new Game().start();"; + let ranges = named_javascript_function_ranges(arrow); + let position = arrow + .find("./arrow-owner.mjs") + .expect("find arrow-owned class method import"); + assert!( + javascript_position_is_reachable(arrow, &ranges, position), + "an arrow must retain the enclosing class method's lexical this owner", + ); + } + + #[test] + fn inherited_instance_and_static_methods_follow_extends_chain() { + let content = "class Base { run() { return import('./inherited-instance.mjs'); } static boot() { return import('./inherited-static.mjs'); } } class Game extends Base {} new Game().run(); Game.boot();"; + let ranges = named_javascript_function_ranges(content); + for source in ["./inherited-instance.mjs", "./inherited-static.mjs"] { + let position = content.find(source).expect("find inherited method import"); + assert!( + javascript_position_is_reachable(content, &ranges, position), + "method lookup must continue through the extends chain: {source}", + ); + } + + let overridden = "class Base { run() { return import('./overridden-base.mjs'); } } class Game extends Base { run() { return import('./override.mjs'); } } new Game().run();"; + let ranges = named_javascript_function_ranges(overridden); + let parent = overridden + .find("./overridden-base.mjs") + .expect("find overridden parent import"); + let child = overridden + .find("./override.mjs") + .expect("find child override import"); + assert!(javascript_position_is_reachable(overridden, &ranges, child)); + assert!( + !javascript_position_is_reachable(overridden, &ranges, parent), + "an override must stop lookup before the parent implementation", + ); + } + + #[test] + fn assignment_can_install_a_function_or_arrow_callable() { + for content in [ + "let start; start = function() { return import('./assigned-function.mjs'); }; start();", + "let start; start = () => import('./assigned-arrow.mjs'); start();", + ] { + let ranges = named_javascript_function_ranges(content); + let position = content + .find("import(") + .expect("find assigned callable import"); + assert!( + javascript_position_is_reachable(content, &ranges, position), + "an assignment to an existing local binding must install the callable range: {content}", + ); + } + } + + #[test] + fn local_function_alias_preserves_the_call_edge() { + let content = "function start() { return import('./aliased-function.mjs'); } const alias = start; alias();"; + let ranges = named_javascript_function_ranges(content); + let position = content + .find("./aliased-function.mjs") + .expect("find aliased function import"); + assert!( + javascript_position_is_reachable(content, &ranges, position), + "calling a local alias must invoke its source function", + ); + + let frozen = "function start() { return import('./captured-function.mjs'); } function decoy() { return import('./later-function.mjs'); } const alias = start; start = decoy; alias();"; + let ranges = named_javascript_function_ranges(frozen); + let captured = frozen + .find("./captured-function.mjs") + .expect("find captured alias function"); + let later = frozen + .find("./later-function.mjs") + .expect("find later reassigned function"); + assert!(javascript_position_is_reachable(frozen, &ranges, captured)); + assert!( + !javascript_position_is_reachable(frozen, &ranges, later), + "a local alias must freeze its source callable when the alias is initialized", + ); + } + + #[test] + fn function_prototype_intrinsics_do_not_swallow_business_methods() { + let business = "const api = { call() { return import('./business-call.mjs'); }, apply() { return import('./business-apply.mjs'); }, bind() { return import('./business-bind.mjs'); } }; api.call(); api.apply(); api.bind();"; + let ranges = named_javascript_function_ranges(business); + for source in [ + "./business-call.mjs", + "./business-apply.mjs", + "./business-bind.mjs", + ] { + let position = business.find(source).expect("find business method import"); + assert!( + javascript_position_is_reachable(business, &ranges, position), + "a business method named call/apply/bind must remain an ordinary member call: {source}", + ); + } + + for invocation in [ + "start.call(null);", + "start.apply(null, []);", + "const bound = start.bind(null); bound();", + "let bound; bound = start.bind(null); bound();", + ] { + let intrinsic = format!( + "function start() {{ return import('./function-prototype.mjs'); }} {invocation}" + ); + let ranges = named_javascript_function_ranges(&intrinsic); + let position = intrinsic + .find("./function-prototype.mjs") + .expect("find Function.prototype import"); + assert!( + javascript_position_is_reachable(&intrinsic, &ranges, position), + "real Function.prototype call/apply/bind invocations must remain callable: {invocation}", + ); + } + } + #[test] fn named_function_expression_uses_its_outer_binding_as_one_range() { let content = diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index d9cd8dfc2..53239aeba 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -5984,6 +5984,8 @@ - JavaScript / ESM alias 与 receiver 补充:imported member 作为参数时只允许受控的 callback API 建立执行 demand,普通日志或元数据传参不得推断为调用。普通 member alias 同时支持声明 initializer 与后续赋值,`new ns.Game()` 等完整 constructor path 必须传播到实例 alias。局部对象、class、实例、`this` 与 `super` 的方法调用统一按 semantic receiver owner 匹配,禁止再按末级方法名跨 owner 扩散到同名 decoy。 - JavaScript / ESM 控制流身份补充:member alias、局部 receiver 与动态 namespace 的赋值必须保存 assignment position 和 enclosing function scope;函数体使用按真实 invocation position 选择当时事件,多次调用跨越赋值边界时合并可能 owner,恒假分支、未调用函数或调用之后的赋值不得覆盖更早使用点。导出的 function 与 class/object member 额外以模块初始化结束作为潜在外部调用点,使声明后生效的顶层赋值进入 demand,同时保留更早本地调用状态。动态依赖传播以原始 owner 模块 AST span 为权威,不因投影重排声明或省略独立赋值语句重算 alias。class method owner 进一步区分 static / instance,class expression 与实例化 alias 使用同一 owner 图。callback API 只接受 semantic 未解析的已知全局调度函数,以及可由 AST 证明的 literal array / dynamic import 调用;被用户定义或遮蔽的同名 `setTimeout / map / then` 不得推断执行参数。 - JavaScript / ESM 深层可达性补充:constructor、`.call/.apply` 与受控 inline callback 建立真实 invocation;具名 function expression 不再生成遮蔽外层 binding 的重叠节点,普通 inline function / arrow 未被执行时保持不可达。受控 callback API 名大小写敏感,并以精确参数索引建立执行边:timer、microtask、RAF、Promise 与数组迭代取第一个参数,`addEventListener` 取第二个参数,delay、initial value、event type、options 和额外参数保持普通值。条件/循环赋值合并执行与跳过状态,conditional expression 合并各 owner,未知确定赋值显式 invalidation;已调用函数对外层 alias 的副作用按调用位置传播,`super` owner 固定在 class 定义点。恒假扫描先屏蔽 parser 识别的注释和 literal;恒假分支区间随单次函数可达性 analysis 预计算、排序合并并以借用二分索引查询,不再使用 thread-local 完整源码 key 或命中时 clone ranges。投影 canonical 根名避让两侧全部非 import binding,dynamic shorthand 保留原键,循环模块按相同原始声明去重,同名 dynamic export 不得拉入无引用本地声明。 +- JavaScript / ESM 深层可达性补充:constructor、`.call/.apply` 与受控 inline callback 建立真实 invocation;具名 function expression 不再生成遮蔽外层 binding 的重叠节点,普通 inline function / arrow 未被执行时保持不可达。条件/循环赋值合并执行与跳过状态,conditional expression 合并各 owner,未知确定赋值显式 invalidation;已调用函数对外层 alias 的副作用按调用位置传播,`super` owner 固定在 class 定义点。恒假扫描先屏蔽 parser 识别的注释和 literal。投影 canonical 根名避让两侧全部非 import binding,dynamic shorthand 保留原键,循环模块按相同原始声明去重,同名 dynamic export 不得拉入无引用本地声明。 +- JavaScript / ESM 构造、继承与 callable 补充:`new` 沿冻结的 class owner 图执行本类显式 constructor、显式 `super()` 或隐式 derived constructor,并支持直接 class expression;instance / static 成员未 override 时继续沿 `extends` 链查找。普通嵌套 function 不继承 class `this`,arrow 保持词法 owner。`let binding; binding = function/arrow`、本地 function alias 和 Function.prototype `.bind()` 结果都建立 callable identity;`.call/.apply/.bind` 只有在 receiver 可解析为 callable 时采用 Function.prototype 语义,业务对象同名方法仍作为普通 receiver method 执行。 - 浏览器因果:状态证据仍只冻结 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` 失败误报为源码失败。 - JavaScript / ESM 循环与体积补充:投影声明按 `(origin module, original root binding)` 保存身份,canonical 重命名不能改写原始身份;删除回流声明前先把引用改接到 importer 已有 canonical,有界轮次未收敛时失败关闭。inline module 与外部脚本共同占用累计 `2 MiB` 源码预算。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 8ea18022d..6834fd0e9 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -835,6 +835,7 @@ game-project/ - 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;调用事件按 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` 不能触发参数执行推断。 +- member alias、局部 receiver 与动态 namespace 的 assignment 必须保留赋值位置和 enclosing function scope;函数体内的使用必须按真实 invocation position 选择当时已生效的事件,多次调用跨过赋值边界时合并全部可能 owner,恒假分支、未调用函数或调用之后的赋值不能倒灌覆盖更早使用点。未知条件或循环内赋值保留执行与跳过两种状态,conditional expression 保留全部可解析 owner,无法解析的确定重赋值显式 invalidation;已调用函数对外层 alias 的最终副作用按调用位置回写,不能被 scope 隔离丢失。被导出的 function、class/object member 还必须把模块初始化完成视为潜在外部调用时点,使声明之后生效的顶层赋值进入 demand,同时保留此前本地调用的旧状态。动态依赖传播继续以原始 owner 模块 AST span 为权威,不得因投影重排声明或省略独立赋值语句而重算出相反 alias。class owner 必须继续区分 static / instance,class expression 与实例化 alias 进入相同 receiver 图,`super` 的 parent owner 在 class 定义位置冻结;`new` 必须覆盖显式 `super()`、隐式 derived constructor 和直接 class expression,未 override 的 instance / static 方法沿冻结的 `extends` 链查找。普通嵌套 function 拥有独立 `this`,只有 arrow 词法继承外层 owner。声明后通过 assignment 安装的 function / arrow、以及本地 function alias 和 `.bind()` 结果必须保留 callable identity。constructor、真正 Function.prototype 的 `.call()` / `.apply()` / `.bind()` 都是实际 invocation,但业务对象同名方法仍按 receiver method 解析;具名 function expression 只保留外层 binding 对应的单一 range,未被受控 API 执行的 inline function / arrow 保持不可达。恒假分支识别必须先用 parser trivia 屏蔽注释和非执行 literal。callback API 只接受 semantic 未解析的已知全局调度函数或 AST 可证明的 literal array / dynamic import 调用;用户定义、导入或遮蔽的同名 `setTimeout / map / then` 不能触发参数执行推断。 - 泥点不足是确定性业务中断,不是瞬态 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 重放,禁止分别执行原响应和持久响应。