修复循环模块投影身份与内联体积门禁
按来源模块和原始根绑定传播投影声明身份 回流去重前把引用改接到既有 canonical 并拒绝未收敛固定点 将 inline module 纳入累计二 MiB JavaScript 预算 新增双端命名捕获循环与内联超限回归 同步技术方案与共享决策记录
This commit is contained in:
+118
-11
@@ -6423,26 +6423,74 @@ pub(in crate::agent) fn javascript_module_binding_projection(
|
||||
projection
|
||||
}
|
||||
|
||||
type JavascriptProjectionDeclarationIdentity = (String, String);
|
||||
|
||||
fn javascript_projection_declaration_identities(
|
||||
module: &str,
|
||||
analysis: &JavascriptModuleAnalysis,
|
||||
) -> BTreeMap<String, JavascriptProjectionDeclarationIdentity> {
|
||||
analysis
|
||||
.root_bindings
|
||||
.iter()
|
||||
.chain(analysis.synthetic_declarations.keys())
|
||||
.map(|binding| (binding.clone(), (module.to_string(), binding.clone())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn javascript_remove_duplicate_projection_declarations(
|
||||
projection: &mut String,
|
||||
importer: &str,
|
||||
projection_identities: &mut BTreeMap<String, JavascriptProjectionDeclarationIdentity>,
|
||||
included_names: &BTreeMap<JavascriptProjectionDeclarationIdentity, String>,
|
||||
) -> bool {
|
||||
let projection_ranges = javascript_top_level_declaration_ranges(projection);
|
||||
let importer_ranges = javascript_top_level_declaration_ranges(importer);
|
||||
let mut duplicate_ranges = BTreeSet::new();
|
||||
let mut declaration_bindings = BTreeMap::<(usize, usize), Vec<String>>::new();
|
||||
for (binding, projection_range) in &projection_ranges {
|
||||
let Some(importer_range) = importer_ranges.get(binding) else {
|
||||
continue;
|
||||
};
|
||||
if projection.get(projection_range.clone()) == importer.get(importer_range.clone()) {
|
||||
duplicate_ranges.insert((projection_range.start, projection_range.end));
|
||||
}
|
||||
declaration_bindings
|
||||
.entry((projection_range.start, projection_range.end))
|
||||
.or_default()
|
||||
.push(binding.clone());
|
||||
}
|
||||
let duplicate_bindings = declaration_bindings
|
||||
.values()
|
||||
.filter(|bindings| {
|
||||
bindings.iter().all(|binding| {
|
||||
projection_identities
|
||||
.get(binding)
|
||||
.is_some_and(|identity| included_names.contains_key(identity))
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let mut renamed_duplicates = BTreeSet::new();
|
||||
for binding in &duplicate_bindings {
|
||||
let Some(identity) = projection_identities.get(binding) else {
|
||||
return false;
|
||||
};
|
||||
let Some(existing) = included_names.get(identity) else {
|
||||
return false;
|
||||
};
|
||||
if binding != existing && !rename_javascript_root_binding(projection, binding, existing) {
|
||||
return false;
|
||||
}
|
||||
renamed_duplicates.insert(existing.clone());
|
||||
}
|
||||
let duplicate_ranges = javascript_top_level_declaration_ranges(projection)
|
||||
.into_iter()
|
||||
.filter(|(binding, _)| renamed_duplicates.contains(binding))
|
||||
.map(|(_, range)| (range.start, range.end))
|
||||
.collect::<BTreeSet<_>>();
|
||||
let mut replacements = duplicate_ranges
|
||||
.into_iter()
|
||||
.map(|(start, end)| (start..end, String::new()))
|
||||
.collect::<Vec<_>>();
|
||||
apply_javascript_span_replacements(projection, &mut replacements)
|
||||
if !apply_javascript_span_replacements(projection, &mut replacements) {
|
||||
return false;
|
||||
}
|
||||
for binding in duplicate_bindings {
|
||||
projection_identities.remove(&binding);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn apply_javascript_span_replacements(
|
||||
@@ -6827,6 +6875,11 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
|
||||
.enumerate()
|
||||
{
|
||||
let inline_id = format!("inline-module:{index}");
|
||||
let inline_bytes = u64::try_from(inline_module.len()).unwrap_or(u64::MAX);
|
||||
total_bytes = total_bytes
|
||||
.checked_add(inline_bytes)
|
||||
.filter(|bytes| *bytes <= MAX_EXTERNAL_GAMEPLAY_JAVASCRIPT_BYTES)
|
||||
.ok_or_else(|| "自主构建外部脚本累计超过 2 MiB".to_string())?;
|
||||
let analysis = javascript_module_analysis(&inline_module, true)
|
||||
.ok_or_else(|| format!("自主构建内联模块不是有效 JavaScript:{inline_id}"))?;
|
||||
let analysis = normalize_javascript_module_analysis_sources("game/index.html", analysis)?;
|
||||
@@ -7156,10 +7209,24 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
|
||||
.module_units
|
||||
.extend(module_contents.values().cloned());
|
||||
let mut projection_contents = module_contents.clone();
|
||||
let original_projection_identities = module_contents
|
||||
.iter()
|
||||
.map(|(module, _)| {
|
||||
let identities = module_analyses
|
||||
.get(module)
|
||||
.map(|analysis| javascript_projection_declaration_identities(module, analysis))
|
||||
.unwrap_or_default();
|
||||
(module.clone(), identities)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let mut projection_identities = original_projection_identities.clone();
|
||||
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) {
|
||||
let mut round_updates = BTreeMap::<String, String>::new();
|
||||
let mut round_identity_updates =
|
||||
BTreeMap::<String, BTreeMap<String, JavascriptProjectionDeclarationIdentity>>::new();
|
||||
for (importer, dependencies) in &module_bindings {
|
||||
let Some(importer_content) = module_contents.get(importer) else {
|
||||
continue;
|
||||
@@ -7284,10 +7351,23 @@ 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::<BTreeMap<_, _>>();
|
||||
for (origin, bindings) in origins {
|
||||
let Some(origin_content) = projection_contents.get(&origin) else {
|
||||
continue;
|
||||
};
|
||||
let Some(origin_projection_identities) = projection_identities.get(&origin) else {
|
||||
return Err(format!(
|
||||
"自主构建模块投影缺少声明身份:{importer} <- {origin}"
|
||||
));
|
||||
};
|
||||
let used_names = bindings
|
||||
.iter()
|
||||
.map(|(exported, _, _, _)| exported.clone())
|
||||
@@ -7297,9 +7377,20 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
|
||||
if projection.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let projection_ranges = javascript_top_level_declaration_ranges(&projection);
|
||||
let mut selected_projection_identities = BTreeMap::new();
|
||||
for binding in projection_ranges.keys() {
|
||||
let Some(identity) = origin_projection_identities.get(binding) else {
|
||||
return Err(format!(
|
||||
"自主构建模块投影声明身份无法解析:{origin}::{binding}"
|
||||
));
|
||||
};
|
||||
selected_projection_identities.insert(binding.clone(), identity.clone());
|
||||
}
|
||||
if !javascript_remove_duplicate_projection_declarations(
|
||||
&mut projection,
|
||||
importer_content,
|
||||
&mut selected_projection_identities,
|
||||
&included_projection_names,
|
||||
) {
|
||||
return Err(format!(
|
||||
"自主构建循环模块投影声明范围无效:{importer} <- {origin}"
|
||||
@@ -7381,6 +7472,15 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
|
||||
));
|
||||
}
|
||||
}
|
||||
for (original, canonical) in &projected_binding_names {
|
||||
let Some(identity) = selected_projection_identities.get(original) else {
|
||||
return Err(format!(
|
||||
"自主构建模块投影重命名后缺少声明身份:{origin}::{original}"
|
||||
));
|
||||
};
|
||||
included_projection_names.insert(identity.clone(), canonical.clone());
|
||||
unit_projection_identities.insert(canonical.clone(), identity.clone());
|
||||
}
|
||||
if javascript_module_analysis(&projection, true).is_none() {
|
||||
return Err(format!("自主构建模块投影重命名后语义无效:{origin}"));
|
||||
}
|
||||
@@ -7507,9 +7607,11 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
|
||||
"自主构建模块投影累计处理超过 32 MiB,已拒绝继续展开".to_string()
|
||||
})?;
|
||||
round_updates.insert(importer.clone(), unit);
|
||||
round_identity_updates.insert(importer.clone(), unit_projection_identities);
|
||||
}
|
||||
}
|
||||
if round_updates.is_empty() {
|
||||
projection_converged = true;
|
||||
break;
|
||||
}
|
||||
let mut changed = false;
|
||||
@@ -7520,10 +7622,15 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
|
||||
}
|
||||
projected_units.insert(importer, unit);
|
||||
}
|
||||
projection_identities.extend(round_identity_updates);
|
||||
if !changed {
|
||||
projection_converged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !projection_converged {
|
||||
return Err("自主构建模块投影未在有界轮次内收敛".to_string());
|
||||
}
|
||||
output.module_units.extend(projected_units.into_values());
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
+22
-4
@@ -3772,12 +3772,12 @@ fn javascript_projection_preserves_nested_names_shorthand_keys_and_cycles() {
|
||||
|
||||
fs::write(
|
||||
root.join("game/a.mjs"),
|
||||
"import { b } from './b.mjs'; export function a() { return b(); }",
|
||||
"import { b } from './b.mjs'; function decoyA() { const b = 0; return b; } export function a() { return b(); }",
|
||||
)
|
||||
.expect("write first cyclic module");
|
||||
fs::write(
|
||||
root.join("game/b.mjs"),
|
||||
"import { a } from './a.mjs'; export function b() { return a(); }",
|
||||
"import { a } from './a.mjs'; function decoyB() { const a = 0; return a; } export function b() { return a(); }",
|
||||
)
|
||||
.expect("write second cyclic module");
|
||||
fs::write(
|
||||
@@ -3791,7 +3791,9 @@ fn javascript_projection_preserves_nested_names_shorthand_keys_and_cycles() {
|
||||
.module_units()
|
||||
.iter()
|
||||
.find(|unit| {
|
||||
unit.contains("a();") && unit.contains("function a()") && unit.contains("function b()")
|
||||
unit.contains("a();")
|
||||
&& unit.contains("function a()")
|
||||
&& unit.contains("function b__agc_import_1()")
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
@@ -3800,10 +3802,26 @@ fn javascript_projection_preserves_nested_names_shorthand_keys_and_cycles() {
|
||||
)
|
||||
});
|
||||
assert_eq!(projected.matches("function a()").count(), 1);
|
||||
assert_eq!(projected.matches("function b()").count(), 1);
|
||||
assert_eq!(projected.matches("function b__agc_import_1()").count(), 1);
|
||||
assert!(!projected.contains("__agc_import_2"));
|
||||
assert!(projected.contains("return a();"));
|
||||
assert!(javascript_is_syntactically_valid(projected, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_inline_modules_share_the_external_script_byte_limit() {
|
||||
let temporary = tempfile::tempdir().expect("create inline module limit project");
|
||||
let oversized = " ".repeat(2 * 1024 * 1024 + 1);
|
||||
let html = format!("<script type=\"module\">{oversized}</script>");
|
||||
|
||||
let error = read_external_gameplay_javascript_at(temporary.path(), &html)
|
||||
.expect_err("inline modules must count toward the 2 MiB JavaScript limit");
|
||||
assert!(
|
||||
error.contains("累计超过 2 MiB"),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_alias_events_follow_function_invocation_time() {
|
||||
let static_source = "import * as real from './real.mjs'; import * as decoy from './decoy.mjs'; let facade = real; function start() { return facade.run(); } start(); facade = decoy;";
|
||||
|
||||
@@ -5986,3 +5986,4 @@
|
||||
- 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 binding,dynamic shorthand 保留原键,循环模块按相同原始声明去重,同名 dynamic export 不得拉入无引用本地声明。
|
||||
- 浏览器因果:状态证据仍只冻结 trusted input listener 及其点击派生微任务内的变化;完整手势身份改由宿主在成功完成 Chromium 元素鼠标输入后调用隔离世界 finish。更早注册的 `window` capture listener 即使调用 `stopImmediatePropagation()` 也不能阻断探针自身的完成身份,页面脚本不能伪造 host finish,RAF / 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` 源码预算。
|
||||
|
||||
@@ -838,6 +838,8 @@ 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` 上限,不能绕过文件读取预算。
|
||||
|
||||
## 2026-07-31 长耗时与恢复收口
|
||||
|
||||
- `autonomous-game-build` 的固定 manifest DAG 是唯一缺省首轮专业执行链。缺省 collaboration policy 不再额外强制 `code-prototype / quality-review / art-*` 静态首波,Runtime 也不再按 Editor Key 或已有图片偷偷追加 Agent。2026-08-03 起,显式项目 policy 或旧 batch 恢复若进入首批 `agent.delegate` 兜底,同样只能激活 `design-director / art-director / code-director`,不得提前激活底层 Agent;这条兜底不能在默认 manifest 前复制同职责委派。
|
||||
|
||||
Reference in New Issue
Block a user