修复循环模块回接与内联源码限额

循环投影声明去重后继续把 import 引用回接到既有 canonical,并补足稳定确认轮次。

内联模块先按原文累计 2 MiB 源码预算,再执行语法和语义失败关闭。

新增 self-cycle、无效超限与无效小型内联模块回归,并同步技术方案与共享决策。
This commit is contained in:
2026-08-04 13:45:29 +08:00
parent 3863d95456
commit d574732156
4 changed files with 71 additions and 17 deletions
@@ -4152,9 +4152,7 @@ fn executable_inline_module_javascript_units_from_html(content: &str) -> Vec<Str
&& !html_has_attribute(tag, "src")
{
let body = &content[body_start..close_start];
if javascript_is_syntactically_valid(body, true) {
executable.push(body.to_string());
}
executable.push(body.to_string());
}
cursor = close_end;
}
@@ -7671,7 +7669,7 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
let mut projected_units = BTreeMap::<String, String>::new();
let mut projection_work_bytes = 0usize;
let mut projection_converged = false;
for _ in 0..module_bindings.len().max(1) {
for _ in 0..module_bindings.len().saturating_add(1).max(1) {
let mut round_updates = BTreeMap::<String, String>::new();
let mut round_identity_updates =
BTreeMap::<String, BTreeMap<String, JavascriptProjectionDeclarationIdentity>>::new();
@@ -7835,6 +7833,7 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
};
selected_projection_identities.insert(binding.clone(), identity.clone());
}
let projection_identities_before_dedup = selected_projection_identities.clone();
if !javascript_remove_duplicate_projection_declarations(
&mut projection,
&mut selected_projection_identities,
@@ -7844,9 +7843,15 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
"自主构建循环模块投影声明范围无效:{importer} <- {origin}"
));
}
if projection.trim().is_empty() {
continue;
}
let existing_projected_binding_names = projection_identities_before_dedup
.iter()
.filter(|(binding, _)| !selected_projection_identities.contains_key(*binding))
.filter_map(|(binding, identity)| {
included_projection_names
.get(identity)
.map(|canonical| (binding.clone(), canonical.clone()))
})
.collect::<BTreeMap<_, _>>();
projection_work_bytes = projection_work_bytes
.checked_add(projection.len())
.filter(|bytes| {
@@ -7895,7 +7900,8 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
.difference(&projection_analysis.root_bindings)
.cloned(),
);
let mut projected_binding_names = BTreeMap::<String, String>::new();
let mut projected_binding_names = existing_projected_binding_names;
let mut newly_projected_binding_names = BTreeMap::<String, String>::new();
for original in &projection_analysis.root_bindings {
remaining_projection.remove(original);
let preferred = preferred_synthetic_alias
@@ -7909,18 +7915,18 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
&remaining_projection,
);
assigned_names.insert(canonical.clone());
projected_binding_names.insert(original.clone(), canonical);
newly_projected_binding_names.insert(original.clone(), canonical);
}
for (original, canonical) in &projected_binding_names {
for (original, canonical) in &newly_projected_binding_names {
if original != canonical
&& !rename_javascript_root_binding(&mut projection, original, canonical)
{
return Err(format!(
"自主构建模块投影绑定无法按符号重命名:{origin}::{original} -> {canonical}"
));
));
}
}
for (original, canonical) in &projected_binding_names {
for (original, canonical) in &newly_projected_binding_names {
let Some(identity) = selected_projection_identities.get(original) else {
return Err(format!(
"自主构建模块投影重命名后缺少声明身份:{origin}::{original}"
@@ -7929,6 +7935,7 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
included_projection_names.insert(identity.clone(), canonical.clone());
unit_projection_identities.insert(canonical.clone(), identity.clone());
}
projected_binding_names.extend(newly_projected_binding_names);
if javascript_module_analysis(&projection, true).is_none() {
return Err(format!("自主构建模块投影重命名后语义无效:{origin}"));
}
@@ -8030,9 +8037,14 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
}
}
}
unit.push('\n');
unit.push_str(&projection);
added_projection = true;
if !projection.trim().is_empty() {
unit.push('\n');
unit.push_str(&projection);
added_projection = true;
}
if !unit_replacements.is_empty() {
added_projection = true;
}
}
if added_projection {
if !apply_javascript_span_replacements(&mut unit, &mut unit_replacements) {
@@ -3806,6 +3806,29 @@ fn javascript_projection_preserves_nested_names_shorthand_keys_and_cycles() {
assert!(!projected.contains("__agc_import_2"));
assert!(projected.contains("return a();"));
assert!(javascript_is_syntactically_valid(projected, true));
fs::write(
root.join("game/self.mjs"),
"import { start as again } from './self.mjs'; export function start() { return 1; } again();",
)
.expect("write self-cyclic module");
let self_cycle_html = "<script type=\"module\" src=\"./self.mjs\"></script><canvas></canvas>";
let self_cycle = read_external_gameplay_javascript_at(root, self_cycle_html)
.expect("a self-cycle must reconnect its import to the existing declaration");
let projected = self_cycle
.module_units()
.iter()
.find(|unit| unit.contains("function start()") && !unit.contains("import {"))
.unwrap_or_else(|| {
panic!(
"self-cycle must emit an import-free projected unit: {:#?}",
self_cycle.module_units()
)
});
assert_eq!(projected.matches("function start()").count(), 1);
assert!(projected.contains("start();"));
assert!(!projected.contains("again();"));
assert!(javascript_is_syntactically_valid(projected, true));
}
#[test]
@@ -3820,6 +3843,25 @@ fn javascript_inline_modules_share_the_external_script_byte_limit() {
error.contains("累计超过 2 MiB"),
"unexpected error: {error}"
);
let invalid_oversized = format!(
"<script type=\"module\">{}const =</script>",
" ".repeat(2 * 1024 * 1024)
);
let error = read_external_gameplay_javascript_at(temporary.path(), &invalid_oversized)
.expect_err("an invalid oversized inline module must fail at the byte boundary");
assert!(
error.contains("累计超过 2 MiB"),
"oversized invalid modules must not be filtered before accounting: {error}"
);
let invalid = "<script type=\"module\">const =</script>";
let error = read_external_gameplay_javascript_at(temporary.path(), invalid)
.expect_err("an invalid inline module must fail closed");
assert!(
error.contains("内联模块不是有效 JavaScriptinline-module:0"),
"unexpected invalid inline module error: {error}"
);
}
#[test]
@@ -5988,5 +5988,5 @@
- 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 执行。
- 浏览器因果:状态证据仍只冻结 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 循环与体积补充:投影声明按 `(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` 不再作为文本恒假值。
@@ -839,7 +839,7 @@ game-project/
- 泥点不足是确定性业务中断,不是瞬态 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 重放,禁止分别执行原响应和持久响应。
- 循环 ESM 的投影身份固定为 `(origin module, original root binding)`;canonical 重命名只改变组合单元中的展示名,身份随投影闭包传播。删除已存在的回流声明前,必须先把其引用改接到 importer 中同一身份的既有 canonical;固定点未在模块数限定轮次内收敛时失败关闭,不能返回最后一轮仍变化的部分结果。inline module 与外部脚本共同计入累计源码 `2 MiB` 上限,不能绕过文件读取预算
- 循环 ESM 的投影身份固定为 `(origin module, original root binding)`;canonical 重命名只改变组合单元中的展示名,身份随投影闭包传播。删除已存在的回流声明后仍必须把 import 引用改接到 importer 中同一身份的既有 canonical;固定点未在模块数 + 1”轮内收敛时失败关闭,不能返回最后一轮仍变化的部分结果。inline module 必须先提取原文并计入与外部脚本共享的累计源码 `2 MiB` 上限,再执行语法和语义校验;超限源码不能因无效语法被过滤,小型无效模块也必须明确失败关闭
## 2026-07-31 长耗时与恢复收口