## Parent - #200 ## 来源 2026-07-28 对 `#200` 落地代码的 Spec review。 ## 问题 `replaceFileAtomicallyIfUnchanged()` 当前执行: 1. 读取目标文件; 2. 与 `expectedBytes` 比较; 3. `await fs.promises.rename(temp, target)` 发布新内容。 Project mutation 队列只能串行化 CDF 进程内的写入。任意外部程序仍可在步骤 2 与 3 之间写入目标文件,该写入随后会被 CDF 的 rename 静默覆盖。因此当前实现提供的是“原子发布 + 进程内串行化”,不是 #200 所写的、面向任意外部写入者的原子 compare-and-swap。 现有测试仅覆盖:调用前文件已变化,以及两次 `store.saveDocument()` 的进程内并发;没有覆盖“比较完成后、发布前”发生外部写入。 ## 必须先完成的决策 标准 Node 文件 API 没有跨平台的“比较现有内容并条件替换”原语,且不合作的外部写入者不会遵守 CDF 的进程锁。因此实现前必须明确产品保证,二选一并记录理由: ### A. 严格外部写保护 保证任意外部程序在任何时刻写入都不会被 CDF 静默覆盖。采用此选项时,必须给出可在 macOS / Windows / Linux 上成立的发布协议或明确受支持的平台边界,并用可控竞态测试证明,而不是再次缩短 check→rename 窗口。 ### B. CDF 内部强保证 + 外部写 best-effort 严格保证所有 CDF 写入者之间不丢数据;对不合作的外部程序只做发布前检测和冲突提示,不宣称无窗口。采用此选项时,必须同步修订 #200 中“原子 CAS / 外部程序不丢修改”的表述、测试口径和用户可见冲突语义,避免实现与承诺继续不一致。 若该保证会成为长期架构约束,按 `docs/agents/domain.md` 判断是否需要 ADR;不要在代码注释里单独做产品决策。 ## 交付内容 - 写明选定的保证等级、适用写入者和平台边界。 - 使 API/type/注释准确表达该保证;禁止继续把 `read + rename` 称为无条件原子 CAS。 - 按选定保证实现冲突检测与错误结果。 - 保留原子发布:读者只能看到旧文件或新文件,不能看到半写文件。 - 失败和冲突必须保留用户未保存内容,并返回当前磁盘内容/版本。 ## 测试 必须新增一个可控竞态 seam,在“完成 expected 版本校验”与“发布”之间插入外部写入,并断言选定保证下的预期结果。 同时保留: - 两个 CDF writer 的交错写入; - 创建目标时的并发 `EEXIST`; - 写入失败无临时文件残留; - 冲突返回当前内容; - 发布中断不产生半写文档。 ## 验收标准 - [ ] 保证等级已明确,不再混用“原子发布”和“原子 CAS”。 - [ ] 对应的竞态测试可在 CI 稳定复现旧窗口。 - [ ] 实现与 #200 / ADR 文案一致。 - [ ] `pnpm test` 覆盖 flow-diagram 全簇并通过。 - [ ] `pnpm run typecheck` 与 `pnpm run build` 通过。 ## 非目标 - 不改变 Excalidraw 文档格式。 - 不修改 `manage_flow_diagram` 动作集或错误码,除非所选一致性保证确实需要新增可区分的冲突码并在 #200 中明确记录。 - 不在本票迁移 revision/rollback 或 renderer 状态机;后续子 Issue 依赖本票的存储契约。
Activity
suntianc commentedon Jul 29, 2026
已在 #245 完成实现并推送;Ubuntu、macOS、Windows CI 均通过。