修复游戏脚本投影与图集事务身份校验

完善 ESM 声明、导入成员与动态依赖的 AST 投影
按真实调用时序解析 alias、receiver owner 与 dynamic import occurrence
补齐匿名默认导出、构造调用、回调与静态实例成员边界
提交图集事务前校验 retained directory handle 的 pathname identity
增加投影、时序、动态依赖与 live commit 回归测试
同步 AI 游戏创作技术方案与共享决策记录
This commit is contained in:
2026-08-04 11:41:23 +08:00
parent ac736ca72c
commit 267171a1d0
5 changed files with 3632 additions and 392 deletions
@@ -4537,6 +4537,11 @@ impl PlatformArtSliceContractRollback {
"{PLATFORM_ART_LOCAL_RECONCILIATION_PREFIX} 平台图集合同待提交,但缺少 prepared 阶段锚定的事务目录句柄"
)
})?;
trusted_transaction_directory.verify().map_err(|error| {
format!(
"{PLATFORM_ART_LOCAL_RECONCILIATION_PREFIX} 平台图集合同发布 committed 前事务目录身份无效:{error}"
)
})?;
trusted_transaction_directory.publish_marker(
STRICT_PLATFORM_ART_TRANSACTION_COMMITTED,
b"committed\n",
@@ -7957,6 +7962,51 @@ mod canvas_generation_tests {
.exists());
}
#[cfg(unix)]
#[test]
fn prepared_transaction_rejects_pathname_replacement_before_live_commit() {
let temporary = tempfile::tempdir().expect("create live commit handle project");
let root = temporary.path();
init_local_game_project_at(root, "live-commit-handle", "committed 事务句柄测试")
.expect("init project");
for (index, local_path) in STRICT_PLATFORM_ART_CONTRACT_PATHS.iter().enumerate() {
let path = root.join(local_path);
if path.exists() {
continue;
}
fs::create_dir_all(path.parent().expect("contract parent"))
.expect("create contract parent");
fs::write(path, format!("committed-contract-{index}"))
.expect("write complete committed contract");
}
let mut transaction = PlatformArtSliceContractRollback::capture(root, "live-commit-handle")
.expect("capture prepared transaction");
let transaction_directory = transaction.transaction_directory.clone();
let displaced = transaction_directory.with_file_name("transaction-commit-displaced");
fs::rename(&transaction_directory, &displaced)
.expect("displace prepared transaction directory before commit");
fs::create_dir(&transaction_directory).expect("create commit pathname substitute");
fs::write(
transaction_directory.join(STRICT_PLATFORM_ART_TRANSACTION_PREPARED),
b"prepared\n",
)
.expect("write substitute prepared marker");
let error = transaction
.commit()
.expect_err("live commit must reject a replaced transaction pathname");
assert!(
error.contains("目录身份发生变化"),
"unexpected error: {error}"
);
assert!(!displaced
.join(STRICT_PLATFORM_ART_TRANSACTION_COMMITTED)
.exists());
assert!(!transaction_directory
.join(STRICT_PLATFORM_ART_TRANSACTION_COMMITTED)
.exists());
}
#[test]
fn durable_strict_contract_transaction_rollback_preserves_concurrently_changed_installed_target(
) {
File diff suppressed because it is too large Load Diff
@@ -5977,7 +5977,11 @@
## 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 状态把当前项纳入逆序回滚,不能留下新旧混合合同。
- 图集事务 live identity`prepared` 后继续由同一 trusted transaction directory handle 贯穿 canonical 提交、`committed` 清理和 live rollback,不再按路径重开并接受替换目录。恢复在任何写入前冻结九项 canonical 全部前态,晚序普通文件变化必须 CAS 失败且不得被旧快照覆盖。没有 durable journal 的 legacy `.previous / .replacement` 只做锚定识别并进入 reconciliation,不自动恢复 canonical 或删除残留。
- 图集事务 live identity`prepared` 后继续由同一 trusted transaction directory handle 贯穿 canonical 提交、`committed` 清理和 live rollback,不再按路径重开并接受替换目录;发布 `committed` marker 前必须验证 retained handle 的权威 pathname identityUnix rename 漂移不得降级为成功 warning。恢复在任何写入前冻结九项 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 固定试玩回执。
- JavaScript / ESM 声明与 namespace 补充:顶层 function/class/variable declaration 直接按 Oxc statement span 投影,禁止用首个分号或换行截断箭头函数、多行 initializer 或多 declarator;对象、数组、默认值和 rest 解构中的全部 binding 必须递归进入导出图,同一声明只投影一次。import symbol 不从 importer 自有 root binding 集合按小写文本扣除,大小写不同的合法绑定继续隔离;模块组合按依赖层数迭代到稳定闭包,使被导出函数引用的 imported dependency 继续进入最终 consumer 单元,同时限制最终 span replacement 后的单个组合单元最多 `2 MiB`、整轮投影累计处理最多 `32 MiB`,分支或循环图超限失败关闭。固定字符串 dynamic import 同样由下游实际使用的 export 反向驱动加载,未使用 export、未调用嵌套函数和恒假分支中的 source 不进入模块单元;被选声明中只有由 `await import` 解构、namespace member 或 `.then(...)` 静态解析到的 export 才投影。callback 参数、解构 alias 和 namespace member 的引用必须按 semantic symbol span 改接到投影根,禁止靠同名文本共现绕过局部遮蔽。完成全部 span replacement 后重新校验完整组合 unit,最终启发式扫描必须先按原始大小写完成 AST 解析和掩码,再归一化文本。匿名 default function / arrow 在原始 AST 中也必须以 collision-safe synthetic binding 注册可外调 root span,保证 wrapper 内 imported member 的传递依赖继续传播;synthetic binding 必须避开真实根绑定,冲突改名只更新 `default` target,不得改写用户同名命名导出。renamed re-export 的 namespace 投影同时携带 importer member 名和 origin export 名,分别用于定位 consumer span 与 origin declaration。named import、namespace import 及 namespace 解构 alias 的成员调用必须保留完整静态成员路径,并用调用 span 的恒假分支可达性过滤后再向上游传播 demand;对象 / class 的直接成员、解构 alias、实例 alias 与下游 wrapper 都遵循同一规则。对象的 method shorthand、函数表达式值、箭头函数值以及 class function-valued field 均按精确函数 span 注册成员根,不能因声明写法不同漏载其可达 dynamic dependency,也不能把同一属性内未调用的嵌套函数误当根。
- JavaScript / ESM occurrence 与成员根补充:dynamic import demand 必须以 source 和 import occurrence position 共同隔离,同 source 的可达裸 import 不得借用不可达 occurrence 的 export;动态 namespace 保留首段 export 后的完整成员路径,声明 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` 反查。
- JavaScript / ESM alias 与 receiver 补充:imported member 作为参数时只允许受控的 callback API 建立执行 demand,普通日志或元数据传参不得推断为调用。普通 member alias 同时支持声明 initializer 与后续赋值,`new ns.Game()` 等完整 constructor path 必须传播到实例 alias。局部对象、class、实例、`this``super` 的方法调用统一按 semantic receiver owner 匹配,禁止再按末级方法名跨 owner 扩散到同名 decoy。
- JavaScript / ESM 控制流身份补充:member alias、局部 receiver 与动态 namespace 的赋值必须保存 assignment position 和 enclosing function scope;函数体使用按真实 invocation position 选择当时事件,多次调用跨越赋值边界时合并可能 owner,恒假分支、未调用函数或调用之后的赋值不得覆盖更早使用点。导出的 function 与 class/object member 额外以模块初始化结束作为潜在外部调用点,使声明后生效的顶层赋值进入 demand,同时保留更早本地调用状态。动态依赖传播以原始 owner 模块 AST span 为权威,不因投影重排声明或省略独立赋值语句重算 alias。class method owner 进一步区分 static / instanceclass expression 与实例化 alias 使用同一 owner 图。callback API 只接受 semantic 未解析的已知全局调度函数,以及可由 AST 证明的 literal array / dynamic import 调用;被用户定义或遮蔽的同名 `setTimeout / map / then` 不得推断执行参数。
- 浏览器因果:状态证据仍只冻结 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