From f47a30cff2a807b0037d29f8afb86d2c14b5fe7b Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 4 Aug 2026 13:14:33 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=84=9A=E6=9C=AC=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=E6=A7=BD=E4=BD=8D=E4=B8=8E=E6=81=92=E5=81=87=E5=8C=BA?= =?UTF-8?q?=E9=97=B4=E7=B4=A2=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 受控 callback API 按大小写敏感名称和真实参数位传播调用 函数可达性 analysis 预计算并借用合并后的恒假区间索引 补充本地及跨模块回归并同步技术方案和共享决策 --- .../runtime_protocol/autonomous_completion.rs | 417 ++++++++++++++---- .../shared-memory/decision-log.md | 2 +- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 +- 3 files changed, 336 insertions(+), 85 deletions(-) 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 7040cd474..b5a287b8a 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 @@ -963,29 +963,32 @@ fn javascript_statement_range(content: &str, start: usize) -> Option Vec> { - std::thread_local! { - static CACHE: std::cell::RefCell>)>> = - const { std::cell::RefCell::new(std::collections::VecDeque::new()) }; +#[derive(Clone, Debug, Default, Eq, PartialEq)] +struct JavascriptLiteralFalseRangeIndex { + ranges: Vec>, +} + +impl JavascriptLiteralFalseRangeIndex { + fn analyze(content: &str) -> Self { + let mut ranges = javascript_obvious_false_branch_ranges_uncached(content); + ranges.sort_by_key(|range| (range.start, range.end)); + let mut merged = Vec::>::with_capacity(ranges.len()); + for range in ranges { + if let Some(previous) = merged.last_mut() { + if range.start <= previous.end { + previous.end = previous.end.max(range.end); + continue; + } + } + merged.push(range); + } + Self { ranges: merged } } - if let Some(cached) = CACHE.with(|cache| { - cache - .borrow() - .iter() - .find(|(source, _)| source == content) - .map(|(_, ranges)| ranges.clone()) - }) { - return cached; + + fn contains(&self, position: usize) -> bool { + let index = self.ranges.partition_point(|range| range.start <= position); + index > 0 && self.ranges[index - 1].contains(&position) } - let ranges = javascript_obvious_false_branch_ranges_uncached(content); - if content.len() <= 2 * 1024 * 1024 { - CACHE.with(|cache| { - let mut cache = cache.borrow_mut(); - cache.push_front((content.to_string(), ranges.clone())); - cache.truncate(8); - }); - } - ranges } fn javascript_obvious_false_branch_ranges_uncached(content: &str) -> Vec> { @@ -1065,7 +1068,7 @@ fn javascript_obvious_false_branch_ranges_uncached(content: &str) -> Vec String { let mut bytes = content.as_bytes().to_vec(); - for range in javascript_obvious_false_branch_ranges(content) { + for range in JavascriptLiteralFalseRangeIndex::analyze(content).ranges { bytes[range].fill(b' '); } String::from_utf8(bytes).expect("masking JavaScript bytes with spaces preserves UTF-8") @@ -1083,6 +1086,20 @@ struct NamedJavascriptFunctionRange { invocations: Vec, } +#[derive(Clone, Debug, Default, Eq, PartialEq)] +struct NamedJavascriptFunctionRanges { + ranges: Vec, + literal_false_ranges: JavascriptLiteralFalseRangeIndex, +} + +impl std::ops::Deref for NamedJavascriptFunctionRanges { + type Target = [NamedJavascriptFunctionRange]; + + fn deref(&self) -> &Self::Target { + &self.ranges + } +} + #[derive(Default)] struct JavascriptFunctionDefinitionCollector { ranges: Vec, @@ -1282,6 +1299,7 @@ struct JavascriptMethodReceiverOwnerCollector<'a> { scoping: &'a JavascriptScoping, ranges: &'a [NamedJavascriptFunctionRange], content: &'a str, + literal_false_ranges: &'a JavascriptLiteralFalseRangeIndex, conditional_ranges: &'a [std::ops::Range], owner_events: BTreeMap>>, @@ -1353,7 +1371,11 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { } _ => Vec::new(), }; - if javascript_position_is_in_literal_false_block(self.content, position) { + if javascript_position_is_in_literal_false_block( + self.content, + self.literal_false_ranges, + position, + ) { return; } let events = self.owner_events.entry(symbol_id).or_default(); @@ -1502,6 +1524,7 @@ struct JavascriptFunctionInvocationCollector<'a, 'b> { scoping: &'b JavascriptScoping, content: &'b str, context_ranges: &'b [NamedJavascriptFunctionRange], + literal_false_ranges: &'b JavascriptLiteralFalseRangeIndex, conditional_ranges: &'b [std::ops::Range], ranges: &'b mut [NamedJavascriptFunctionRange], binding_ranges: BTreeMap>, @@ -1556,10 +1579,13 @@ fn javascript_expression_is_known_event_target( } } -fn javascript_call_executes_known_callback_arguments( +fn javascript_known_callback_argument_indices( call: &JavascriptCallExpression<'_>, scoping: &JavascriptScoping, -) -> bool { +) -> &'static [usize] { + const NONE: &[usize] = &[]; + const FIRST: &[usize] = &[0]; + const SECOND: &[usize] = &[1]; let unresolved_global = |identifier: &oxc_ast::ast::IdentifierReference<'_>| { identifier .reference_id @@ -1567,37 +1593,47 @@ fn javascript_call_executes_known_callback_arguments( .is_some_and(|reference_id| scoping.get_reference(reference_id).symbol_id().is_none()) }; if let JavascriptExpression::Identifier(identifier) = &call.callee { - return unresolved_global(identifier) - && matches!( - identifier.name.to_ascii_lowercase().as_str(), - "addeventlistener" - | "queuemicrotask" - | "requestanimationframe" - | "setinterval" - | "settimeout" - ); + if !unresolved_global(identifier) { + return NONE; + } + return match identifier.name.as_str() { + "addEventListener" => SECOND, + "queueMicrotask" | "requestAnimationFrame" | "setInterval" | "setTimeout" => FIRST, + _ => NONE, + }; } let Some(member) = call.callee.as_member_expression() else { - return false; + return NONE; }; let Some(name) = member.static_property_name() else { - return false; + return NONE; }; - let name = name.to_ascii_lowercase(); - if matches!(name.as_str(), "catch" | "finally" | "then") { - return matches!(member.object(), JavascriptExpression::ImportExpression(_)); + if matches!(name, "catch" | "finally" | "then") { + return if matches!(member.object(), JavascriptExpression::ImportExpression(_)) { + FIRST + } else { + NONE + }; } if matches!( - name.as_str(), - "every" | "filter" | "find" | "foreach" | "map" | "reduce" | "some" + name, + "every" | "filter" | "find" | "forEach" | "map" | "reduce" | "some" ) { - return matches!(member.object(), JavascriptExpression::ArrayExpression(_)); + return if matches!(member.object(), JavascriptExpression::ArrayExpression(_)) { + FIRST + } else { + NONE + }; } if matches!( - name.as_str(), - "addeventlistener" | "requestanimationframe" | "setinterval" | "settimeout" + name, + "addEventListener" + | "queueMicrotask" + | "requestAnimationFrame" + | "setInterval" + | "setTimeout" ) { - return if name == "addeventlistener" { + let known_receiver = if name == "addEventListener" { javascript_expression_is_known_event_target(member.object(), scoping) } else { matches!( @@ -1610,8 +1646,17 @@ fn javascript_call_executes_known_callback_arguments( ) ) }; + return if known_receiver { + if name == "addEventListener" { + SECOND + } else { + FIRST + } + } else { + NONE + }; } - false + NONE } impl JavascriptFunctionInvocationCollector<'_, '_> { @@ -1662,6 +1707,7 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { events, self.context_ranges, self.content, + self.literal_false_ranges, self.conditional_ranges, at, ) @@ -1789,8 +1835,8 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { } } - fn call_executes_callback_arguments(&self, call: &JavascriptCallExpression<'_>) -> bool { - javascript_call_executes_known_callback_arguments(call, self.scoping) + fn callback_argument_indices(&self, call: &JavascriptCallExpression<'_>) -> &'static [usize] { + javascript_known_callback_argument_indices(call, self.scoping) } } @@ -1830,11 +1876,13 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionInvocationCollector<'a, '_> { } else { self.record_expression(&call.callee, at); } - if self.call_executes_callback_arguments(call) { - for argument in &call.arguments { - if let Some(expression) = argument.as_expression() { - self.record_expression(expression, at); - } + for index in self.callback_argument_indices(call) { + if let Some(expression) = call + .arguments + .get(*index) + .and_then(|argument| argument.as_expression()) + { + self.record_expression(expression, at); } } oxc_ast_visit::walk::walk_call_expression(self, call); @@ -1862,7 +1910,8 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionInvocationCollector<'a, '_> { } } -fn named_javascript_function_ranges(content: &str) -> Vec { +fn named_javascript_function_ranges(content: &str) -> NamedJavascriptFunctionRanges { + let literal_false_ranges = JavascriptLiteralFalseRangeIndex::analyze(content); let allocator = JavascriptAllocator::default(); let parsed = JavascriptParser::new( &allocator, @@ -1871,7 +1920,10 @@ fn named_javascript_function_ranges(content: &str) -> Vec Vec Vec Vec, ) -> bool { @@ -2041,7 +2098,11 @@ fn javascript_named_function_is_reachable( let definition = &ranges[function_index]; for call in &definition.invocations { if (definition.start..definition.end).contains(call) - || javascript_position_is_in_literal_false_block(content, *call) + || javascript_position_is_in_literal_false_block( + content, + &ranges.literal_false_ranges, + *call, + ) { continue; } @@ -2064,10 +2125,14 @@ fn javascript_named_function_is_reachable( fn javascript_position_is_reachable( content: &str, - ranges: &[NamedJavascriptFunctionRange], + ranges: &NamedJavascriptFunctionRanges, position: usize, ) -> bool { - if javascript_position_is_in_literal_false_block(content, position) { + if javascript_position_is_in_literal_false_block( + content, + &ranges.literal_false_ranges, + position, + ) { return false; } let enclosing = ranges @@ -2081,7 +2146,11 @@ fn javascript_position_is_reachable( }) } -fn javascript_position_is_in_literal_false_block(content: &str, position: usize) -> bool { +fn javascript_position_is_in_literal_false_block( + content: &str, + literal_false_ranges: &JavascriptLiteralFalseRangeIndex, + position: usize, +) -> bool { let statement_start = content[..position.min(content.len())] .rfind([';', '{', '}']) .map(|index| index + 1) @@ -2097,9 +2166,7 @@ fn javascript_position_is_in_literal_false_block(content: &str, position: usize) { return true; } - javascript_obvious_false_branch_ranges(content) - .into_iter() - .any(|range| range.contains(&position)) + literal_false_ranges.contains(position) } fn identifier_before(content: &str, position: usize) -> Option { @@ -4606,6 +4673,7 @@ fn javascript_alias_scope_effect( events: &[JavascriptAliasEvent], ranges: &[NamedJavascriptFunctionRange], content: &str, + literal_false_ranges: &JavascriptLiteralFalseRangeIndex, conditional_ranges: &[std::ops::Range], scope: Option<(usize, usize)>, until: usize, @@ -4615,7 +4683,11 @@ fn javascript_alias_scope_effect( for (index, event) in events.iter().enumerate().filter(|(_, event)| { event.scope == scope && event.position <= until - && !javascript_position_is_in_literal_false_block(content, event.position) + && !javascript_position_is_in_literal_false_block( + content, + literal_false_ranges, + event.position, + ) }) { transitions .entry(event.position) @@ -4635,13 +4707,18 @@ fn javascript_alias_scope_effect( *invocation <= until && javascript_alias_scope_at(ranges, *invocation) == scope && !(range.start..range.end).contains(invocation) - && !javascript_position_is_in_literal_false_block(content, *invocation) + && !javascript_position_is_in_literal_false_block( + content, + literal_false_ranges, + *invocation, + ) }) { visiting.insert(callee_scope); let mut effect = javascript_alias_scope_effect( events, ranges, content, + literal_false_ranges, conditional_ranges, Some(callee_scope), range.end, @@ -4674,6 +4751,7 @@ fn javascript_alias_event_indices_at( events: &[JavascriptAliasEvent], ranges: &[NamedJavascriptFunctionRange], content: &str, + literal_false_ranges: &JavascriptLiteralFalseRangeIndex, conditional_ranges: &[std::ops::Range], use_position: usize, visiting: &mut BTreeSet<(usize, usize)>, @@ -4690,7 +4768,11 @@ fn javascript_alias_event_indices_at( { for invocation in &range.invocations { if (range.start..range.end).contains(invocation) - || javascript_position_is_in_literal_false_block(content, *invocation) + || javascript_position_is_in_literal_false_block( + content, + literal_false_ranges, + *invocation, + ) { continue; } @@ -4698,6 +4780,7 @@ fn javascript_alias_event_indices_at( events, ranges, content, + literal_false_ranges, conditional_ranges, *invocation, visiting, @@ -4710,6 +4793,7 @@ fn javascript_alias_event_indices_at( events, ranges, content, + literal_false_ranges, conditional_ranges, content.len(), visiting, @@ -4721,6 +4805,7 @@ fn javascript_alias_event_indices_at( events, ranges, content, + literal_false_ranges, conditional_ranges, range.start.saturating_sub(1), visiting, @@ -4735,6 +4820,7 @@ fn javascript_alias_event_indices_at( events, ranges, content, + literal_false_ranges, conditional_ranges, use_scope, use_position, @@ -4748,6 +4834,7 @@ fn javascript_alias_event_values<'a, T>( events: &'a [JavascriptAliasEvent], ranges: &[NamedJavascriptFunctionRange], content: &str, + literal_false_ranges: &JavascriptLiteralFalseRangeIndex, conditional_ranges: &[std::ops::Range], use_position: usize, ) -> Option> { @@ -4755,6 +4842,7 @@ fn javascript_alias_event_values<'a, T>( events, ranges, content, + literal_false_ranges, conditional_ranges, use_position, &mut BTreeSet::new(), @@ -4772,6 +4860,7 @@ struct JavascriptDynamicImportUsageCollector<'a> { scoping: &'a JavascriptScoping, content: &'a str, ranges: &'a [NamedJavascriptFunctionRange], + literal_false_ranges: &'a JavascriptLiteralFalseRangeIndex, conditional_ranges: &'a [std::ops::Range], namespace_sources: BTreeMap>>, exports: BTreeMap>, @@ -4787,6 +4876,7 @@ struct JavascriptImportMemberCallCollector<'a> { scoping: &'a JavascriptScoping, content: &'a str, ranges: &'a [NamedJavascriptFunctionRange], + literal_false_ranges: &'a JavascriptLiteralFalseRangeIndex, conditional_ranges: &'a [std::ops::Range], imports: BTreeMap, namespace_imports: BTreeSet, @@ -4812,7 +4902,11 @@ impl JavascriptImportMemberCallCollector<'_> { value: Option<(String, Vec)>, position: usize, ) { - if javascript_position_is_in_literal_false_block(self.content, position) { + if javascript_position_is_in_literal_false_block( + self.content, + self.literal_false_ranges, + position, + ) { return; } self.member_aliases @@ -4846,6 +4940,7 @@ impl JavascriptImportMemberCallCollector<'_> { events, self.ranges, self.content, + self.literal_false_ranges, self.conditional_ranges, position, ) { @@ -5000,12 +5095,14 @@ impl<'a> VisitJavascript<'a> for JavascriptImportMemberCallCollector<'_> { } self.record(&local, path, position); } - if javascript_call_executes_known_callback_arguments(call, self.scoping) { - for argument in &call.arguments { - if let Some(expression) = argument.as_expression() { - for (local, path) in self.resolve_expression(expression, position) { - self.record(&local, path, position); - } + for index in javascript_known_callback_argument_indices(call, self.scoping) { + if let Some(expression) = call + .arguments + .get(*index) + .and_then(|argument| argument.as_expression()) + { + for (local, path) in self.resolve_expression(expression, position) { + self.record(&local, path, position); } } } @@ -5075,7 +5172,11 @@ impl JavascriptDynamicImportUsageCollector<'_> { import_position: usize, assignment_position: usize, ) { - if javascript_position_is_in_literal_false_block(self.content, assignment_position) { + if javascript_position_is_in_literal_false_block( + self.content, + self.literal_false_ranges, + assignment_position, + ) { return; } self.namespace_sources @@ -5123,6 +5224,7 @@ impl JavascriptDynamicImportUsageCollector<'_> { events, self.ranges, self.content, + self.literal_false_ranges, self.conditional_ranges, position, ) @@ -5320,6 +5422,7 @@ impl<'a> VisitJavascript<'a> for JavascriptDynamicImportUsageCollector<'_> { events, self.ranges, self.content, + self.literal_false_ranges, self.conditional_ranges, position, ) @@ -5366,6 +5469,7 @@ impl<'a> VisitJavascript<'a> for JavascriptDynamicImportUsageCollector<'_> { events, self.ranges, self.content, + self.literal_false_ranges, self.conditional_ranges, position, ) @@ -5750,6 +5854,7 @@ pub(super) fn javascript_module_analysis( scoping: semantic.semantic.scoping(), content, ranges: &alias_ranges, + literal_false_ranges: &alias_ranges.literal_false_ranges, conditional_ranges: &conditional_ranges, namespace_sources: BTreeMap::new(), exports: BTreeMap::new(), @@ -5852,6 +5957,7 @@ pub(super) fn javascript_module_analysis( scoping: semantic.semantic.scoping(), content, ranges: &alias_ranges, + literal_false_ranges: &alias_ranges.literal_false_ranges, conditional_ranges: &conditional_ranges, imports: collector .import_symbols @@ -5917,7 +6023,7 @@ pub(super) fn javascript_module_analysis( fn javascript_dynamic_import_position_is_reachable( content: &str, - ranges: &[NamedJavascriptFunctionRange], + ranges: &NamedJavascriptFunctionRanges, position: usize, ) -> bool { javascript_position_is_reachable(content, ranges, position) @@ -5925,7 +6031,7 @@ fn javascript_dynamic_import_position_is_reachable( fn javascript_named_function_is_reachable_from_roots( content: &str, - ranges: &[NamedJavascriptFunctionRange], + ranges: &NamedJavascriptFunctionRanges, function_index: usize, roots: &BTreeSet, visiting: &mut BTreeSet, @@ -5939,7 +6045,11 @@ fn javascript_named_function_is_reachable_from_roots( let definition = &ranges[function_index]; for call in &definition.invocations { if (definition.start..definition.end).contains(call) - || javascript_position_is_in_literal_false_block(content, *call) + || javascript_position_is_in_literal_false_block( + content, + &ranges.literal_false_ranges, + *call, + ) { continue; } @@ -5964,11 +6074,15 @@ fn javascript_named_function_is_reachable_from_roots( fn javascript_projected_dynamic_import_position_is_reachable( content: &str, - ranges: &[NamedJavascriptFunctionRange], + ranges: &NamedJavascriptFunctionRanges, roots: &BTreeSet, position: usize, ) -> bool { - if javascript_position_is_in_literal_false_block(content, position) { + if javascript_position_is_in_literal_false_block( + content, + &ranges.literal_false_ranges, + position, + ) { return false; } let enclosing = ranges @@ -7637,7 +7751,7 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at( fn reachable_named_javascript_functions<'a>( content: &'a str, - ranges: &'a [NamedJavascriptFunctionRange], + ranges: &'a NamedJavascriptFunctionRanges, name_markers: &[&str], ) -> Vec<(&'a str, &'a str)> { ranges @@ -7887,11 +8001,16 @@ fn javascript_contains_identifier_call(content: &str, name: &str) -> bool { fn javascript_contains_reachable_identifier_call(content: &str, name: &str) -> bool { let marker = format!("{name}("); + let literal_false_ranges = JavascriptLiteralFalseRangeIndex::analyze(content); content.match_indices(&marker).any(|(position, _)| { (position == 0 || (!is_ascii_word_byte(content.as_bytes()[position - 1]) && content.as_bytes()[position - 1] != b'$')) - && !javascript_position_is_in_literal_false_block(content, position) + && !javascript_position_is_in_literal_false_block( + content, + &literal_false_ranges, + position, + ) }) } @@ -7901,6 +8020,7 @@ fn javascript_contains_reachable_call_with_first_argument( argument: &str, ) -> bool { let marker = format!("{name}({argument}"); + let literal_false_ranges = JavascriptLiteralFalseRangeIndex::analyze(content); content.match_indices(&marker).any(|(position, _)| { (position == 0 || (!is_ascii_word_byte(content.as_bytes()[position - 1]) @@ -7909,7 +8029,11 @@ fn javascript_contains_reachable_call_with_first_argument( .as_bytes() .get(position + marker.len()) .is_none_or(|byte| !is_ascii_word_byte(*byte) && *byte != b'$') - && !javascript_position_is_in_literal_false_block(content, position) + && !javascript_position_is_in_literal_false_block( + content, + &literal_false_ranges, + position, + ) }) } @@ -9651,6 +9775,133 @@ mod javascript_projection_reachability_tests { )); } + fn assert_callback_reachability(content: &str, expected: &[&str], rejected: &[&str]) { + let ranges = named_javascript_function_ranges(content); + for source in expected { + let position = content.find(source).expect("find expected callback import"); + assert!( + javascript_position_is_reachable(content, &ranges, position), + "expected callback must be reachable: {source}", + ); + } + for source in rejected { + let position = content.find(source).expect("find rejected callback import"); + assert!( + !javascript_position_is_reachable(content, &ranges, position), + "non-callback argument must remain unreachable: {source}", + ); + } + } + + #[test] + fn controlled_callback_apis_are_case_sensitive_and_use_only_callback_slots() { + assert_callback_reachability( + "function callback(){return import('./timer-callback.mjs')} function delay(){return import('./timer-delay.mjs')} function extra(){return import('./timer-extra.mjs')} setTimeout(callback, delay, extra);", + &["./timer-callback.mjs"], + &["./timer-delay.mjs", "./timer-extra.mjs"], + ); + assert_callback_reachability( + "function callback(){return import('./wrong-case.mjs')} setTIMEOUT(callback);", + &[], + &["./wrong-case.mjs"], + ); + assert_callback_reachability( + "function eventType(){return import('./event-type.mjs')} function listener(){return import('./listener.mjs')} function options(){return import('./event-options.mjs')} document.addEventListener(eventType, listener, options);", + &["./listener.mjs"], + &["./event-type.mjs", "./event-options.mjs"], + ); + for api in [ + "queueMicrotask", + "requestAnimationFrame", + "setInterval", + "globalThis.setTimeout", + ] { + let content = format!( + "function callback(){{return import('./{api}-callback.mjs')}} function extra(){{return import('./{api}-extra.mjs')}} {api}(callback, extra);" + ); + assert_callback_reachability( + &content, + &[&format!("./{api}-callback.mjs")], + &[&format!("./{api}-extra.mjs")], + ); + } + for api in ["then", "catch", "finally"] { + let content = format!( + "function callback(){{return import('./promise-{api}-callback.mjs')}} function extra(){{return import('./promise-{api}-extra.mjs')}} import('./seed.mjs').{api}(callback, extra);" + ); + assert_callback_reachability( + &content, + &[&format!("./promise-{api}-callback.mjs")], + &[&format!("./promise-{api}-extra.mjs")], + ); + } + for api in [ + "map", "filter", "every", "find", "forEach", "some", "reduce", + ] { + let content = format!( + "function callback(){{return import('./array-{api}-callback.mjs')}} function extra(){{return import('./array-{api}-extra.mjs')}} [1].{api}(callback, extra);" + ); + assert_callback_reachability( + &content, + &[&format!("./array-{api}-callback.mjs")], + &[&format!("./array-{api}-extra.mjs")], + ); + } + } + + #[test] + fn import_member_callback_demands_use_the_same_exact_slots() { + let content = "import * as api from './api.mjs'; setTimeout(api.timer, api.delay); document.addEventListener(api.eventType, api.listener, api.options); import('./seed.mjs').then(api.fulfilled, api.rejected); [1].reduce(api.reducer, api.initialValue); setTIMEOUT(api.wrongCase); [1].foreach(api.wrongMethodCase);"; + let analysis = javascript_module_analysis(content, true).expect("analyze callback imports"); + let members = analysis + .import_member_calls + .get("api") + .expect("record controlled callback member demands"); + for callback in ["timer", "listener", "fulfilled", "reducer"] { + assert!( + members + .keys() + .any(|path| path.first().is_some_and(|member| member == callback)), + "missing callback demand for {callback}: {members:?}", + ); + } + for non_callback in [ + "delay", + "eventType", + "options", + "rejected", + "initialValue", + "wrongCase", + "wrongMethodCase", + ] { + assert!( + !members + .keys() + .any(|path| path.first().is_some_and(|member| member == non_callback)), + "non-callback argument must not become a demand for {non_callback}: {members:?}", + ); + } + } + + #[test] + fn repeated_reachability_queries_borrow_one_false_range_index() { + let content = "if (false) { function hidden() { return import('./hidden.mjs'); } hidden(); } function visible() { return import('./visible.mjs'); } visible();"; + let ranges = named_javascript_function_ranges(content); + assert_eq!(ranges.literal_false_ranges.ranges.len(), 1); + let false_ranges_storage = ranges.literal_false_ranges.ranges.as_ptr(); + for _ in 0..256 { + let hidden = content.find("./hidden.mjs").expect("find hidden import"); + let visible = content.find("./visible.mjs").expect("find visible import"); + assert!(!javascript_position_is_reachable(content, &ranges, hidden)); + assert!(javascript_position_is_reachable(content, &ranges, visible)); + assert_eq!( + ranges.literal_false_ranges.ranges.as_ptr(), + false_ranges_storage, + "queries must borrow the analysis-owned range index", + ); + } + } + #[test] fn alias_control_flow_preserves_runtime_possible_owners() { let conditional = "const real = { run() { return import('./real.mjs'); } }; const decoy = { run() { return import('./decoy.mjs'); } }; let receiver = real; if (flag) receiver = decoy; function start() { return receiver.run(); } start();"; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index df825bb23..593904dfb 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -5983,7 +5983,7 @@ - JavaScript / ESM occurrence 与成员根补充:dynamic import demand 必须以 source 和 import occurrence position 共同隔离,同 source 的可达裸 import 不得借用不可达 occurrence 的 export;动态 namespace 保留首段 export 后的完整成员路径,声明 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` 反查。 - 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 未被执行时保持不可达。条件/循环赋值合并执行与跳过状态,conditional expression 合并各 owner,未知确定赋值显式 invalidation;已调用函数对外层 alias 的副作用按调用位置传播,`super` owner 固定在 class 定义点。恒假扫描先屏蔽 parser 识别的注释和 literal。投影 canonical 根名避让两侧全部非 import binding,dynamic shorthand 保留原键,循环模块按相同原始声明去重,同名 dynamic export 不得拉入无引用本地声明。 +- 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 不得拉入无引用本地声明。 - 浏览器因果:状态证据仍只冻结 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 f708fa5de..9478b9ec8 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -832,7 +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,不能因声明写法遗漏,也不能把属性内未调用的嵌套函数升级为根。 -- 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` 反查。 +- 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,恒假分支、未调用函数或调用之后的赋值不能倒灌覆盖更早使用点。未知条件或循环内赋值保留执行与跳过两种状态,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 定义位置冻结。constructor、`.call()` 与 `.apply()` 都是实际 invocation;具名 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、路径、密钥或任意上游正文。