修复脚本别名求值顺序与分支状态
Project CI / Repository checks (push) Failing after 45s
Project CI / Frontend tests (push) Successful in 3m7s
Project CI / Backend tests (push) Successful in 3m40s
Project CI / Native shell tests (push) Failing after 17m14s

冻结 receiver 赋值时的 source owner,阻止后续重赋值倒灌

按 JavaScript 参数与 RHS 求值顺序传播嵌套调用副作用

补齐 switch 条件赋值、无条件退出截断与条件关键字作用域语义

新增回归测试并同步技术方案与项目决策记录
This commit is contained in:
2026-08-04 13:15:55 +08:00
parent f47a30cff2
commit 149b21622d
4 changed files with 155 additions and 39 deletions
@@ -911,8 +911,7 @@ fn javascript_condition_is_obviously_false(condition: &str) -> bool {
let compact = condition
.chars()
.filter(|character| !character.is_ascii_whitespace())
.collect::<String>()
.to_ascii_lowercase();
.collect::<String>();
matches!(
compact.as_str(),
"false"
@@ -920,7 +919,6 @@ fn javascript_condition_is_obviously_false(condition: &str) -> bool {
| "!true"
| "!!0"
| "null"
| "undefined"
| "1===0"
| "0===1"
| "1==0"
@@ -936,8 +934,7 @@ fn javascript_condition_is_obviously_true(condition: &str) -> bool {
let compact = condition
.chars()
.filter(|character| !character.is_ascii_whitespace())
.collect::<String>()
.to_ascii_lowercase();
.collect::<String>();
matches!(
compact.as_str(),
"true" | "1" | "!false" | "!!1" | "1===1" | "1==1" | "true===true" | "true==true"
@@ -1312,6 +1309,7 @@ enum JavascriptReceiverOwnerValue {
Alias {
source: JavascriptSymbolId,
force_instance: bool,
frozen_at: usize,
},
}
@@ -1353,6 +1351,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
.map(|source| JavascriptReceiverOwnerValue::Alias {
source,
force_instance: true,
frozen_at: position,
})
.into_iter()
.collect(),
@@ -1361,12 +1360,13 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
.map(|source| JavascriptReceiverOwnerValue::Alias {
source,
force_instance: false,
frozen_at: position,
})
.into_iter()
.collect(),
JavascriptExpression::ConditionalExpression(expression) => {
let mut values = self.receiver_values(&expression.consequent);
values.extend(self.receiver_values(&expression.alternate));
let mut values = self.receiver_values(&expression.consequent, position);
values.extend(self.receiver_values(&expression.alternate, position));
values
}
_ => Vec::new(),
@@ -1401,6 +1401,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
fn receiver_values(
&self,
initializer: &JavascriptExpression<'_>,
position: usize,
) -> Vec<JavascriptReceiverOwnerValue> {
match initializer {
JavascriptExpression::ObjectExpression(object) => {
@@ -1422,6 +1423,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
.map(|source| JavascriptReceiverOwnerValue::Alias {
source,
force_instance: true,
frozen_at: position,
})
.into_iter()
.collect(),
@@ -1430,12 +1432,13 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
.map(|source| JavascriptReceiverOwnerValue::Alias {
source,
force_instance: false,
frozen_at: position,
})
.into_iter()
.collect(),
JavascriptExpression::ConditionalExpression(expression) => {
let mut values = self.receiver_values(&expression.consequent);
values.extend(self.receiver_values(&expression.alternate));
let mut values = self.receiver_values(&expression.consequent, position);
values.extend(self.receiver_values(&expression.alternate, position));
values
}
_ => Vec::new(),
@@ -1491,7 +1494,7 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> {
self.record_binding_initializer(
symbol_id,
initializer,
declarator.span.start as usize,
declarator.span.end as usize,
);
}
}
@@ -1511,7 +1514,7 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> {
self.record_binding_initializer(
symbol_id,
&assignment.right,
assignment.span.start as usize,
assignment.span.end as usize,
);
}
}
@@ -1659,6 +1662,12 @@ fn javascript_known_callback_argument_indices(
NONE
}
fn javascript_invocation_effect_position(span: oxc_span::Span) -> usize {
// A nested call's closing delimiter precedes its parent's closing delimiter. Using that
// source position models argument/RHS evaluation before the enclosing call or assignment.
(span.end as usize).saturating_sub(1)
}
impl JavascriptFunctionInvocationCollector<'_, '_> {
fn record_identifier(&mut self, identifier: &oxc_ast::ast::IdentifierReference<'_>, at: usize) {
let Some(reference_id) = identifier.reference_id.get() else {
@@ -1719,15 +1728,14 @@ impl JavascriptFunctionInvocationCollector<'_, '_> {
JavascriptReceiverOwnerValue::Alias {
source,
force_instance,
} => {
owners.extend(
self.receiver_owners_for_symbol(*source, at, visiting)
.into_iter()
.map(|(start, end, is_static)| {
(start, end, if *force_instance { false } else { is_static })
}),
);
}
frozen_at,
} => owners.extend(
self.receiver_owners_for_symbol(*source, *frozen_at, visiting)
.into_iter()
.map(|(start, end, is_static)| {
(start, end, if *force_instance { false } else { is_static })
}),
),
}
}
}
@@ -1807,11 +1815,18 @@ impl JavascriptFunctionInvocationCollector<'_, '_> {
indices
}
fn record_expression(&mut self, expression: &JavascriptExpression<'_>, at: usize) {
fn record_expression(
&mut self,
expression: &JavascriptExpression<'_>,
resolve_at: usize,
invoke_at: usize,
) {
match expression {
JavascriptExpression::Identifier(identifier) => self.record_identifier(identifier, at),
JavascriptExpression::Identifier(identifier) => {
self.record_identifier(identifier, invoke_at)
}
JavascriptExpression::ParenthesizedExpression(parenthesized) => {
self.record_expression(&parenthesized.expression, at);
self.record_expression(&parenthesized.expression, resolve_at, invoke_at);
}
JavascriptExpression::FunctionExpression(_)
| JavascriptExpression::ArrowFunctionExpression(_) => {
@@ -1821,14 +1836,14 @@ impl JavascriptFunctionInvocationCollector<'_, '_> {
.get(&(span.start as usize, span.end as usize))
{
for index in indices {
self.ranges[*index].invocations.push(at);
self.ranges[*index].invocations.push(invoke_at);
}
}
}
_ => {
if let Some(member) = expression.as_member_expression() {
for index in self.method_indices_for_member(member, at) {
self.ranges[index].invocations.push(at);
for index in self.method_indices_for_member(member, resolve_at) {
self.ranges[index].invocations.push(invoke_at);
}
}
}
@@ -1866,15 +1881,16 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionInvocationCollector<'a, '_> {
}
fn visit_call_expression(&mut self, call: &JavascriptCallExpression<'a>) {
let at = call.span.start as usize;
let resolve_at = call.span.start as usize;
let invoke_at = javascript_invocation_effect_position(call.span);
if let Some(member) = call.callee.as_member_expression() {
if matches!(member.static_property_name(), Some("call" | "apply")) {
self.record_expression(member.object(), at);
self.record_expression(member.object(), resolve_at, invoke_at);
} else {
self.record_expression(&call.callee, at);
self.record_expression(&call.callee, resolve_at, invoke_at);
}
} else {
self.record_expression(&call.callee, at);
self.record_expression(&call.callee, resolve_at, invoke_at);
}
for index in self.callback_argument_indices(call) {
if let Some(expression) = call
@@ -1882,16 +1898,17 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionInvocationCollector<'a, '_> {
.get(*index)
.and_then(|argument| argument.as_expression())
{
self.record_expression(expression, at);
self.record_expression(expression, resolve_at, invoke_at);
}
}
oxc_ast_visit::walk::walk_call_expression(self, call);
}
fn visit_new_expression(&mut self, expression: &oxc_ast::ast::NewExpression<'a>) {
let at = expression.span.start as usize;
let resolve_at = expression.span.start as usize;
let invoke_at = javascript_invocation_effect_position(expression.span);
let mut indices = self
.receiver_owner(&expression.callee, at)
.receiver_owner(&expression.callee, resolve_at)
.into_iter()
.flat_map(|(start, end, _)| {
self.method_ranges
@@ -1904,7 +1921,7 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionInvocationCollector<'a, '_> {
indices.sort_unstable();
indices.dedup();
for index in indices {
self.ranges[index].invocations.push(at);
self.ranges[index].invocations.push(invoke_at);
}
oxc_ast_visit::walk::walk_new_expression(self, expression);
}
@@ -4604,6 +4621,15 @@ impl<'a> VisitJavascript<'a> for JavascriptConditionalExecutionCollector<'_> {
self.push_span(statement.body.span());
oxc_ast_visit::walk::walk_while_statement(self, statement);
}
fn visit_switch_statement(&mut self, statement: &oxc_ast::ast::SwitchStatement<'a>) {
for case in &statement.cases {
for consequent in &case.consequent {
self.push_span(consequent.span());
}
}
oxc_ast_visit::walk::walk_switch_statement(self, statement);
}
}
fn javascript_conditional_execution_ranges(content: &str) -> Vec<std::ops::Range<usize>> {
@@ -4679,6 +4705,13 @@ fn javascript_alias_scope_effect<T>(
until: usize,
visiting: &mut BTreeSet<(usize, usize)>,
) -> JavascriptAliasSelection {
let until = scope
.and_then(|(start, end)| {
javascript_top_level_unconditional_termination_from_ast(&content[start..end])
.flatten()
.map(|termination| start + termination)
})
.map_or(until, |termination| until.min(termination));
let mut transitions = BTreeMap::<usize, Vec<JavascriptAliasSelection>>::new();
for (index, event) in events.iter().enumerate().filter(|(_, event)| {
event.scope == scope
@@ -5050,7 +5083,7 @@ impl<'a> VisitJavascript<'a> for JavascriptImportMemberCallCollector<'_> {
}
}
if let Some(initializer) = &declarator.init {
let position = declarator.span.start as usize;
let position = declarator.span.end as usize;
for (local, path) in self.resolve_expression(initializer, position) {
match &declarator.id {
JavascriptBindingPattern::BindingIdentifier(identifier) => {
@@ -5131,7 +5164,7 @@ impl<'a> VisitJavascript<'a> for JavascriptImportMemberCallCollector<'_> {
if let oxc_ast::ast::AssignmentTarget::AssignmentTargetIdentifier(identifier) =
&assignment.left
{
let position = assignment.span.start as usize;
let position = assignment.span.end as usize;
if let Some(symbol_id) = identifier
.reference_id
.get()
@@ -5337,10 +5370,10 @@ impl<'a> VisitJavascript<'a> for JavascriptDynamicImportUsageCollector<'_> {
symbol_id,
source,
import_position,
assignment.span.start as usize,
assignment.span.end as usize,
);
} else {
let position = assignment.span.start as usize;
let position = assignment.span.end as usize;
self.namespace_sources.entry(symbol_id).or_default().push(
JavascriptAliasEvent {
position,
@@ -9711,6 +9744,19 @@ mod javascript_projection_reachability_tests {
);
}
#[test]
fn receiver_alias_freezes_its_source_owner_at_assignment_time() {
let content = "const first = { run() { return import('./first.mjs'); } }; const second = { run() { return import('./second.mjs'); } }; let source = first; const receiver = source; source = second; receiver.run();";
let ranges = named_javascript_function_ranges(content);
let first = content.find("./first.mjs").expect("find original owner");
let second = content
.find("./second.mjs")
.expect("find later source owner");
assert!(javascript_position_is_reachable(content, &ranges, first));
assert!(!javascript_position_is_reachable(content, &ranges, second));
}
#[test]
fn local_constructor_body_is_reachable_from_new_expression() {
let content = "class Helper { constructor() { return import('./constructor.mjs'); } } function start() { return new Helper(); } start();";
@@ -3885,6 +3885,75 @@ fn javascript_alias_events_follow_function_invocation_time() {
);
}
#[test]
fn javascript_switch_assignments_preserve_all_possible_aliases() {
let source = "import * as initial from './initial.mjs'; import * as first from './first.mjs'; import * as fallback from './fallback.mjs'; let facade = initial; switch (mode) { case 1: facade = first; break; default: facade = fallback; } facade.run();";
let analysis = super::autonomous_completion::javascript_module_analysis(source, true)
.expect("analyze switch alias branches");
for binding in ["initial", "first", "fallback"] {
assert!(
analysis.import_member_calls.contains_key(binding),
"switch must retain possible receiver {binding}: {:#?}",
analysis.import_member_calls
);
}
}
#[test]
fn javascript_function_alias_effect_stops_after_unconditional_exit() {
for exit in ["return", "throw failure"] {
let source = format!(
"import * as initial from './initial.mjs'; import * as selected from './selected.mjs'; import * as unreachable from './unreachable.mjs'; let facade = initial; function mutate() {{ facade = selected; {exit}; facade = unreachable; }} mutate(); facade.run();"
);
let analysis = super::autonomous_completion::javascript_module_analysis(&source, true)
.expect("analyze alias effect before unconditional exit");
assert!(analysis.import_member_calls.contains_key("selected"));
assert!(
!analysis.import_member_calls.contains_key("unreachable"),
"alias effect after {exit} must be unreachable"
);
}
}
#[test]
fn javascript_nested_call_alias_effects_follow_javascript_evaluation_order() {
let nested = "import * as initial from './initial.mjs'; import * as innerValue from './inner.mjs'; import * as outerValue from './outer.mjs'; let facade = initial; function inner() { facade = innerValue; } function outer() { facade = outerValue; } outer(inner()); facade.run();";
let analysis = super::autonomous_completion::javascript_module_analysis(nested, true)
.expect("analyze nested call evaluation order");
assert!(analysis.import_member_calls.contains_key("outerValue"));
assert!(!analysis.import_member_calls.contains_key("innerValue"));
let assignment = "import * as initial from './initial.mjs'; import * as transient from './transient.mjs'; let receiver = initial; function mutate() { receiver = transient; return null; } receiver = mutate(); receiver.run();";
let analysis = super::autonomous_completion::javascript_module_analysis(assignment, true)
.expect("analyze assignment RHS evaluation order");
assert!(
!analysis.import_member_calls.contains_key("transient"),
"the assignment result must override its RHS call's alias side effect"
);
}
#[test]
fn javascript_constant_conditions_are_case_and_scope_sensitive() {
for condition in ["FALSE", "TRUE", "undefined"] {
let declaration = if condition == "undefined" {
"export function start(undefined) {"
} else {
"export function start() {"
};
let source = format!(
"import * as initial from './initial.mjs'; import * as selected from './selected.mjs'; {declaration} let facade = initial; if ({condition}) facade = selected; facade.run(); }}"
);
let analysis = super::autonomous_completion::javascript_module_analysis(&source, true)
.expect("analyze case- and scope-sensitive condition");
for binding in ["initial", "selected"] {
assert!(
analysis.import_member_calls.contains_key(binding),
"{condition} must remain an unknown condition and preserve {binding}"
);
}
}
}
#[test]
fn game_chat_pure_continue_does_not_inherit_across_sessions() {
let (_temporary, root, original_state, original_contract) =
@@ -5987,3 +5987,4 @@
- 浏览器因果:状态证据仍只冻结 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 重命名不能改写原始身份;删除回流声明前先把引用改接到 importer 已有 canonical,有界轮次未收敛时失败关闭。inline module 与外部脚本共同占用累计 `2 MiB` 源码预算。
- JavaScript / ESM alias 求值顺序补充:receiver alias 保存赋值完成时刻并在该时刻解析 source owner,后续 source 重赋值不得倒灌。调用事件按内层参数 / RHS 先于外层调用 / assignment 生效;`switch case/default` 赋值一律保留跳过与各分支可能状态,普通函数内无条件 `return / throw` 截断之后的 alias 副作用。恒真 / 恒假关键字大小写敏感,可能被局部或参数遮蔽的 `undefined` 不再作为文本恒假值。
@@ -834,7 +834,7 @@ 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,恒假分支、未调用函数或调用之后的赋值不能倒灌覆盖更早使用点。未知条件循环内赋值保留执行与跳过两种状态,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 定义位置冻结。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` 不能触发参数执行推断。
- 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` 不能触发参数执行推断。
- 泥点不足是确定性业务中断,不是瞬态 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 重放,禁止分别执行原响应和持久响应。