修复可调用回调与视觉脚本可达性

合并条件表达式两端可调用身份并保留成员 bind 赋值结果

穿透回调调用括号并校正 Promise 回调参数槽

使用原始大小写脚本计算视觉资产语义可达性

补充回归测试与 Runtime 技术决策文档
This commit is contained in:
2026-08-04 13:48:58 +08:00
parent d574732156
commit e37945e306
3 changed files with 242 additions and 52 deletions
@@ -1358,6 +1358,11 @@ enum JavascriptCallableValue {
source: JavascriptSymbolId,
captured_at: usize,
},
Member {
receiver: JavascriptSymbolId,
name: String,
captured_at: usize,
},
}
impl JavascriptMethodReceiverOwnerCollector<'_> {
@@ -1445,40 +1450,64 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
}
}
fn callable_value(
fn callable_values(
&self,
expression: &JavascriptExpression<'_>,
position: usize,
) -> Option<JavascriptCallableValue> {
) -> Vec<JavascriptCallableValue> {
match expression {
JavascriptExpression::FunctionExpression(_)
| JavascriptExpression::ArrowFunctionExpression(_) => {
let span = expression.span();
Some(JavascriptCallableValue::Direct((
vec![JavascriptCallableValue::Direct((
span.start as usize,
span.end as usize,
)))
}
JavascriptExpression::Identifier(_) => {
self.referenced_symbol(expression)
.map(|source| JavascriptCallableValue::Alias {
source,
captured_at: position,
})
))]
}
JavascriptExpression::Identifier(_) => self
.referenced_symbol(expression)
.map(|source| JavascriptCallableValue::Alias {
source,
captured_at: position,
})
.into_iter()
.collect(),
JavascriptExpression::ParenthesizedExpression(parenthesized) => {
self.callable_value(&parenthesized.expression, position)
self.callable_values(&parenthesized.expression, position)
}
JavascriptExpression::ConditionalExpression(conditional) => {
let mut values = self.callable_values(&conditional.consequent, position);
values.extend(self.callable_values(&conditional.alternate, position));
values
}
JavascriptExpression::CallExpression(call) => call
.callee
.as_member_expression()
.filter(|member| member.static_property_name() == Some("bind"))
.and_then(|member| self.referenced_symbol(member.object()))
.map(|source| JavascriptCallableValue::Alias {
source,
captured_at: position,
}),
_ => None,
.into_iter()
.flat_map(|bind| {
if let Some(source) = self.referenced_symbol(bind.object()) {
return vec![JavascriptCallableValue::Alias {
source,
captured_at: position,
}];
}
bind.object()
.as_member_expression()
.and_then(|member| {
self.referenced_symbol(member.object())
.zip(member.static_property_name().map(str::to_string))
})
.map(|(receiver, name)| JavascriptCallableValue::Member {
receiver,
name,
captured_at: position,
})
.into_iter()
.collect()
})
.collect(),
_ => Vec::new(),
}
}
@@ -1495,19 +1524,29 @@ impl JavascriptMethodReceiverOwnerCollector<'_> {
) {
return;
}
let value = self.callable_value(expression, position);
self.callable_events
.entry(symbol_id)
.or_default()
.push(JavascriptAliasEvent {
let values = self.callable_values(expression, position);
let events = self.callable_events.entry(symbol_id).or_default();
if values.is_empty() {
events.push(JavascriptAliasEvent {
position,
scope: javascript_alias_scope_at(self.ranges, position),
value,
value: None,
conditional: javascript_position_is_conditionally_executed(
self.conditional_ranges,
position,
),
});
} else {
events.extend(values.into_iter().map(|value| JavascriptAliasEvent {
position,
scope: javascript_alias_scope_at(self.ranges, position),
value: Some(value),
conditional: javascript_position_is_conditionally_executed(
self.conditional_ranges,
position,
),
}));
}
}
fn receiver_values(
@@ -1613,9 +1652,9 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> {
initializer,
JavascriptExpression::FunctionExpression(_)
| JavascriptExpression::ArrowFunctionExpression(_)
) && self
.callable_value(initializer, declarator.span.start as usize)
.is_some()
) && !self
.callable_values(initializer, declarator.span.start as usize)
.is_empty()
{
self.record_callable_assignment(
symbol_id,
@@ -1690,6 +1729,9 @@ fn javascript_expression_is_known_event_target(
scoping: &JavascriptScoping,
) -> bool {
match expression {
JavascriptExpression::ParenthesizedExpression(parenthesized) => {
javascript_expression_is_known_event_target(&parenthesized.expression, scoping)
}
JavascriptExpression::Identifier(identifier) => {
javascript_identifier_is_unresolved_known_global(
identifier,
@@ -1722,6 +1764,7 @@ fn javascript_known_callback_argument_indices(
) -> &'static [usize] {
const NONE: &[usize] = &[];
const FIRST: &[usize] = &[0];
const FIRST_AND_SECOND: &[usize] = &[0, 1];
const SECOND: &[usize] = &[1];
let unresolved_global = |identifier: &oxc_ast::ast::IdentifierReference<'_>| {
identifier
@@ -1729,7 +1772,11 @@ fn javascript_known_callback_argument_indices(
.get()
.is_some_and(|reference_id| scoping.get_reference(reference_id).symbol_id().is_none())
};
if let JavascriptExpression::Identifier(identifier) = &call.callee {
let mut callee = &call.callee;
while let JavascriptExpression::ParenthesizedExpression(parenthesized) = callee {
callee = &parenthesized.expression;
}
if let JavascriptExpression::Identifier(identifier) = callee {
if !unresolved_global(identifier) {
return NONE;
}
@@ -1739,15 +1786,23 @@ fn javascript_known_callback_argument_indices(
_ => NONE,
};
}
let Some(member) = call.callee.as_member_expression() else {
let Some(member) = callee.as_member_expression() else {
return NONE;
};
let Some(name) = member.static_property_name() else {
return NONE;
};
let mut receiver = member.object();
while let JavascriptExpression::ParenthesizedExpression(parenthesized) = receiver {
receiver = &parenthesized.expression;
}
if matches!(name, "catch" | "finally" | "then") {
return if matches!(member.object(), JavascriptExpression::ImportExpression(_)) {
FIRST
return if matches!(receiver, JavascriptExpression::ImportExpression(_)) {
if name == "then" {
FIRST_AND_SECOND
} else {
FIRST
}
} else {
NONE
};
@@ -1756,7 +1811,7 @@ fn javascript_known_callback_argument_indices(
name,
"every" | "filter" | "find" | "forEach" | "map" | "reduce" | "some"
) {
return if matches!(member.object(), JavascriptExpression::ArrayExpression(_)) {
return if matches!(receiver, JavascriptExpression::ArrayExpression(_)) {
FIRST
} else {
NONE
@@ -1771,10 +1826,10 @@ fn javascript_known_callback_argument_indices(
| "setTimeout"
) {
let known_receiver = if name == "addEventListener" {
javascript_expression_is_known_event_target(member.object(), scoping)
javascript_expression_is_known_event_target(receiver, scoping)
} else {
matches!(
member.object(),
receiver,
JavascriptExpression::Identifier(identifier)
if javascript_identifier_is_unresolved_known_global(
identifier,
@@ -1854,6 +1909,23 @@ impl JavascriptFunctionInvocationCollector<'_, '_> {
*captured_at,
visiting,
)),
JavascriptCallableValue::Member {
receiver,
name,
captured_at,
} => {
for owner in self.receiver_owners_for_symbol(
*receiver,
*captured_at,
&mut BTreeSet::new(),
) {
indices.extend(self.method_indices_for_owner(
owner,
name,
&mut BTreeSet::new(),
));
}
}
}
}
}
@@ -2737,11 +2809,12 @@ fn game_index_visibly_uses_visual_asset(
let Ok(html) = std::str::from_utf8(html) else {
return false;
};
let content = strip_art_reference_comments(html).to_ascii_lowercase();
if !content.contains(&asset_path) {
let original_content = strip_art_reference_comments(html);
let heuristic_content = original_content.to_ascii_lowercase();
if !heuristic_content.contains(&asset_path) {
return false;
}
let markup = strip_script_blocks(&content);
let markup = strip_script_blocks(&heuristic_content);
let mut tag_cursor = 0;
while let Some(start_offset) = markup[tag_cursor..].find('<') {
@@ -2806,28 +2879,31 @@ fn game_index_visibly_uses_visual_asset(
}) else {
return false;
};
let identifiers = canvas_visual_identifiers(&content, &markup, &asset_path);
let identifiers = canvas_visual_identifiers(&heuristic_content, &markup, &asset_path);
if identifiers.is_empty() {
return false;
}
let javascript = executable_javascript_from_html(&content);
let original_javascript = executable_javascript_from_html(&original_content);
let heuristic_javascript = original_javascript.to_ascii_lowercase();
let mut significant_draws = 0usize;
let function_ranges = named_javascript_function_ranges(&javascript);
let function_ranges = named_javascript_function_ranges(&original_javascript);
let mut draw_cursor = 0;
while let Some(offset) = javascript[draw_cursor..].find("drawimage(") {
while let Some(offset) = heuristic_javascript[draw_cursor..].find("drawimage(") {
let call = draw_cursor + offset;
let arguments_start = call + "drawimage(".len();
draw_cursor = arguments_start;
if position_is_inside_javascript_string(&javascript, call)
|| !javascript_position_is_reachable(&javascript, &function_ranges, call)
if position_is_inside_javascript_string(&heuristic_javascript, call)
|| !javascript_position_is_reachable(&original_javascript, &function_ranges, call)
{
continue;
}
let Some(arguments_end) = javascript_call_arguments_end(&javascript, arguments_start)
let Some(arguments_end) =
javascript_call_arguments_end(&heuristic_javascript, arguments_start)
else {
break;
};
let arguments = split_javascript_arguments(&javascript[arguments_start..arguments_end]);
let arguments =
split_javascript_arguments(&heuristic_javascript[arguments_start..arguments_end]);
if arguments
.first()
.is_some_and(|identifier| identifiers.contains(*identifier))
@@ -9961,6 +10037,33 @@ pub(in crate::agent) fn autonomous_game_build_completion_blocker_at_locked(
mod visible_destination_tests {
use super::*;
fn mixed_case_main_loop_html(invocation: &str) -> Vec<u8> {
format!(
"<!doctype html><html><body><canvas width=320 height=180></canvas><script>const context=document.querySelector('canvas').getContext('2d');const PlayerArt=new Image();PlayerArt.src='../assets/art-spec.png';function MainLoop(){{context.drawImage(PlayerArt,0,0,64,64);}}function mainloop(){{return 0;}}{invocation}();</script></body></html>"
)
.into_bytes()
}
#[test]
fn visual_asset_reachability_keeps_original_identifier_case_for_called_main_loop() {
assert!(game_index_visibly_uses_visual_asset(
&mixed_case_main_loop_html("MainLoop"),
"assets/art-spec.png",
(64, 64),
VisualAssetUsageRequirement::CanvasDraw,
));
}
#[test]
fn visual_asset_reachability_does_not_merge_differently_cased_main_loops() {
assert!(!game_index_visibly_uses_visual_asset(
&mixed_case_main_loop_html("mainloop"),
"assets/art-spec.png",
(64, 64),
VisualAssetUsageRequirement::CanvasDraw,
));
}
#[test]
fn canvas_draw_destination_rejects_offscreen_and_unbounded_dynamic_coordinates() {
let canvas = (320.0, 180.0);
@@ -10183,6 +10286,29 @@ mod javascript_projection_reachability_tests {
}
}
#[test]
fn conditional_expression_preserves_both_possible_callables() {
for (content, sources) in [
(
"function first() { return import('./conditional-first.mjs'); } function second() { return import('./conditional-second.mjs'); } const start = flag ? first : second; start();",
["./conditional-first.mjs", "./conditional-second.mjs"],
),
(
"function first() { return import('./assigned-first.mjs'); } function second() { return import('./assigned-second.mjs'); } let start; start = flag ? first : second; start();",
["./assigned-first.mjs", "./assigned-second.mjs"],
),
] {
let ranges = named_javascript_function_ranges(content);
for source in sources {
let position = content.find(source).expect("find conditional callable import");
assert!(
javascript_position_is_reachable(content, &ranges, position),
"both conditional callable branches must remain reachable: {source}"
);
}
}
}
#[test]
fn local_function_alias_preserves_the_call_edge() {
let content = "function start() { return import('./aliased-function.mjs'); } const alias = start; alias();";
@@ -10246,6 +10372,26 @@ mod javascript_projection_reachability_tests {
}
}
#[test]
fn member_bind_results_remain_callable_after_declaration_or_assignment() {
for invocation in [
"const bound = api.run.bind(api); bound();",
"let bound; bound = api.run.bind(api); bound();",
] {
let content = format!(
"const api = {{ run() {{ return import('./member-bind.mjs'); }} }}; {invocation}"
);
let ranges = named_javascript_function_ranges(&content);
let position = content
.find("./member-bind.mjs")
.expect("find bound member import");
assert!(
javascript_position_is_reachable(&content, &ranges, position),
"a bound local member must preserve its callable identity: {invocation}"
);
}
}
#[test]
fn named_function_expression_uses_its_outer_binding_as_one_range() {
let content =
@@ -10353,11 +10499,13 @@ mod javascript_projection_reachability_tests {
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")],
);
let callback = format!("./promise-{api}-callback.mjs");
let extra = format!("./promise-{api}-extra.mjs");
if api == "then" {
assert_callback_reachability(&content, &[&callback, &extra], &[]);
} else {
assert_callback_reachability(&content, &[&callback], &[&extra]);
}
}
for api in [
"map", "filter", "every", "find", "forEach", "some", "reduce",
@@ -10373,6 +10521,47 @@ mod javascript_projection_reachability_tests {
}
}
#[test]
fn controlled_callback_apis_unwrap_parenthesized_callees_and_receivers() {
assert_callback_reachability(
"function callback(){return import('./parenthesized-timer.mjs')} (setTimeout)(callback, 0);",
&["./parenthesized-timer.mjs"],
&[],
);
assert_callback_reachability(
"function callback(){return import('./parenthesized-array.mjs')} ([1]).map(callback);",
&["./parenthesized-array.mjs"],
&[],
);
assert_callback_reachability(
"function fulfilled(){return import('./parenthesized-import-fulfilled.mjs')} function rejected(){return import('./parenthesized-import-rejected.mjs')} (import('./seed.mjs')).then(fulfilled, rejected);",
&[
"./parenthesized-import-fulfilled.mjs",
"./parenthesized-import-rejected.mjs",
],
&[],
);
}
#[test]
fn promise_then_executes_both_callbacks_but_catch_and_finally_only_the_first() {
assert_callback_reachability(
"function fulfilled(){return import('./then-fulfilled.mjs')} function rejected(){return import('./then-rejected.mjs')} import('./seed.mjs').then(fulfilled, rejected);",
&["./then-fulfilled.mjs", "./then-rejected.mjs"],
&[],
);
for api in ["catch", "finally"] {
let content = format!(
"function callback(){{return import('./{api}-callback.mjs')}} function extra(){{return import('./{api}-extra.mjs')}} import('./seed.mjs').{api}(callback, extra);"
);
assert_callback_reachability(
&content,
&[&format!("./{api}-callback.mjs")],
&[&format!("./{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);";
@@ -10381,7 +10570,7 @@ mod javascript_projection_reachability_tests {
.import_member_calls
.get("api")
.expect("record controlled callback member demands");
for callback in ["timer", "listener", "fulfilled", "reducer"] {
for callback in ["timer", "listener", "fulfilled", "rejected", "reducer"] {
assert!(
members
.keys()
@@ -10393,7 +10582,6 @@ mod javascript_projection_reachability_tests {
"delay",
"eventType",
"options",
"rejected",
"initialValue",
"wrongCase",
"wrongMethodCase",
@@ -5986,6 +5986,7 @@
- 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 静默过滤,无效小模块也明确失败关闭。
@@ -836,6 +836,7 @@ game-project/
- 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` 合并。
- 泥点不足是确定性业务中断,不是瞬态 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 重放,禁止分别执行原响应和持久响应。