修复图集恢复竞态与模块绑定冲突

冻结九路径恢复前态并让可信事务句柄贯穿提交与回滚
将无日志的旧图集残留收口为只读识别和人工对账
按符号大小写保留导入别名并为投影根绑定分配无冲突名称
补齐普通文件竞态、目录替换和模块命名冲突回归
同步图集事务与 ESM 投影技术约束
This commit is contained in:
2026-08-04 07:47:30 +08:00
parent 7118b03f1a
commit ac736ca72c
5 changed files with 360 additions and 174 deletions
File diff suppressed because it is too large Load Diff
@@ -2763,7 +2763,7 @@ fn javascript_source_type(is_module: bool) -> JavascriptSourceType {
}
}
fn javascript_is_syntactically_valid(content: &str, is_module: bool) -> bool {
pub(in crate::agent) fn javascript_is_syntactically_valid(content: &str, is_module: bool) -> bool {
let allocator = JavascriptAllocator::default();
let parsed =
JavascriptParser::new(&allocator, content, javascript_source_type(is_module)).parse();
@@ -3495,6 +3495,7 @@ struct JavascriptModuleAnalysis {
static_sources: Vec<String>,
import_declaration_ranges: Vec<std::ops::Range<usize>>,
imports: Vec<(String, Vec<(String, String)>)>,
root_bindings: BTreeSet<String>,
used_import_locals: BTreeSet<String>,
import_reference_spans: BTreeMap<String, Vec<JavascriptImportReferenceSpan>>,
namespace_import_members: BTreeMap<String, BTreeMap<String, Vec<std::ops::Range<usize>>>>,
@@ -3571,7 +3572,7 @@ impl<'a> VisitJavascript<'a> for JavascriptModuleAnalysisCollector {
for specifier in declaration.specifiers.iter().flatten() {
match specifier {
JavascriptImportDeclarationSpecifier::ImportSpecifier(specifier) => {
let local = specifier.local.name.to_string().to_ascii_lowercase();
let local = specifier.local.name.to_string();
bindings.push((
javascript_module_export_name(&specifier.imported),
local.clone(),
@@ -3588,7 +3589,7 @@ impl<'a> VisitJavascript<'a> for JavascriptModuleAnalysisCollector {
}
}
JavascriptImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
let local = specifier.local.name.to_string().to_ascii_lowercase();
let local = specifier.local.name.to_string();
bindings.push(("default".to_string(), local.clone()));
if let Some(symbol_id) = specifier.local.symbol_id.get() {
self.import_symbols.push((local, symbol_id));
@@ -3597,7 +3598,7 @@ impl<'a> VisitJavascript<'a> for JavascriptModuleAnalysisCollector {
}
}
JavascriptImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
let local = specifier.local.name.to_string().to_ascii_lowercase();
let local = specifier.local.name.to_string();
bindings.push(("*".to_string(), local.clone()));
if let Some(symbol_id) = specifier.local.symbol_id.get() {
self.import_symbols.push((local.clone(), symbol_id));
@@ -3878,6 +3879,13 @@ fn javascript_module_analysis(content: &str, is_module: bool) -> Option<Javascri
}
let mut collector = JavascriptModuleAnalysisCollector::default();
collector.visit_program(&parsed.program);
collector.analysis.root_bindings = semantic
.semantic
.scoping()
.get_bindings(semantic.semantic.scoping().root_scope_id())
.keys()
.map(|name| name.to_ascii_lowercase())
.collect();
let mut namespace_usage = JavascriptNamespaceUsageCollector {
scoping: semantic.semantic.scoping(),
namespaces: collector
@@ -4025,9 +4033,7 @@ fn javascript_top_level_declarations(content: &str) -> std::collections::BTreeMa
.map(|offset| cursor + offset + 1)
.or_else(|| content[cursor..].find('\n').map(|offset| cursor + offset))
.unwrap_or(content.len());
declarations
.entry(name)
.or_insert_with(|| content[start..end].to_string());
declarations.insert(name, content[start..end].to_string());
}
}
declarations
@@ -4114,6 +4120,24 @@ pub(in crate::agent) fn apply_javascript_span_replacements(
true
}
fn unique_javascript_projection_binding_name(
preferred: &str,
assigned: &BTreeSet<String>,
remaining_projection: &BTreeSet<String>,
) -> String {
let preferred = preferred.to_ascii_lowercase();
if !assigned.contains(&preferred) && !remaining_projection.contains(&preferred) {
return preferred;
}
for index in 1_u64.. {
let candidate = format!("{preferred}__agc_import_{index}");
if !assigned.contains(&candidate) && !remaining_projection.contains(&candidate) {
return candidate;
}
}
unreachable!("a monotonically suffixed JavaScript binding name must become unique")
}
pub(in crate::agent) fn rename_javascript_root_binding(
content: &mut String,
from: &str,
@@ -4533,6 +4557,12 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
}
}
}
let mut unit_root_bindings = importer_analysis.root_bindings.clone();
for (_, bindings) in &importer_analysis.imports {
for (_, local) in bindings {
unit_root_bindings.remove(&local.to_ascii_lowercase());
}
}
for (origin, bindings) in origins {
let Some(origin_content) = module_contents.get(&origin) else {
continue;
@@ -4542,29 +4572,55 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
.map(|(exported, _, _)| exported.to_ascii_lowercase())
.collect::<BTreeSet<_>>();
let mut projection = javascript_module_binding_projection(origin_content, &used_names);
let mut synthetic_aliases = BTreeMap::<String, String>::new();
for (exported, local, namespace) in &bindings {
let origin_local = module_analyses
.get(&origin)
.and_then(|analysis| analysis.exports.get(&exported.to_ascii_lowercase()))
.and_then(|target| match target {
JavascriptExportTarget::Local(name) => Some(name.as_str()),
JavascriptExportTarget::Reexport { .. } => None,
})
.unwrap_or(exported.as_str());
if namespace.is_none() && origin_local == "__agc_default_export__" {
synthetic_aliases
.entry(origin_local.to_string())
.or_insert_with(|| local.clone());
}
if projection.is_empty() {
continue;
}
for (synthetic, canonical) in &synthetic_aliases {
if !rename_javascript_root_binding(&mut projection, synthetic, canonical) {
let projection_analysis = javascript_module_analysis(&projection, true)
.ok_or_else(|| format!("自主构建模块投影不是有效 JavaScript:{origin}"))?;
let preferred_synthetic_alias =
bindings.iter().find_map(|(exported, local, namespace)| {
let origin_local = module_analyses
.get(&origin)
.and_then(|analysis| analysis.exports.get(&exported.to_ascii_lowercase()))
.and_then(|target| match target {
JavascriptExportTarget::Local(name) => Some(name.as_str()),
JavascriptExportTarget::Reexport { .. } => None,
})
.unwrap_or(exported.as_str());
(namespace.is_none() && origin_local == "__agc_default_export__")
.then(|| local.to_ascii_lowercase())
});
let mut remaining_projection = projection_analysis.root_bindings.clone();
let mut assigned_bindings = unit_root_bindings.clone();
let mut projected_binding_names = BTreeMap::<String, String>::new();
for original in &projection_analysis.root_bindings {
remaining_projection.remove(original);
let preferred = if original == "__agc_default_export__" {
preferred_synthetic_alias.as_deref().unwrap_or(original)
} else {
original
};
let canonical = unique_javascript_projection_binding_name(
preferred,
&assigned_bindings,
&remaining_projection,
);
assigned_bindings.insert(canonical.clone());
projected_binding_names.insert(original.clone(), canonical);
}
for (original, canonical) in &projected_binding_names {
if original != canonical
&& !rename_javascript_root_binding(&mut projection, original, canonical)
{
return Err(format!(
"自主构建模块匿名 default 投影绑定无法按符号重命名:{origin}::{synthetic} -> {canonical}"
"自主构建模块投影绑定无法按符号重命名:{origin}::{original} -> {canonical}"
));
}
}
if javascript_module_analysis(&projection, true).is_none() {
return Err(format!("自主构建模块投影重命名后语义无效:{origin}"));
}
unit_root_bindings = assigned_bindings;
for (exported, local, namespace) in bindings {
let origin_local = module_analyses
.get(&origin)
@@ -4574,10 +4630,11 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
JavascriptExportTarget::Reexport { .. } => None,
})
.unwrap_or(exported.as_str());
let projected_local = synthetic_aliases
.get(origin_local)
let projected_local = projected_binding_names
.get(&origin_local.to_ascii_lowercase())
.map(String::as_str)
.unwrap_or(origin_local);
let normalized_local = local.to_ascii_lowercase();
if let Some(namespace) = namespace {
if let Some(ranges) = importer_analysis
.namespace_import_members
@@ -4591,37 +4648,22 @@ pub(in crate::agent) fn read_external_gameplay_javascript_at(
.map(|range| (range, projected_local.to_string())),
);
}
} else if origin_local == "__agc_default_export__" {
if projected_local != local {
if let Some(spans) = importer_analysis.import_reference_spans.get(&local) {
unit_replacements.extend(spans.iter().map(|span| {
let replacement = if span.shorthand {
format!("{local}: {projected_local}")
} else {
projected_local.to_string()
};
(span.range.clone(), replacement)
}));
}
}
} else if origin_local != local {
} else if projected_local != normalized_local {
if let Some(spans) = importer_analysis.import_reference_spans.get(&local) {
unit_replacements.extend(spans.iter().map(|span| {
let replacement = if span.shorthand {
format!("{local}: {origin_local}")
format!("{normalized_local}: {projected_local}")
} else {
origin_local.to_string()
projected_local.to_string()
};
(span.range.clone(), replacement)
}));
}
}
}
if !projection.is_empty() {
unit.push('\n');
unit.push_str(&projection);
added_projection = true;
}
unit.push('\n');
unit.push_str(&projection);
added_projection = true;
}
if added_projection {
if !apply_javascript_span_replacements(&mut unit, &mut unit_replacements) {
@@ -2793,6 +2793,10 @@ fn javascript_module_projection_preserves_duplicate_aliases_without_duplicate_or
"two declarations from one dependency",
"import { rotatePiece as turnLeft } from './origin.mjs'; import { rotatePiece as turnRight } from './origin.mjs'; turnLeft(); turnRight();",
),
(
"case-distinct local aliases",
"import { rotatePiece as turn, rotatePiece as Turn } from './origin.mjs'; turn(); Turn();",
),
] {
fs::write(root.join("game/main.mjs"), source).expect("write duplicate alias importer");
let modules = read_external_gameplay_javascript_at(root, html)
@@ -2883,6 +2887,32 @@ fn javascript_module_projection_preserves_duplicate_aliases_without_duplicate_or
);
}
fs::write(
root.join("game/origin.mjs"),
"const turn = () => 'helper'; export default function() { return turn(); }",
)
.expect("write anonymous default with a colliding private binding");
fs::write(
root.join("game/main.mjs"),
"import turn from './origin.mjs'; turn();",
)
.expect("write anonymous default alias that collides in its origin");
let modules = read_external_gameplay_javascript_at(root, html)
.expect("read collision-safe anonymous default projection");
let projected = modules
.module_units()
.iter()
.find(|unit| unit.contains("const turn__agc_import_1 = function()"))
.unwrap_or_else(|| {
panic!(
"anonymous default must receive a collision-free canonical alias: {:#?}",
modules.module_units()
)
});
assert!(projected.contains("const turn = () => 'helper';"));
assert!(projected.contains("turn__agc_import_1();"));
assert!(javascript_is_syntactically_valid(projected, true));
fs::write(
root.join("game/bridge-a.mjs"),
"export { default } from './origin.mjs';",
@@ -2917,6 +2947,27 @@ fn javascript_module_projection_preserves_duplicate_aliases_without_duplicate_or
1,
"an anonymous default reached through two bridges must be projected once",
);
fs::write(
root.join("game/origin.mjs"),
"export function rotatePiece() { return 'rotated'; }",
)
.expect("restore named export origin");
fs::write(
root.join("game/main.mjs"),
"import { rotatePiece as turnPiece } from './origin.mjs'; const rotatePiece = 'metadata'; turnPiece();",
)
.expect("write named import whose origin binding collides in the importer");
let modules = read_external_gameplay_javascript_at(root, html)
.expect("read collision-safe named projection");
let projected = modules
.module_units()
.iter()
.find(|unit| unit.contains("function rotatepiece__agc_import_1()"))
.expect("named origin binding must avoid importer root bindings");
assert!(projected.contains("const rotatepiece = 'metadata';"));
assert!(projected.contains("rotatepiece__agc_import_1();"));
assert!(javascript_is_syntactically_valid(projected, true));
}
#[test]
@@ -5977,6 +5977,7 @@
## 2026-08-04 图集事务与 Tetris 完成门使用句柄和 AST 收口
- 图集事务:九文件旧合同在写 `prepared` 前必须全部持有可信源句柄并整体复读;Unix 事务控制文件统一通过锚定目录句柄的 `openat / unlinkat + O_NOFOLLOW + O_NONBLOCK` 操作,FIFO 等非普通文件必须在读取前失败关闭,恢复前态也必须在同一叶子句柄上稳定双读并复核前后元数据与当前 inode。Windows 祖先 pin 只请求读访问并拒绝 delete sharing,可重复持有;只有事务叶子句柄请求删除访问。事务捕获与恢复 CAS 从 canonical 项目根句柄逐组件打开或创建父目录,staging、no-replace link/move 与 unlink 均相对固定父目录句柄执行;清理事务证据前再次复核整组安装结果。安装后的任何清理错误都按实际 canonical 状态把当前项纳入逆序回滚,不能留下新旧混合合同。
- JavaScript / ESMTetris 静态连续性检查以 Oxc parser、semantic 与 AST visitor 为权威。无效语法、ASI、template interpolation、正则 / 注释、表达式体箭头、参数和词法遮蔽、export alias、import 后再 export 的 bridge、re-export 与缺失导出链接不再由字符串扫描猜测;跨模块 alias 保留 origin 根绑定名称,只按解析到 import symbol 的 reference span 改写 importer。同一 export 的多个本地 alias 保序保留,同一 dependency 的多条 import declaration 合并绑定;投影在 importer 内按最终 origin 聚合,因此同一 origin 经不同 dependency 或 bridge 到达时也只生成一次根声明。object shorthand 展开后保留原键;namespace 只改写绑定到 import symbol 的完整 member span,同文本属性和局部遮蔽均不得连带改写。HTML `type` 存在时优先于 legacy `language`。源码投影仍只是静态语义门,最终完成继续要求绑定当前 revision 的真实 Chromium 固定试玩回执
- 图集事务 live identity`prepared` 后继续由同一 trusted transaction directory handle 贯穿 canonical 提交、`committed` 清理和 live rollback,不再按路径重开并接受替换目录。恢复在任何写入前冻结九项 canonical 全部前态,晚序普通文件变化必须 CAS 失败且不得被旧快照覆盖。没有 durable journal 的 legacy `.previous / .replacement` 只做锚定识别并进入 reconciliation,不自动恢复 canonical 或删除残留
- JavaScript / ESMTetris 静态连续性检查以 Oxc parser、semantic 与 AST visitor 为权威。无效语法、ASI、template interpolation、正则 / 注释、表达式体箭头、参数和词法遮蔽、export alias、import 后再 export 的 bridge、re-export 与缺失导出链接不再由字符串扫描猜测;跨模块 alias 保留 origin 根绑定名称,只按解析到 import symbol 的 reference span 改写 importer。同一 export 的多个本地 alias 按大小写敏感的 symbol identity 保序保留,同一 dependency 的多条 import declaration 合并绑定;投影在 importer 内按最终 origin 聚合,因此同一 origin 经不同 dependency 或 bridge 到达时也只生成一次根声明。组合单元为 importer 和所有 origin 根绑定分配无冲突的确定性名称,匿名 default、同源私有根名、importer 局部根名及不同 origin 都不得合并;重命名后必须重新通过 parser 与 semantic,只有最终启发式扫描文本统一小写。object shorthand 展开后保留原键;namespace 只改写绑定到 import symbol 的完整 member span,同文本属性和局部遮蔽均不得连带改写。HTML `type` 存在时优先于 legacy `language`。源码投影仍只是静态语义门,最终完成继续要求绑定当前 revision 的真实 Chromium 固定试玩回执。
- 浏览器因果:状态证据仍只冻结 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` 失败误报为源码失败。
File diff suppressed because one or more lines are too long