修复脚本调用与终止求值顺序

冻结 identifier callee 与 new constructor 的参数前身份,并将调用副作用保留在参数之后。

将 callable assignment 延后到 RHS 完成,保留自调用赋值的旧 callable。

按 Oxc AST 函数体与语句边界处理 return、throw、guard 和 catch 的 alias 副作用。

新增六类求值顺序回归并同步技术方案与项目决策记录。
This commit is contained in:
2026-08-04 13:52:24 +08:00
parent e37945e306
commit 4bed4a149a
4 changed files with 257 additions and 43 deletions
@@ -1403,7 +1403,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
.map(|source| JavascriptReceiverOwnerValue::Alias {
source,
force_instance: true,
frozen_at: position,
frozen_at: new_expression.span.start as usize,
})
.into_iter()
.collect(),
@@ -1453,7 +1453,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
fn callable_values(
&self,
expression: &JavascriptExpression<'_>,
position: usize,
captured_at: usize,
) -> Vec<JavascriptCallableValue> {
match expression {
JavascriptExpression::FunctionExpression(_)
@@ -1468,16 +1468,16 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
.referenced_symbol(expression)
.map(|source| JavascriptCallableValue::Alias {
source,
captured_at: position,
captured_at,
})
.into_iter()
.collect(),
JavascriptExpression::ParenthesizedExpression(parenthesized) => {
self.callable_values(&parenthesized.expression, position)
self.callable_values(&parenthesized.expression, captured_at)
}
JavascriptExpression::ConditionalExpression(conditional) => {
let mut values = self.callable_values(&conditional.consequent, position);
values.extend(self.callable_values(&conditional.alternate, position));
let mut values = self.callable_values(&conditional.consequent, captured_at);
values.extend(self.callable_values(&conditional.alternate, captured_at));
values
}
JavascriptExpression::CallExpression(call) => call
@@ -1489,7 +1489,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
if let Some(source) = self.referenced_symbol(bind.object()) {
return vec![JavascriptCallableValue::Alias {
source,
captured_at: position,
captured_at,
}];
}
bind.object()
@@ -1501,7 +1501,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
.map(|(receiver, name)| JavascriptCallableValue::Member {
receiver,
name,
captured_at: position,
captured_at,
})
.into_iter()
.collect()
@@ -1515,6 +1515,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
&mut self,
symbol_id: JavascriptSymbolId,
expression: &JavascriptExpression<'_>,
captured_at: usize,
position: usize,
) {
if javascript_position_is_in_literal_false_block(
@@ -1524,7 +1525,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
) {
return;
}
let values = self.callable_values(expression, position);
let values = self.callable_values(expression, captured_at);
let events = self.callable_events.entry(symbol_id).or_default();
if values.is_empty() {
events.push(JavascriptAliasEvent {
@@ -1574,7 +1575,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
.map(|source| JavascriptReceiverOwnerValue::Alias {
source,
force_instance: true,
frozen_at: position,
frozen_at: new_expression.span.start as usize,
})
.into_iter()
.collect(),
@@ -1659,7 +1660,8 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> {
self.record_callable_assignment(
symbol_id,
initializer,
declarator.span.start as usize,
initializer.span().start as usize,
declarator.span.end as usize,
);
}
}
@@ -1685,7 +1687,8 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> {
self.record_callable_assignment(
symbol_id,
&assignment.right,
assignment.span.start as usize,
assignment.right.span().start as usize,
assignment.span.end as usize,
);
}
}
@@ -1971,7 +1974,12 @@ impl JavascriptFunctionInvocationCollector<'_, '_> {
indices
}
fn record_identifier(&mut self, identifier: &oxc_ast::ast::IdentifierReference<'_>, at: usize) {
fn record_identifier(
&mut self,
identifier: &oxc_ast::ast::IdentifierReference<'_>,
resolve_at: usize,
invoke_at: usize,
) {
let Some(symbol_id) = identifier
.reference_id
.get()
@@ -1979,8 +1987,8 @@ impl JavascriptFunctionInvocationCollector<'_, '_> {
else {
return;
};
for index in self.callable_indices_for_symbol(symbol_id, at, &mut BTreeSet::new()) {
self.ranges[index].invocations.push(at);
for index in self.callable_indices_for_symbol(symbol_id, resolve_at, &mut BTreeSet::new()) {
self.ranges[index].invocations.push(invoke_at);
}
}
@@ -2185,7 +2193,7 @@ impl JavascriptFunctionInvocationCollector<'_, '_> {
) {
match expression {
JavascriptExpression::Identifier(identifier) => {
self.record_identifier(identifier, invoke_at)
self.record_identifier(identifier, resolve_at, invoke_at)
}
JavascriptExpression::ParenthesizedExpression(parenthesized) => {
self.record_expression(&parenthesized.expression, resolve_at, invoke_at);
@@ -4942,9 +4950,59 @@ impl JavascriptConditionalExecutionCollector<'_> {
fn push_span(&mut self, span: oxc_span::Span) {
self.ranges.push(span.start as usize..span.end as usize);
}
fn mark_guarded_tail(&mut self, statements: &[JavascriptStatement<'_>]) {
for (index, statement) in statements.iter().enumerate() {
let JavascriptStatement::IfStatement(guard) = statement else {
continue;
};
if guard.alternate.is_some()
|| self.condition(&guard.test).is_some()
|| !javascript_statement_definitely_terminates(&guard.consequent)
{
continue;
}
for guarded in &statements[index + 1..] {
self.push_span(guarded.span());
}
break;
}
}
}
fn javascript_statement_definitely_terminates(statement: &JavascriptStatement<'_>) -> bool {
match statement {
JavascriptStatement::ReturnStatement(_) | JavascriptStatement::ThrowStatement(_) => true,
JavascriptStatement::BlockStatement(block) => block
.body
.iter()
.any(javascript_statement_definitely_terminates),
JavascriptStatement::IfStatement(statement) => {
statement.alternate.as_ref().is_some_and(|alternate| {
javascript_statement_definitely_terminates(&statement.consequent)
&& javascript_statement_definitely_terminates(alternate)
})
}
_ => false,
}
}
impl<'a> VisitJavascript<'a> for JavascriptConditionalExecutionCollector<'_> {
fn visit_function_body(&mut self, body: &oxc_ast::ast::FunctionBody<'a>) {
self.mark_guarded_tail(&body.statements);
oxc_ast_visit::walk::walk_function_body(self, body);
}
fn visit_block_statement(&mut self, block: &oxc_ast::ast::BlockStatement<'a>) {
self.mark_guarded_tail(&block.body);
oxc_ast_visit::walk::walk_block_statement(self, block);
}
fn visit_catch_clause(&mut self, clause: &oxc_ast::ast::CatchClause<'a>) {
self.push_span(clause.body.span);
oxc_ast_visit::walk::walk_catch_clause(self, clause);
}
fn visit_if_statement(&mut self, statement: &oxc_ast::ast::IfStatement<'a>) {
match self.condition(&statement.test) {
Some(true) => {
@@ -8250,31 +8308,94 @@ fn javascript_termination_is_single_statement_guard(content: &str, start: usize)
.is_some_and(|keyword| matches!(keyword.as_str(), "if" | "while" | "for" | "with"))
}
fn javascript_top_level_unconditional_termination_from_ast(content: &str) -> Option<Option<usize>> {
let body_start = content.find('{')?;
let body_end = matching_javascript_brace(content, body_start)?;
let prefix = "async function* __tetris_gate__(){";
let wrapped = format!("{prefix}{}\n}}", &content[body_start + 1..body_end]);
let allocator = JavascriptAllocator::default();
let parsed = JavascriptParser::new(&allocator, &wrapped, javascript_source_type(false)).parse();
if parsed.panicked || !parsed.diagnostics.is_empty() {
return None;
#[derive(Clone)]
struct JavascriptFunctionBodyControlFlow {
function: std::ops::Range<usize>,
body: std::ops::Range<usize>,
termination_end: Option<usize>,
}
#[derive(Default)]
struct JavascriptFunctionBodyControlFlowCollector {
functions: Vec<JavascriptFunctionBodyControlFlow>,
}
impl JavascriptFunctionBodyControlFlowCollector {
fn push(&mut self, function_span: oxc_span::Span, body: &oxc_ast::ast::FunctionBody<'_>) {
self.functions.push(JavascriptFunctionBodyControlFlow {
function: function_span.start as usize..function_span.end as usize,
body: body.span.start as usize..body.span.end as usize,
termination_end: body
.statements
.iter()
.find(|statement| {
matches!(
statement,
JavascriptStatement::ReturnStatement(_)
| JavascriptStatement::ThrowStatement(_)
)
})
.map(|statement| statement.span().end as usize),
});
}
let function = match parsed.program.body.first()? {
JavascriptStatement::FunctionDeclaration(function) => function,
_ => return None,
};
let body = function.body.as_ref()?;
let termination = body.statements.iter().find(|statement| {
matches!(
statement,
JavascriptStatement::ReturnStatement(_) | JavascriptStatement::ThrowStatement(_)
}
impl<'a> VisitJavascript<'a> for JavascriptFunctionBodyControlFlowCollector {
fn visit_function(&mut self, function: &JavascriptFunction<'a>, flags: JavascriptScopeFlags) {
if let Some(body) = &function.body {
self.push(function.span, body);
}
oxc_ast_visit::walk::walk_function(self, function, flags);
}
fn visit_arrow_function_expression(
&mut self,
function: &JavascriptArrowFunctionExpression<'a>,
) {
if let oxc_ast::ast::ArrowFunctionBody::FunctionBody(body) = &function.body {
self.push(function.span, body);
}
oxc_ast_visit::walk::walk_arrow_function_expression(self, function);
}
}
fn javascript_top_level_unconditional_termination_from_ast(content: &str) -> Option<Option<usize>> {
for (prefix, suffix) in [("", ""), ("(", ")"), ("({", "})")] {
let wrapped = format!("{prefix}{content}{suffix}");
let allocator = JavascriptAllocator::default();
let parsed = JavascriptParser::new(
&allocator,
&wrapped,
JavascriptSourceType::default().with_unambiguous(true),
)
});
Some(
termination
.map(|statement| body_start + 1 + statement.span().start as usize - prefix.len()),
)
.parse();
if parsed.panicked || !parsed.diagnostics.is_empty() {
continue;
}
let content_start = prefix.len();
let content_end = content_start + content.len();
let mut collector = JavascriptFunctionBodyControlFlowCollector::default();
collector.visit_program(&parsed.program);
let Some(function) = collector
.functions
.into_iter()
.filter(|function| {
content_start <= function.function.start
&& function.function.end <= content_end
&& content_start <= function.body.start
&& function.body.end <= content_end
})
.max_by_key(|function| function.function.end - function.function.start)
else {
continue;
};
return Some(
function
.termination_end
.map(|termination| termination - content_start),
);
}
None
}
fn javascript_before_top_level_unconditional_termination(content: &str) -> &str {
@@ -3974,6 +3974,100 @@ fn javascript_nested_call_alias_effects_follow_javascript_evaluation_order() {
);
}
#[test]
fn javascript_identifier_callee_is_frozen_before_arguments_but_invoked_after_them() {
let source = "import * as initial from './initial.mjs'; import * as originalValue from './original.mjs'; import * as replacementValue from './replacement.mjs'; let receiver = initial; function original() { receiver = originalValue; } function replacement() { receiver = replacementValue; } let start = original; function swap() { start = replacement; } start(swap()); receiver.run();";
let analysis = super::autonomous_completion::javascript_module_analysis(source, true)
.expect("analyze identifier callee evaluation order");
assert!(analysis.import_member_calls.contains_key("originalValue"));
assert!(
!analysis
.import_member_calls
.contains_key("replacementValue"),
"arguments may mutate the binding, but the already-resolved callee must stay original"
);
}
#[test]
fn javascript_new_callee_and_instance_owner_are_frozen_before_arguments() {
let source = "import * as initial from './initial.mjs'; import * as originalConstructor from './original-constructor.mjs'; import * as originalRun from './original-run.mjs'; import * as replacementConstructor from './replacement-constructor.mjs'; import * as replacementRun from './replacement-run.mjs'; let constructorReceiver = initial; let methodReceiver = initial; class Original { constructor() { constructorReceiver = originalConstructor; } run() { methodReceiver = originalRun; } } class Replacement { constructor() { constructorReceiver = replacementConstructor; } run() { methodReceiver = replacementRun; } } let Constructor = Original; function swap() { Constructor = Replacement; } const instance = new Constructor(swap()); instance.run(); constructorReceiver.run(); methodReceiver.run();";
let analysis = super::autonomous_completion::javascript_module_analysis(source, true)
.expect("analyze constructor evaluation order");
for dependency in ["originalConstructor", "originalRun"] {
assert!(
analysis.import_member_calls.contains_key(dependency),
"missing frozen constructor dependency {dependency}"
);
}
for dependency in ["replacementConstructor", "replacementRun"] {
assert!(
!analysis.import_member_calls.contains_key(dependency),
"argument-side reassignment must not replace the resolved constructor owner: {dependency}"
);
}
}
#[test]
fn javascript_callable_assignment_takes_effect_after_its_rhs() {
let source = "import * as initial from './initial.mjs'; import * as selected from './selected.mjs'; let receiver = initial; function original() { receiver = selected; return null; } let start = original; start = start(); receiver.run();";
let analysis = super::autonomous_completion::javascript_module_analysis(source, true)
.expect("analyze callable self-assignment evaluation order");
assert!(
analysis.import_member_calls.contains_key("selected"),
"start = start() must invoke the value that existed before the RHS began"
);
}
#[test]
fn javascript_return_and_throw_expressions_apply_effects_before_termination() {
let returned = "import * as initial from './initial.mjs'; import * as selected from './selected.mjs'; import * as unreachable from './unreachable.mjs'; let receiver = initial; function stop() { return receiver = selected; receiver = unreachable; } stop(); receiver.run();";
let analysis = super::autonomous_completion::javascript_module_analysis(returned, true)
.expect("analyze return assignment effect");
assert!(analysis.import_member_calls.contains_key("selected"));
assert!(!analysis.import_member_calls.contains_key("unreachable"));
let thrown = "import * as initial from './initial.mjs'; import * as selected from './selected.mjs'; import * as unreachable from './unreachable.mjs'; let receiver = initial; function select() { receiver = selected; } function stop() { throw select(); receiver = unreachable; } stop(); receiver.run();";
let analysis = super::autonomous_completion::javascript_module_analysis(thrown, true)
.expect("analyze throw call effect");
assert!(analysis.import_member_calls.contains_key("selected"));
assert!(!analysis.import_member_calls.contains_key("unreachable"));
}
#[test]
fn javascript_function_termination_uses_the_ast_body_span() {
let source = "import * as initial from './initial.mjs'; import * as selected from './selected.mjs'; import * as unreachable from './unreachable.mjs'; let receiver = initial; function stop({ value = {} } = {}) { receiver = selected; return value; receiver = unreachable; } stop(); receiver.run();";
let analysis = super::autonomous_completion::javascript_module_analysis(source, true)
.expect("analyze function body after destructured default parameter");
assert!(analysis.import_member_calls.contains_key("selected"));
assert!(
!analysis.import_member_calls.contains_key("unreachable"),
"parameter braces must not be mistaken for the function body"
);
}
#[test]
fn javascript_guard_and_catch_effects_preserve_the_skipped_state() {
let guarded = "import * as initial from './initial.mjs'; import * as selected from './selected.mjs'; let receiver = initial; function mutate() { if (skip) return; receiver = selected; } mutate(); receiver.run();";
let analysis = super::autonomous_completion::javascript_module_analysis(guarded, true)
.expect("analyze guard-clause alias effect");
for binding in ["initial", "selected"] {
assert!(
analysis.import_member_calls.contains_key(binding),
"guard clause must preserve possible owner {binding}"
);
}
let caught = "import * as initial from './initial.mjs'; import * as selected from './selected.mjs'; let receiver = initial; function mutate() { try { work(); } catch (error) { receiver = selected; } } mutate(); receiver.run();";
let analysis = super::autonomous_completion::javascript_module_analysis(caught, true)
.expect("analyze catch-clause alias effect");
for binding in ["initial", "selected"] {
assert!(
analysis.import_member_calls.contains_key(binding),
"catch clause must preserve possible owner {binding}"
);
}
}
#[test]
fn javascript_constant_conditions_are_case_and_scope_sensitive() {
for condition in ["FALSE", "TRUE", "undefined"] {
@@ -5984,10 +5984,10 @@
- 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 / instanceclass 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 bindingdynamic 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 bindingdynamic shorthand 保留原键,循环模块按相同原始声明去重,同名 dynamic export 不得拉入无引用本地声明。
- JavaScript / ESM 构造、继承与 callable 补充:`new` 沿冻结的 class owner 图执行本类显式 constructor、显式 `super()` 或隐式 derived constructor,并支持直接 class expressioninstance / 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 执行。
- JavaScript / ESM callable、callback 与视觉可达性补充:callable conditional expression 合并两端全部身份;声明式与后续赋值式 member `.bind()` 都冻结成员 owner。受控 callback API 穿透 callee / receiver 外层括号,Promise `.then` 取 fulfilled 与 rejected 两个 callback 槽,`.catch/.finally` 仍仅取首槽。视觉资产路径与 `drawImage` 启发式可使用 ASCII 小写副本,但 AST、semantic binding 和函数可达性只解析原始大小写 JavaScript,大小写不同的 `MainLoop/mainloop` 不得合并。
- 浏览器因果:状态证据仍只冻结 trusted input listener 及其点击派生微任务内的变化;完整手势身份改由宿主在成功完成 Chromium 元素鼠标输入后调用隔离世界 finish。更早注册的 `window` capture listener 即使调用 `stopImmediatePropagation()` 也不能阻断探针自身的完成身份,页面脚本不能伪造 host finishRAF / 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 重命名不能改写原始身份;删除回流声明后仍把 import 引用改接到 importer 已有 canonical,并给固定点保留“模块数 + 1”轮的产出与稳定确认预算,未收敛时失败关闭。inline module 先按浏览器可执行标签提取原文并计入与外部脚本共享的累计 `2 MiB` 源码预算,再做语法和语义校验;无效超限模块不能被 helper 静默过滤,无效小模块也明确失败关闭。
- JavaScript / ESM alias 求值顺序补充:receiver alias 保存赋值完成时刻并在该时刻解析 source owner,后续 source 重赋值不得倒灌。调用事件按内层参数 / RHS 先于外层调用 / assignment 生效;`switch case/default` 赋值一律保留跳过与各分支可能状态,普通函数内无条件 `return / throw` 截断之后的 alias 副作用。恒真 / 恒假关键字大小写敏感,可能被局部或参数遮蔽的 `undefined` 不再作为文本恒假值。
- JavaScript / ESM callee 与终止顺序补充:identifier callee 和 `new C(args)` 的 constructor / instance owner 在实参前冻结,invocation effect 保留在实参之后;callable assignment 到 RHS 完成后才生效,`start = start()` 继续调用旧值。`return / throw` 表达式中的 assignment / call 先执行,截断点取表达式之后的 AST statement end;函数体使用 Oxc body span,不从默认参数或解构参数中的首个 `{` 猜测。未知 guard clause 后续与 `catch` 体一律按 conditional effect 合并旧状态。
@@ -834,9 +834,8 @@ game-project/
- 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` 反查。受控 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 / instanceclass 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 / instanceclass 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` 不能触发参数执行推断
- callable conditional expression 必须合并 consequent / alternate 的全部可调用身份;声明式和后续赋值式 `api.run.bind(api)` 都在绑定时冻结成员 owner 与方法名。受控 callback 识别穿透 callee 与 receiver 外层括号;Promise `.then` 的 fulfilled / rejected 两个参数都是 callback`.catch` / `.finally` 仍仅取第一个。视觉资产启发式允许使用 ASCII 小写副本查找路径、`drawImage` 与图片变量,但函数调用图、semantic binding 和可达性必须始终解析原始大小写 JavaScript,禁止把 `MainLoop``mainloop` 合并。
- 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 / instanceclass 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,未 override 的 instance / static 方法沿冻结的 `extends` 链查找普通嵌套 function 拥有独立 `this`,只有 arrow 词法继承外层 owner。声明后通过 assignment 安装的 function / arrow、本地 function alias 和 `.bind()` 结果必须保留 callable identity;只有真正 Function.prototype 的 `.call()` / `.apply()` / `.bind()` 采用 callable 语义,业务对象同名方法仍按 receiver method 解析。callable conditional expression 必须合并 consequent / alternate 的全部可调用身份;声明式和后续赋值式 `api.run.bind(api)` 都在绑定时冻结成员 owner 与方法名。受控 callback 识别穿透 callee 与 receiver 外层括号;Promise `.then` 的 fulfilled / rejected 两个参数都是 callback`.catch` / `.finally` 仍仅取第一个。视觉资产启发式允许使用 ASCII 小写副本查找路径、`drawImage` 与图片变量,但函数调用图、semantic binding 和可达性必须始终解析原始大小写 JavaScript,禁止把 `MainLoop``mainloop` 合并
- 泥点不足是确定性业务中断,不是瞬态 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 重放,禁止分别执行原响应和持久响应。