From 3212955c0824a20de1de672e865c553ab5264757 Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 4 Aug 2026 15:57:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DESM=E8=A7=A3=E6=9E=84?= =?UTF-8?q?=E6=8A=95=E5=BD=B1=E4=B8=8E=E6=A8=A1=E5=9D=97=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按源码顺序投影副作用依赖声明并按声明身份去重重命名 支持对象和数组解构写入导出绑定及最终可调用根传播 按可达绑定使用建立动态导入解构需求 扩展顶层 Promise await 的静态不完成识别与回归 同步技术方案与共享决策记录 --- .../runtime_protocol/autonomous_completion.rs | 510 ++++++++++++++++-- .../autonomous_completion_contract_tests.rs | 207 +++++++ .../shared-memory/decision-log.md | 7 + ...¹案】AI游戏创作智能体App实施计划-2026-06-24.md | 4 +- 4 files changed, 671 insertions(+), 57 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 92bed62a4..ee48d5fc8 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 @@ -6642,21 +6642,90 @@ fn javascript_awaited_import_source( } } -fn javascript_promise_executor_is_statically_empty(expression: &JavascriptExpression<'_>) -> bool { - match expression { - JavascriptExpression::ArrowFunctionExpression(function) => match &function.body { - oxc_ast::ast::ArrowFunctionBody::FunctionBody(body) => body.statements.is_empty(), - _ => false, - }, - JavascriptExpression::FunctionExpression(function) => function - .body - .as_ref() - .is_some_and(|body| body.statements.is_empty()), - JavascriptExpression::ParenthesizedExpression(parenthesized) => { - javascript_promise_executor_is_statically_empty(&parenthesized.expression) - } - _ => false, +struct JavascriptPromiseExecutorSettlementCollector<'a> { + scoping: &'a JavascriptScoping, + resolver_symbols: BTreeSet, + settles: bool, +} + +impl JavascriptPromiseExecutorSettlementCollector<'_> { + fn expression_is_resolver(&self, expression: &JavascriptExpression<'_>) -> bool { + javascript_expression_root_symbol(expression, self.scoping) + .is_some_and(|symbol_id| self.resolver_symbols.contains(&symbol_id)) } + + fn expression_is_direct_resolver_reference( + &self, + expression: &JavascriptExpression<'_>, + ) -> bool { + match expression { + JavascriptExpression::Identifier(identifier) => identifier + .reference_id + .get() + .and_then(|reference_id| self.scoping.get_reference(reference_id).symbol_id()) + .is_some_and(|symbol_id| self.resolver_symbols.contains(&symbol_id)), + JavascriptExpression::ParenthesizedExpression(parenthesized) => { + self.expression_is_direct_resolver_reference(&parenthesized.expression) + } + _ => false, + } + } +} + +impl<'a> VisitJavascript<'a> for JavascriptPromiseExecutorSettlementCollector<'_> { + fn visit_call_expression(&mut self, call: &JavascriptCallExpression<'a>) { + if self.expression_is_resolver(&call.callee) + || call.arguments.iter().any(|argument| { + argument.as_expression().is_some_and(|expression| { + self.expression_is_direct_resolver_reference(expression) + }) + }) + { + self.settles = true; + } + oxc_ast_visit::walk::walk_call_expression(self, call); + } + + fn visit_throw_statement(&mut self, statement: &oxc_ast::ast::ThrowStatement<'a>) { + self.settles = true; + oxc_ast_visit::walk::walk_throw_statement(self, statement); + } +} + +fn javascript_promise_executor_is_statically_non_settling( + expression: &JavascriptExpression<'_>, + scoping: &JavascriptScoping, +) -> bool { + let resolver_symbols = match expression { + JavascriptExpression::ArrowFunctionExpression(function) => function + .params + .items + .iter() + .take(2) + .filter_map(|parameter| parameter.pattern.get_binding_identifier()?.symbol_id.get()) + .collect::>(), + JavascriptExpression::FunctionExpression(function) => function + .params + .items + .iter() + .take(2) + .filter_map(|parameter| parameter.pattern.get_binding_identifier()?.symbol_id.get()) + .collect::>(), + JavascriptExpression::ParenthesizedExpression(parenthesized) => { + return javascript_promise_executor_is_statically_non_settling( + &parenthesized.expression, + scoping, + ); + } + _ => return false, + }; + let mut collector = JavascriptPromiseExecutorSettlementCollector { + scoping, + resolver_symbols, + settles: false, + }; + collector.visit_expression(expression); + !collector.settles } struct JavascriptNonCompletingTopLevelAwaitCollector<'a> { @@ -6695,12 +6764,17 @@ impl<'a> VisitJavascript<'a> for JavascriptNonCompletingTopLevelAwaitCollector<' .symbol_id() .is_none() }); - let executor_is_empty = new_expression + let executor_is_non_settling = new_expression .arguments .first() .and_then(JavascriptArgument::as_expression) - .is_some_and(javascript_promise_executor_is_statically_empty); - if promise_is_global && executor_is_empty { + .is_some_and(|executor| { + javascript_promise_executor_is_statically_non_settling( + executor, + self.scoping, + ) + }); + if promise_is_global && executor_is_non_settling { self.found = true; } } @@ -6935,7 +7009,6 @@ impl JavascriptDynamicImportUsageCollector<'_> { } } JavascriptBindingPattern::ObjectPattern(pattern) if pattern.rest.is_none() => { - let exports = self.exports.entry(source.to_string()).or_default(); for property in &pattern.properties { let (Some(identifier), Some(name)) = ( property.value.get_binding_identifier(), @@ -6944,13 +7017,6 @@ impl JavascriptDynamicImportUsageCollector<'_> { continue; }; let exported = name.to_string(); - exports.insert(exported.clone()); - self.demands - .entry((source.to_string(), import_position)) - .or_default() - .entry((exported.clone(), Vec::new())) - .or_default() - .push(import_position); if let Some(symbol_id) = identifier.symbol_id.get() { self.bindings.insert( symbol_id, @@ -7113,6 +7179,16 @@ impl<'a> VisitJavascript<'a> for JavascriptDynamicImportUsageCollector<'_> { .and_then(|reference_id| self.scoping.get_reference(reference_id).symbol_id()) .and_then(|symbol_id| self.bindings.get(&symbol_id)) { + self.exports + .entry(source.clone()) + .or_default() + .insert(exported.clone()); + self.demands + .entry((source.clone(), *import_position)) + .or_default() + .entry((exported.clone(), Vec::new())) + .or_default() + .push(identifier.span.start as usize); let references = self .binding_references .entry((source.clone(), *import_position)) @@ -7814,6 +7890,146 @@ fn javascript_callable_expression_function_indices( } } +fn javascript_assignment_target_callable_indices( + target: &oxc_ast::ast::AssignmentTarget<'_>, + value: &JavascriptExpression<'_>, + root_symbol: JavascriptSymbolId, + ranges: &[NamedJavascriptFunctionRange], + scoping: &JavascriptScoping, +) -> BTreeSet { + if javascript_assignment_target_root_symbol(target, scoping) == Some(root_symbol) { + return javascript_callable_expression_function_indices(value, ranges, scoping); + } + let value = match value { + JavascriptExpression::ParenthesizedExpression(parenthesized) => &parenthesized.expression, + value => value, + }; + match (target, value) { + ( + oxc_ast::ast::AssignmentTarget::ArrayAssignmentTarget(target), + JavascriptExpression::ArrayExpression(value), + ) => target + .elements + .iter() + .zip(&value.elements) + .filter_map(|(target, value)| Some((target.as_ref()?, value.as_expression()?))) + .flat_map(|(target, value)| { + javascript_assignment_target_maybe_default_callable_indices( + target, + value, + root_symbol, + ranges, + scoping, + ) + }) + .collect(), + ( + oxc_ast::ast::AssignmentTarget::ObjectAssignmentTarget(target), + JavascriptExpression::ObjectExpression(value), + ) => { + let property_value = |name: &str| { + value.properties.iter().find_map(|property| { + let oxc_ast::ast::ObjectPropertyKind::ObjectProperty(property) = property + else { + return None; + }; + (property.key.static_name().as_deref() == Some(name)).then_some(&property.value) + }) + }; + let mut indices = BTreeSet::new(); + for property in &target.properties { + match property { + oxc_ast::ast::AssignmentTargetProperty::AssignmentTargetPropertyIdentifier( + property, + ) => { + let is_root = + property + .binding + .reference_id + .get() + .is_some_and(|reference_id| { + scoping.get_reference(reference_id).symbol_id() + == Some(root_symbol) + }); + if is_root { + if let Some(value) = property_value(property.binding.name.as_str()) { + indices.extend(javascript_callable_expression_function_indices( + value, ranges, scoping, + )); + } + if let Some(default) = &property.init { + indices.extend(javascript_callable_expression_function_indices( + default, ranges, scoping, + )); + } + } + } + oxc_ast::ast::AssignmentTargetProperty::AssignmentTargetPropertyProperty( + property, + ) => { + let Some(name) = property.name.static_name() else { + continue; + }; + let Some(value) = property_value(&name) else { + continue; + }; + indices.extend( + javascript_assignment_target_maybe_default_callable_indices( + &property.binding, + value, + root_symbol, + ranges, + scoping, + ), + ); + } + } + } + indices + } + _ => BTreeSet::new(), + } +} + +fn javascript_assignment_target_maybe_default_callable_indices( + target: &oxc_ast::ast::AssignmentTargetMaybeDefault<'_>, + value: &JavascriptExpression<'_>, + root_symbol: JavascriptSymbolId, + ranges: &[NamedJavascriptFunctionRange], + scoping: &JavascriptScoping, +) -> BTreeSet { + if let Some(target) = target.as_assignment_target() { + return javascript_assignment_target_callable_indices( + target, + value, + root_symbol, + ranges, + scoping, + ); + } + let oxc_ast::ast::AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(target) = target + else { + return BTreeSet::new(); + }; + let mut indices = javascript_assignment_target_callable_indices( + &target.binding, + value, + root_symbol, + ranges, + scoping, + ); + let mut symbols = BTreeSet::new(); + javascript_assignment_target_root_symbols(&target.binding, scoping, &mut symbols); + if symbols.contains(&root_symbol) { + indices.extend(javascript_callable_expression_function_indices( + &target.init, + ranges, + scoping, + )); + } + indices +} + fn javascript_projected_assignment_root_function_indices( content: &str, ranges: &[NamedJavascriptFunctionRange], @@ -7844,15 +8060,35 @@ fn javascript_projected_assignment_root_function_indices( let JavascriptStatement::ExpressionStatement(statement) = statement else { continue; }; - let JavascriptExpression::AssignmentExpression(assignment) = &statement.expression else { + let Some(assignment) = + javascript_assignment_expression_from_expression(&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 { + let target = + javascript_assignment_target_root_symbol_and_member_path(&assignment.left, scoping); + if target.is_none() && required_static.is_none() && member_path.is_empty() { + let mut target_symbols = BTreeSet::new(); + javascript_assignment_target_root_symbols( + &assignment.left, + scoping, + &mut target_symbols, + ); + if target_symbols.contains(&root_symbol) { + final_roots = Some(javascript_assignment_target_callable_indices( + &assignment.left, + &assignment.right, + root_symbol, + ranges, + scoping, + )); + } + continue; + } + let Some((target_symbol, mut target_path)) = target else { continue; }; if target_symbol != root_symbol { @@ -8336,6 +8572,85 @@ fn javascript_assignment_target_root_symbol( } } +fn javascript_assignment_expression_from_expression<'a>( + expression: &'a JavascriptExpression<'a>, +) -> Option<&'a oxc_ast::ast::AssignmentExpression<'a>> { + match expression { + JavascriptExpression::AssignmentExpression(assignment) => Some(assignment), + JavascriptExpression::ParenthesizedExpression(parenthesized) => { + javascript_assignment_expression_from_expression(&parenthesized.expression) + } + _ => None, + } +} + +fn javascript_assignment_target_root_symbols( + target: &oxc_ast::ast::AssignmentTarget<'_>, + scoping: &JavascriptScoping, + symbols: &mut BTreeSet, +) { + if let Some(symbol_id) = javascript_assignment_target_root_symbol(target, scoping) { + symbols.insert(symbol_id); + return; + } + match target { + oxc_ast::ast::AssignmentTarget::ArrayAssignmentTarget(pattern) => { + for element in pattern.elements.iter().flatten() { + javascript_assignment_target_maybe_default_root_symbols(element, scoping, symbols); + } + if let Some(rest) = &pattern.rest { + javascript_assignment_target_root_symbols(&rest.target, scoping, symbols); + } + } + oxc_ast::ast::AssignmentTarget::ObjectAssignmentTarget(pattern) => { + for property in &pattern.properties { + match property { + oxc_ast::ast::AssignmentTargetProperty::AssignmentTargetPropertyIdentifier( + property, + ) => { + if let Some(symbol_id) = + property + .binding + .reference_id + .get() + .and_then(|reference_id| { + scoping.get_reference(reference_id).symbol_id() + }) + { + symbols.insert(symbol_id); + } + } + oxc_ast::ast::AssignmentTargetProperty::AssignmentTargetPropertyProperty( + property, + ) => javascript_assignment_target_maybe_default_root_symbols( + &property.binding, + scoping, + symbols, + ), + } + } + if let Some(rest) = &pattern.rest { + javascript_assignment_target_root_symbols(&rest.target, scoping, symbols); + } + } + _ => {} + } +} + +fn javascript_assignment_target_maybe_default_root_symbols( + target: &oxc_ast::ast::AssignmentTargetMaybeDefault<'_>, + scoping: &JavascriptScoping, + symbols: &mut BTreeSet, +) { + if let Some(target) = target.as_assignment_target() { + javascript_assignment_target_root_symbols(target, scoping, symbols); + } else if let oxc_ast::ast::AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(target) = + target + { + javascript_assignment_target_root_symbols(&target.binding, scoping, symbols); + } +} + #[derive(Clone)] struct JavascriptTopLevelAssignmentWrite { position: usize, @@ -8366,28 +8681,33 @@ fn javascript_top_level_assignment_writes( let JavascriptStatement::ExpressionStatement(statement) = statement else { continue; }; - let JavascriptExpression::AssignmentExpression(assignment) = &statement.expression else { - continue; - }; - let Some(symbol_id) = javascript_assignment_target_root_symbol(&assignment.left, scoping) + let Some(assignment) = + javascript_assignment_expression_from_expression(&statement.expression) else { continue; }; - let Some(name) = root_names.get(&symbol_id) else { + let mut symbols = BTreeSet::new(); + javascript_assignment_target_root_symbols(&assignment.left, scoping, &mut symbols); + if symbols.is_empty() { continue; - }; + } let span = statement.span; let Some(source) = content.get(span.start as usize..span.end as usize) else { continue; }; - writes - .entry(name.clone()) - .or_default() - .push(JavascriptTopLevelAssignmentWrite { - position: span.start as usize, - range: span.start as usize..span.end as usize, - source: source.to_string(), - }); + for name in symbols + .into_iter() + .filter_map(|symbol_id| root_names.get(&symbol_id)) + { + writes + .entry(name.clone()) + .or_default() + .push(JavascriptTopLevelAssignmentWrite { + position: span.start as usize, + range: span.start as usize..span.end as usize, + source: source.to_string(), + }); + } } writes } @@ -8953,7 +9273,10 @@ fn javascript_global_side_effect_projection(content: &str) -> String { if parsed.panicked || !parsed.diagnostics.is_empty() { return String::new(); } - let mut projection = String::new(); + let declarations = javascript_top_level_declarations(content); + let declaration_ranges = javascript_top_level_declaration_ranges(content); + let mut fragments = BTreeMap::<(usize, usize), String>::new(); + let mut pending_dependencies = Vec::::new(); for statement in &parsed.program.body { let JavascriptStatement::ExpressionStatement(statement) = statement else { continue; @@ -8980,7 +9303,41 @@ fn javascript_global_side_effect_projection(content: &str) -> String { { continue; } - projection.push_str(fragment); + fragments.insert( + (span.start as usize, span.end as usize), + fragment.to_string(), + ); + let Some(dependencies) = + javascript_projection_fragment_dependencies(fragment, &declarations) + else { + return String::new(); + }; + pending_dependencies.extend(dependencies); + } + let mut included_dependencies = BTreeSet::new(); + while let Some(dependency) = pending_dependencies.pop() { + if !included_dependencies.insert(dependency.clone()) { + continue; + } + let (Some(declaration), Some(range)) = ( + declarations.get(&dependency), + declaration_ranges.get(&dependency), + ) else { + continue; + }; + fragments + .entry((range.start, range.end)) + .or_insert_with(|| declaration.clone()); + let Some(dependencies) = + javascript_projection_fragment_dependencies(declaration, &declarations) + else { + return String::new(); + }; + pending_dependencies.extend(dependencies); + } + let mut projection = String::new(); + for fragment in fragments.into_values() { + projection.push_str(&fragment); projection.push('\n'); } projection @@ -9479,16 +9836,67 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at( let mut unit_prefix = String::new(); let mut unit_replacements = Vec::<(std::ops::Range, String)>::new(); let mut added_projection = false; + let mut unit_root_bindings = importer_analysis.root_bindings.clone(); + let mut unit_binding_names = importer_analysis.binding_names.clone(); + let mut unit_projection_identities = original_projection_identities + .get(importer) + .cloned() + .unwrap_or_default(); + let mut included_projection_names = unit_projection_identities + .iter() + .map(|(binding, identity)| (identity.clone(), binding.clone())) + .collect::>(); for dependency in javascript_static_dependency_initialization_order(importer, &module_bindings) { let Some(dependency_content) = module_contents.get(&dependency) else { continue; }; - let side_effects = javascript_global_side_effect_projection(dependency_content); + let mut side_effects = javascript_global_side_effect_projection(dependency_content); if side_effects.is_empty() { continue; } + let Some(side_effect_analysis) = javascript_module_analysis(&side_effects, true) + else { + return Err(format!( + "自主构建模块副作用投影不是有效 JavaScript:{dependency}" + )); + }; + let mut remaining = side_effect_analysis.root_bindings.clone(); + let mut assigned_names = unit_binding_names.clone(); + let mut side_effect_bindings = BTreeMap::::new(); + for original in &side_effect_analysis.root_bindings { + remaining.remove(original); + let canonical = unique_javascript_projection_binding_name( + original, + &assigned_names, + &remaining, + ); + assigned_names.insert(canonical.clone()); + side_effect_bindings.insert(original.clone(), canonical); + } + for (original, canonical) in &side_effect_bindings { + if original != canonical + && !rename_javascript_root_binding(&mut side_effects, original, canonical) + { + return Err(format!( + "自主构建模块副作用绑定无法按符号重命名:{dependency}::{original} -> {canonical}" + )); + } + let identity = (dependency.clone(), original.clone()); + included_projection_names.insert(identity.clone(), canonical.clone()); + unit_projection_identities.insert(canonical.clone(), identity); + unit_root_bindings.insert(canonical.clone()); + } + let Some(renamed_side_effect_analysis) = + javascript_module_analysis(&side_effects, true) + else { + return Err(format!( + "自主构建模块副作用投影重命名后语义无效:{dependency}" + )); + }; + assigned_names.extend(renamed_side_effect_analysis.binding_names); + unit_binding_names = assigned_names; unit_prefix.push_str(&side_effects); added_projection = true; } @@ -9603,16 +10011,6 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at( } } } - let mut unit_root_bindings = importer_analysis.root_bindings.clone(); - let mut unit_binding_names = importer_analysis.binding_names.clone(); - let mut unit_projection_identities = original_projection_identities - .get(importer) - .cloned() - .unwrap_or_default(); - let mut included_projection_names = unit_projection_identities - .iter() - .map(|(binding, identity)| (identity.clone(), binding.clone())) - .collect::>(); for origin in origin_order { let Some(bindings) = origins.remove(&origin) else { continue; 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 b1faead64..efcead174 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 @@ -3951,6 +3951,139 @@ fn javascript_side_effect_imports_precede_importers_without_linking_local_decoys ); } +#[test] +fn javascript_side_effect_projection_keeps_required_local_declarations_in_source_order() { + let temporary = tempfile::tempdir().expect("create ordered side-effect dependency project"); + let root = temporary.path(); + fs::create_dir_all(root.join("game")).expect("create game directory"); + fs::write( + root.join("game/setup.mjs"), + "const makeRenderer = () => () => 1; globalThis.renderFrame = makeRenderer(); const unrelated = 'decoy';", + ) + .expect("write side-effect dependency with a local helper"); + fs::write( + root.join("game/main.mjs"), + "import './setup.mjs'; window.renderFrame();", + ) + .expect("write side-effect importer"); + + let modules = read_external_gameplay_javascript_at( + root, + "", + ) + .expect("project the side effect together with its local dependency"); + let projected = modules + .module_units() + .iter() + .find(|unit| unit.contains("window.renderFrame()") && unit.contains("makeRenderer")) + .expect("the ordered side-effect projection must join the importer"); + let helper = projected + .find("const makeRenderer") + .expect("the side-effect projection must include its local declaration"); + let side_effect = projected + .find("globalThis.renderFrame = makeRenderer()") + .expect("the side-effect projection must include the observable write"); + assert!( + helper < side_effect, + "a required local declaration must remain before the side effect that reads it: {projected}", + ); + assert!( + !projected.contains("const unrelated"), + "an unrelated dependency-local declaration must not leak into the projection: {projected}", + ); + + fs::write( + root.join("game/setup.mjs"), + "export const makeRenderer = () => () => 1; globalThis.renderFrame = makeRenderer();", + ) + .expect("write a side-effect helper that is also imported"); + fs::write( + root.join("game/main.mjs"), + "import { makeRenderer } from './setup.mjs'; window.renderFrame(); makeRenderer()();", + ) + .expect("write an importer that also reads the side-effect helper"); + let modules = read_external_gameplay_javascript_at( + root, + "", + ) + .expect("deduplicate a declaration shared by side-effect and binding projection"); + let projected = modules + .module_units() + .iter() + .find(|unit| { + unit.contains("globalThis.renderFrame = makeRenderer()") + && unit.contains("window.renderFrame()") + && unit.contains("makeRenderer()()") + }) + .expect("the shared helper projection must join its importer"); + assert_eq!( + projected.matches("const makeRenderer").count(), + 1, + "a helper shared by side-effect and binding demand must be declared once: {projected}", + ); +} + +#[test] +fn javascript_destructuring_assignments_project_the_final_exported_callable() { + let html = ""; + for (label, assignments) in [ + ( + "object", + "({ start } = { start: () => import('./old.mjs').then(({ oldRun }) => oldRun()) }); ({ start } = { start: () => import('./dependency.mjs').then(({ run }) => run()) });", + ), + ( + "array", + "[start] = [() => import('./old.mjs').then(({ oldRun }) => oldRun())]; [start] = [() => import('./dependency.mjs').then(({ run }) => run())];", + ), + ] { + let temporary = tempfile::tempdir().expect("create destructuring 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"), + format!("export let start; {assignments}"), + ) + .expect("write destructuring live-binding assignments"); + fs::write( + root.join("game/old.mjs"), + "export function oldRun() { return import('./missing-old.mjs'); }", + ) + .expect("write obsolete destructured callable"); + fs::write( + root.join("game/dependency.mjs"), + "export function run() { return import('./final.mjs').then(({ finish }) => finish()); }", + ) + .expect("write final destructured callable dependency"); + fs::write( + root.join("game/final.mjs"), + "export function finish() { return 'done'; }", + ) + .expect("write final destructured callable closure"); + fs::write( + root.join("game/main.mjs"), + "import { start } from './origin.mjs'; start();", + ) + .expect("write destructured callable consumer"); + + let modules = read_external_gameplay_javascript_at(root, html) + .unwrap_or_else(|error| panic!("project {label} destructuring assignment: {error}")); + let projected = modules + .module_units() + .iter() + .find(|unit| unit.contains("start();") && unit.contains("function finish()")) + .unwrap_or_else(|| { + panic!( + "{label} destructuring assignment must project its final callable closure: {:#?}", + modules.module_units() + ) + }); + assert!( + !projected.contains("function oldRun()"), + "only the final destructuring assignment may define the exported callable root: {projected}", + ); + } +} + #[test] fn javascript_projection_preserves_dependency_declaration_source_order() { let temporary = tempfile::tempdir().expect("create declaration-order project"); @@ -4087,6 +4220,80 @@ fn javascript_top_level_await_fails_closed_only_when_statically_non_completing() .expect("write completing top-level await with nested decoy"); read_external_gameplay_javascript_at(root, html) .expect("a completing top-level await and uncalled nested decoy remain valid"); + + for source in [ + "await new Promise(() => 42); export function start() { return 1; }", + "await new Promise((resolve) => console.log(resolve.name)); export function start() { return 1; }", + "await new Promise((resolve) => { function nested(resolve) { resolve(); } }); export function start() { return 1; }", + ] { + fs::write(root.join("game/dependency.mjs"), source) + .expect("write a non-settling Promise executor"); + let error = read_external_gameplay_javascript_at(root, html) + .expect_err("an executor that never invokes its resolver must not complete"); + assert!( + error.contains("top-level await") || error.contains("顶层 await"), + "{error}", + ); + } + + for source in [ + "await new Promise((resolve) => resolve(1)); export function start() { return 1; }", + "await new Promise((_resolve, reject) => reject(new Error('done'))).catch(() => {}); export function start() { return 1; }", + "const Promise = class { constructor() {} }; await new Promise(() => 42); export function start() { return 1; }", + "async function nested() { await new Promise(() => 42); } await Promise.resolve(); export function start() { return 1; }", + ] { + fs::write(root.join("game/dependency.mjs"), source) + .expect("write a completing or out-of-scope Promise case"); + read_external_gameplay_javascript_at(root, html) + .expect("resolver calls, Promise shadowing and nested awaits must remain valid"); + } +} + +#[test] +fn javascript_dynamic_then_destructuring_requires_a_reachable_binding_use() { + let temporary = tempfile::tempdir().expect("create dynamic then demand project"); + let root = temporary.path(); + fs::create_dir_all(root.join("game")).expect("create game directory"); + fs::write( + root.join("game/dependency.mjs"), + "export function run() { return import('./missing-unused.mjs'); }", + ) + .expect("write demand-sensitive dynamic export"); + fs::write( + root.join("game/origin.mjs"), + "export function start() { return import('./dependency.mjs').then(({ run }) => { if (false) run(); }); }", + ) + .expect("write unused dynamic destructuring callback"); + fs::write( + root.join("game/main.mjs"), + "import { start } from './origin.mjs'; start();", + ) + .expect("write dynamic then consumer"); + let html = ""; + read_external_gameplay_javascript_at(root, html) + .expect("pure destructuring and unreachable uses must not demand the export callable"); + + fs::write( + root.join("game/dependency.mjs"), + "export function run() { return import('./final.mjs').then(({ finish }) => finish()); }", + ) + .expect("write reachable dynamic export"); + fs::write( + root.join("game/final.mjs"), + "export function finish() { return 'used'; }", + ) + .expect("write reachable dynamic export closure"); + fs::write( + root.join("game/origin.mjs"), + "export function start() { return import('./dependency.mjs').then(({ run }) => run()); }", + ) + .expect("write used dynamic destructuring callback"); + let modules = read_external_gameplay_javascript_at(root, html) + .expect("a reachable destructured binding call must demand its export closure"); + assert!(modules + .module_units() + .iter() + .any(|unit| unit.contains("start();") && unit.contains("function finish()"))); } #[test] diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index cd2713af8..0cdb15c34 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -6010,3 +6010,10 @@ - ESM 最终绑定与 occurrence 补充:外部 callable root 只认模块初始化完成时同一 live binding / member 的最后一次直接顶层 assignment,旧 RHS 不得加载;直接 awaited namespace member 在赋值、受控 callback、constructor 和深层静态 member path 中仍按 `(source, occurrence)` 传播并替换首段 export。assignment class expression 的 static / instance member demand 分离。 - ESM 初始化与阻塞补充:projection declaration 保持原模块源码顺序,dependency origin 保持 importer 声明顺序;循环 canonical 去重同时删除声明和对应初始化写入。side-effect static import 只联结按 dependency-before-importer 排列的直接 `globalThis / window` 顶层 effect,不暴露 dependency local binding;静态可判定永不完成的 top-level await 至少对未遮蔽全局 `await new Promise(() => {})` 失败关闭,嵌套函数同形 decoy 和可完成 await 保持允许。 + +## 2026-08-04 ESM 解构写入与模块完成性收口 + +- side-effect static import 若读取 dependency-local declaration,组合投影必须把该声明及其 semantic 依赖按原源码位置放在 effect 前;同一声明也被 importer 使用时按 origin declaration identity 去重并统一做 collision-safe canonical 重命名,既不泄露无关 local,也不引入 TDZ 或重复声明。 +- exported live binding 的顶层 object / array destructuring assignment 纳入 projection;最终一次写入中与目标 root 对应的属性或槽位 callable 才作为 dynamic demand root,更早写入和相邻 decoy 不得回流。 +- 未遮蔽全局 top-level `await new Promise(executor)` 的 executor 若不调用或传递 resolve / reject、也不显式 throw,则返回值不参与 Promise settle,静态门按不完成失败关闭;resolver 按 semantic symbol 识别,Promise 遮蔽与嵌套函数内 await decoy 继续保留。 +- dynamic import `.then` callback 的对象解构只建立 occurrence-scoped binding,不再仅因读取 export 属性就形成 callable demand;只有该 binding 的可达引用或调用才向 export 内部传播动态依赖,未使用和恒假引用保持关闭。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index e67160f3e..821f79553 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -833,7 +833,9 @@ game-project/ - 俄罗斯方块任务固定使用 `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 的写入不随文本同名混入。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 或在每个位置查询时复制区间、重扫整份源码。 +- 顶层 object / array destructuring assignment 写入 exported live binding 时,projection 必须按 assignment target 内实际解析到的 root symbol 收集整条写入,并从最终一次写入中精确选取对应对象属性或数组槽位的 function / arrow 作为外部 callable root;更早写入和其它解构槽位不得反向加载 dynamic dependency。 +- 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,继续服从恒假分支与函数可达性过滤。`.then(({ run }) => ...)` 的解构本身不构成 callable demand,只有绑定存在可达引用或调用时才传播对应 export;未使用绑定和恒假分支中的引用不得加载 export 内动态依赖。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 或在每个位置查询时复制区间、重扫整份源码。 +- side-effect static import 的 `globalThis / window` 顶层 effect 如果读取 dependency-local declaration,必须把该 declaration 的 semantic 依赖闭包按原模块源码位置共同投影,不能把 effect 统一前置到声明之前制造 TDZ;同一 declaration 同时服务 side effect 和 imported binding 时按 `(origin module, root binding)` identity 去重并参与 canonical 重命名。未遮蔽全局 `await new Promise(executor)` 只有 executor 可静态观察到 resolve / reject 调用、把 resolver 交给可能调用它的 callback,或显式 throw 时才视为可能 settle;仅返回表达式、读取 resolver 或只调用遮蔽参数均按不完成失败关闭,嵌套函数中的 top-level-await 形状继续不影响模块求值。 - 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`;`