# 策略持久化自持化(设计 · 2026-09-27) > **一句话**:让「团队 / 策略」的落点**可核** —— `settings` 服务可用时优先用它;不可用时落到**插件自己的文件**;两者都不行才退回进程内存。**红线 = 「当前落点」始终可核**(服务 / 文件 / ⚠ 仅内存 三态之一必须在读面首行显式可见);写失败与折叠各有留痕,不静默。 > **状态**:✅ **已实施**(2026-09-27 设计评审 PASS → 同日 eng_coder 三阶段交付;文件 `lib/index.js` + `host-half.test.mjs` + 四份文档;**当次实测** `node host-half.test.mjs` → `1237 (failed: 0)`、`node client-half.test.mjs` → `269 (failed: 0)`;基线 `1203` / `269` ⇒ 新增 **34** 条宿主断言)。「第三方(`session-link-pro`)和文件的关系」一处本档未规定,实施时按「折叠先于迁移、旧命名空间留在原处」落地;回写时点已在 §3.1 档 4 写实(attach 后置链**末端**),理由与证据见 [verification-log.md](verification-log.md) 的「策略持久化自持化」一节;**损坏的 `policy.json` 不被覆盖**亦为实施时补的裁定(本档未写,代码评审修复轮 🟡#1 已补进 §3.4/§3.5 与 U11)。一次性真机迁移由父侧执行,步骤见 `../AGENTS.md` §七。 --- ## §1 现象与证据(2026-09-27 真机诊断,desktop 部署) | # | 读数 | 含义 | |---|---|---| | 1 | 建队那一刻,`` 下**没有任何设置文件被写**(只有别的插件的 store) | 写路径没落到磁盘 | | 2 | `profiles\desktop\cordis.patch.yml` 里 `teams: [threat-intel]`(`createdAt=1789749527022`),插件读出「共 0 个团队」 | 读路径也没接到那份存储 | | 3 | `team_link_status team=threat-intel` ⇒「不在注册表中(已知团队:l1l2-verify)」 | 只认本进程内存里那份 | | 4 | 代码 `attachFrom`(`lib/index.js:1612`):`ctx.get("settings")` 拿不到 ⇒ `inject(["settings"])` 等 ⇒ **内存兜底 + 恰好一行 warn** | 根因:`settings` 服务在本部署始终没对它激活 | | 5 | `threat-intel` 的 `createdAt` ≈ 2026-09-16(远早于今天) | **当初能持久化** ⇒ 是部署侧后来变了,不是插件一直如此 | | 6 | 同部署其它插件:`undo-savepoint` → `undo*/…json`、`dream-skin` → `dream-skin.json`、`thinking-effort` → `thinking-effort-loaded.json` | 本部署的插件**各自写自己的 JSON**,不依赖宿主 settings 服务 | **未定位点(如实标注)**:是*这个 desktop 构建没挂 settings 服务*,还是*插件改成 bundle 后命名空间换了、旧数据成孤儿*。要确认需读宿主 asar 内代码(先前会话试过,读不到)。**本设计不依赖该定位**:文件后端在两种成因下都成立。 --- ## §2 需求与判据(R) - **R1 持久**:settings 不可用的部署里,团队/策略**重启后仍在**。 - **R2 可核(红线)**:任何写入都必须能指出落点(服务 / 文件 / 仅内存);**「仅内存」必须显式可见**,不许静默。 - **R3 不回归**:settings 可用的部署里行为与今天一致(仍走服务,且**不**新建文件)。 - **R4 迁移**:把孤儿数据(`cordis.patch.yml` 的 `teams`)一次性导入新落点。 - **R5 原子**:崩溃/断电不得产生半个状态文件。 --- ## §3 设计 ### 3.1 落点优先级(三档 → 四档 → 五档) 1. **settings 已 attach** ⇒ 用它(与今天一致,`scope` 非空)。 2. **未 attach、但能写文件** ⇒ **文件后端** `/team-link/policy.json`。 3. **文件也不可写** ⇒ 进程内存 + 恰好一行 warn(**现状**,保留为最后兜底)。 4. **文件有数据、服务后来 attach** ⇒ **折叠**:把文件内容并进服务,服务成为落点;并列痕。**折叠落定后立即把「合并结果」重写回文件并盖 `foldedAt`**(实为 attach 后置链**末端**,理由见 verification-log)—— 这样若服务之后再次缺席(重启回第 2 档),文件里是**合并后的真值**,不会是折叠前的过期快照(否则折叠后新建的团队会表现为「消失」)。 5. **快路径折叠(分歧审计修复轮 🟡#5 增补,2026-09-27)**:服务在 activation 就已 attach 时同样走第 4 档,**不再因为「挂得早」而不读文件**。 - 服务**为默认** + 文件**有状态** ⇒ **折叠**(并入服务 + 留痕 + 合并结果回写文件并盖 `foldedAt`),规则与第 4 档**同一条**; - 服务**非默认** ⇒ 按 3.3 **服务胜**,文件那一侧**连读都不读**(U6 第二条断言:零字节变动)。 > 增补理由:原实现里 `loadPolicyFile()` 只有一个调用点(`if (scope === null)`),于是「服务从一开始就在场」的部署**永远读不到文件** —— 文件后端时期写下的团队在下次启动里不可见,正是 §1 现象的形状,而 U6 的第二条断言把它锁成了规格。 > 复用现有机制:插件本就有「**memory-only startup window** → 服务晚挂时折叠」的完整逻辑(`memoryDirty` / 折叠 warn / 失败保留重试)。本设计把那个「memory」从**纯内存**换成**文件支撑**,是同一套逻辑的对称扩展,不是新发明。 ### 3.2 路径解析(新增,插件目前完全不读环境) - `DSH_HOME`(实测存在:`C:\Users\magic\.dsh`)⇒ 目录 `/team-link/`,文件 `policy.json`。 - 缺失时退回 `os.homedir()/.dsh/team-link/policy.json`;两者都拿不到 ⇒ 落第 3 档 + warn。 - **落在全局目录,不落工作区**:团队是按名字全局寻址的(只有会话列表是工作区作用域),放工作区会造成「换个工作区团队就没了」。 ### 3.3 文件 schema(评审 🟡#1 补) ```json { "schema": 1, "savedAt": "2026-09-27T06:26:24.786Z", "foldedAt": null, "policy": { /* 与 settings 命名空间同键集 */ } } ``` - `policy` 的键集**镜像** settings 命名空间(迁移时不得有损,迁移型改动必须一次性写全)。 - `savedAt`:每次成功写入刷新(出处);`foldedAt`:折叠落定时刻(`null` = 从未折叠)。 - **折叠胜负裁决不靠时间戳**(服务侧没有可比时间),沿用本仓既有的 legacy 迁移规则:**服务非默认 ⇒ 服务胜(文件归档 `policy.superseded-.json` 并留痕);服务为默认而文件非默认 ⇒ 文件折叠进服务;两边都非默认 ⇒ 服务胜 + 留痕**(不做静默合并)。 ### 3.4 写入语义 - **原子**:写 `.tmp` → `fsync` → `rename` 覆盖目标;目录先 `mkdir -p`。 - **写失败**:保留内存态 + warn(**不静默**),下一次写入重试。warn 门**按段复位**(代码评审修复轮 🟡#2):一道门 `fileWriteWarned` 过去是**进程级一次性**的,于是第一段失败 warn 过之后,再下一次失败完全静默;现在**写入成功即复位** ⇒ 一段失败 = 一行,成功后新的一段失败再报一行(与 `attachWarned` 按窗口复位同一范式)。 - **损坏档守卫(代码评审修复轮 🟡#1 新增)**:载入时判定文件**损坏**(读不出 / 非法 JSON / 没有 `policy` 对象 —— **`policy` 是数组也算**:数组不是命名空间镜像,会被 `normalizePolicy` 读成一份全默认的空视图;代码评审第 2 轮 🔵#2 补上,判据即 `typeof doc.policy !== "object" || Array.isArray(doc.policy)`,与「缺失 / null」走同一条置坏分支)之后,**一律不覆盖**那份文件 —— 每一次写都直接返回并在留痕里**如实说明**(不静默,也不吞掉第二次以后)。修复前这里只有一个「本次不会覆盖该文件」的 warn 而 `persistPolicyFile` **毫无守卫**,于是文件后端下**下一次任何写入**都会用原子 rename 把损坏文件整个替换掉,落点行还会从「⚠ 仅进程内存」翻成「存储:文件」(`policyPlacement` 只看 `fileUsable`)。 - **人工处置 = 唯一出口**:`policy.json` 损坏时插件**不猜、不合并、不覆盖**,只把状态落进程内存并报第 5 个原因码;**修好或移走该文件**(用户自己动手)后重启进程即恢复。每个 store 只读一次文件,所以「修好了但插件还在跑」不会自动生效。 - **只写一处**:attach 成功后**不再写文件**(避免双写不一致);反之亦然。**读**不受这条约束 —— 读只发生在「服务为默认」这一支(§3.1 第 5 档),写仍然只有一处。 - **`foldedAt` 只由折叠落定(代码评审修复轮 🔵#3 新增)**:回写合并结果时**只有本次链上确有折叠/裁决落定**才盖新戳,判据取折叠那一步的结局(`MEMORY_WINDOW_OUTCOME_NONE` = 「未挂载期间无写入」那一种除外;**catch 支例外**:链在折叠结果产出**之前**断掉时读不到那个结局,catch 那一支对 `syncFileFromService` 显式传 `foldHappened: false` —— **宁可漏盖不冒盖**,保留上一次真正落定的戳,代码评审第 2 轮 🟡#1)。修复前无条件盖戳,于是「最近一次 attach 时刻」冒充「折叠落定时刻」;**回写本身不受这条约束**(U3/U4/U8/U10 要求的合并结果写回照旧),只是没有折叠的那一段保留上一次真正落定的戳,逐字节不动。 - **`fold failed` 不回写(代码评审第 3 轮 🟡#1 新增)**:折叠**失败**(`fold failed` —— 窗口数据没并入服务、仍只活在文件里)的那一次,链尾**连回写都不做**:把服务视图写回文件会把窗口数据从内存与文件**两边同时**抹掉(内存这边在回写里被服务视图整份替换),且不留归档。文件保留未并入的那一份,与折叠失败那一行留痕(「仍在落点里、本次未能并入」)逐字一致;`memoryDirty` 保持置位,下一次 attach 的折叠会重试。盖戳判据同批收紧为「**确已落定**的两个结局」(`folded into settings` / `not folded`)才为真 —— `fold failed` 对旧判据(`outcome !== NONE`)取真,照样盖新戳,与「宁可漏盖不冒盖」相反。**不用「先归档再覆盖」那条路**:归档救不回内存那份镜像,宁可保守不回写。 ### 3.5 可核(R2 的落地面) `team_link_roster action=get` 与 `team_link_status` 的**首行**显示当前落点,三态各一句: - `存储:设置服务(team-link 命名空间)` - `存储:文件 <绝对路径>` - `⚠ 存储:仅进程内存(重启即失)—— 原因:` - 前四个里 `路径解析失败` 与 `文件不可写` 同源,都是「拿不到可写的落点」; - **第 5 项 `文件损坏(不覆盖;请人工处理)` 是代码评审修复轮 🟡#1 增补的**,与既有四项并列。它与「不可写」**不是**同一件事:文件**写得进去**,插件只是**拒绝**用它(§3.4 的损坏档守卫),并把「人工处置」这条唯一出口写在原因码里。 - **两因并存时并陈(代码评审第 2 轮 🔵#3)**:服务侧 `no-register` 与文件侧原因(`路径解析失败` / `文件不可写` / `文件损坏`)**同时**成立时,落点行**并陈**两条 —— `⚠ 存储:仅进程内存(重启即失)—— 原因:no-register;另:文件损坏(不覆盖;请人工处理)`。**优先序**:主因仍取 `no-register`(与服务侧单因形态一致),文件侧那一因以 `;另:` 附在后面;**四种单因形态逐字不变**(只有一条原因时不出现「另:」)。修复前 `no-register` 独占一行,把「文件损坏、请人工处理」这条唯一行动信号整个吞掉。 ### 3.6 迁移(R4) - **迁移前置(分歧审计修复轮 🟡#5 增补)**:手工导入 `policy.json` 之前,先确认那一份**真的会被读到** —— 服务侧的 settings 命名空间此刻必须是空的(服务非默认时按 §3.3 服务胜,文件那一份**连读都不读**)。服务里已经有团队时,先导出、比对、再决定是「直接用服务侧」还是「先把服务侧清空再导入」;否则导入的文件会安静地躺在磁盘上不生效(正是本次修复前快路径的形状)。 - 代码侧**不读宿主配置文件**(那是不该有的耦合)。 - 一次性数据迁移由实做者/父侧**手工执行一次**:把 `cordis.patch.yml` 里 `team-link.config.teams` 的内容写成新 `policy.json`,并在账本留读数(迁移前后各一条)。 - 既有的 legacy 命名空间迁移(`session-link-pro` → `team-link`)**一行不动**。 --- ## §4 判据(断言清单,实施前必须逐条先红) | 号 | 断言 | 档 | |---|---|---| | U1 | 无服务时:写 → 读回一致,且文件存在 | 文件后端 | | U2 | 原子:留下半写 `.tmp` 不影响读取(读只认 `policy.json`) | 原子 | | U3 | 折叠:文件有数据 + 服务后 attach ⇒ 数据并入服务,随后落点是服务 | 折叠 | | U4 | 冲突:文件与服务都有数据 ⇒ 按 §3.3 裁决:**服务非默认 ⇒ 服务胜 + 文件归档 + 留痕**(**不比时间戳**) | 折叠 | | U5 | 可核行:三种落点各一条(含「仅内存」必须显式带原因) | 红线 R2 | | U6 | 回归:服务可用时**不**创建/不写文件 | 红线 R3 | | U7 | 路径:`DSH_HOME` 缺失 → 退回 homedir;两者皆缺 → 内存 + warn | 路径 | | U8 | 回归:legacy 命名空间迁移行为不变 | 迁移 | | U9 | **迁移可核**:一次性导入后 `roster get` **显示 threat-intel**,且落点行 = `存储:文件 <绝对路径>` | 迁移 | | U10 | **快路径折叠**(分歧审计修复轮 🟡#5 新增):服务在 activation 就已 attach 且为默认时,文件里那一份照样并入命名空间;并入**之前**文件侧原样归档为 `policy.superseded-.json`(逐字等于导入那一份);折叠后文件里是**合并结果**并盖上 `foldedAt`;留痕恰好一行 + 链上 `folded into settings` | 折叠 | | U5 增补 | 状态卡段①的「来源」**跟随落点**:设置 / 文件 / 内存三态各说一句,落点不是设置服务时不再说「来源:设置」(分歧审计修复轮 🔵#9) | 红线 R2 | | U11 | **损坏档守卫**(代码评审修复轮 🟡#1 新增):seed 一份**非法 JSON** + 主动触发一次写 ⇒ ① 磁盘上那份文件**逐字节不变**;② 落点行原因 = `文件损坏(不覆盖;请人工处理)`(**不**翻成「存储:文件」);③ 判坏在载入留痕里说出口、且**每一次**写都再如实留一行;④ 状态本身不受影响(两个团队在读面看得见,段①说「进程内存」) | 红线 R2 | | U12 | **写失败 warn 按段复位**(代码评审修复轮 🟡#2 新增):一段里连续两次失败**只报一行**(★ 负相 控制项);中间那次**写入成功**之后新的一段失败**又报一行** | 红线 R2 | | 🔵#3 判据 | **`foldedAt` 盖戳门**(代码评审修复轮 🔵#3 新增):链上没有折叠发生(`memory window: none`)的启动 ⇒ `foldedAt` **逐字节保持**上一次真正落定的值;对照项(★ 负相)钉住那个夹具确实落在「没有折叠」这一支上 | 折叠 | | 🔵#4 判据 | **`refused` 是真实的未挂载窗口**(代码评审修复轮 🔵#4 新增):服务在场但**拒绝注册**、后来才挂上 ⇒ ① 折叠留痕说「服务在场但拒绝注册」而不是「服务从一开始就在场」的快路径措辞;② 不给本进程自己写的镜像多归档一份 `policy.superseded-*.json`;对照项(★ 负相)钉住夹具真的挂上并折叠了 | 折叠 | | 🟡#1 判据 | **`fold failed` 之后链尾不回写**(代码评审第 3 轮 🟡#1 新增):`scope.update` 在 register 成功后抛错 ⇒ 折叠必失败 ⇒ ① 文件里那份**逐字节不变**(窗口数据仍在落点里)② `foldedAt` 不重盖 ③ 留痕说「仍在落点里、未并入」与文件实况一致(控制条件并入同条:链上 `memory window: fold failed` 钉住夹具真落在那一支) | 折叠 | | 🟡#2 判据 | **detach 窗口判据随 `detach()` 复位**(代码评审第 3 轮 🟡#2 新增):provider 掉线触发 detach → 窗口内有写入 → provider 回来再挂 ⇒ ① 折叠留痕如实说「provider 被 detach 后又回来」(不说快路径措辞、也不说成 startup window)② 不给本进程自己写的镜像多归档一份(与 🔵#4 同一条裁定) | 折叠 | **红相要求**:8 条新断言必须打在**实施前**的实现上取红相读数(逐条或聚合),逐字进报告;覆盖缺口类(本就绿)按 `★ 负相` 如实标注,**不冒充红相**(本仓既有纪律 ①②③)。 **分歧审计修复轮(2026-09-27)追加的 7 条**(U10 五条 + U5 增补两条)同样取红相:修复前实测 `assertion total: 1244 (failed: 6)` —— 6 条红、1 条(U10「折叠后落点是设置服务」)修复前即绿,按 `★ 负相` 标注(它断言的是「折叠**之后**的落点」,而修复前的缺陷是折叠压根没发生,落点对、团队空)。 **代码评审修复轮(2026-09-27)追加的 11 条**(U11 四条 + U12 两条 + 🔵#3 两条 + 🔵#4 三条)同样取红相:修复前实测 `assertion total: 1255 (failed: 8)` —— 8 条红;另 3 条(U12 控制项、🔵#3 对照项、🔵#4 对照项)修复前即绿,按 `★ 负相` 标注(它们是控制项/对照项,作用是让上面那几条红**不是空转的绿**:分别钉住「那两次写真的失败了」「那个夹具真的落在没有折叠的那一支」「那个夹具真的挂上并折叠了」)。 **代码评审第 2 轮(2026-09-27)追加的 7 条**(🟡#1 两条 + 🔵#2 三条 + 🔵#3 两条)同样取红相:修复前实测 `assertion total: 1262 (failed: 4)` —— 4 条红(🟡#1 catch 支盖戳门 1 条 + 🔵#2 数组 policy 2 条 + 🔵#3 两因并存 1 条);另 3 条(各判据一条控制项)修复前即绿,按 `★ 负相` 标注。起点 `1262 (failed: 4)` → 修复后 `1262 (failed: 0)`,断言总数不变(判据先于实现落位,红相读数里已含新判据)。 **代码评审第 3 轮(2026-09-27)追加的 2 条**(🟡#1 一条 + 🟡#2 一条)同样取红相:修复前实测 `assertion total: 1264 (failed: 2)` —— 2 条红,即两条新判据本身(🟡#1 的红来自「文件被服务视图整份替换、foldedAt 重盖、窗口数据两边消失」;🟡#2 的红来自「折叠留痕仍说 startup window」);本轮没有单独的 ★ 负相控制项(控制条件并入同一条断言:链上 `memory window: fold failed` / provider2 命名空间里看得到窗口写入)。起点 `1264 (failed: 2)` → 修复后 `1264 (failed: 0)`。 --- ## §5 影响面 - `lib/index.js`:`createPolicyStore` 及其 `attachFrom` / `attach` / 折叠路径(`:1281`–`:1651` 为主战场);分歧审计修复轮另加 `loadFileIfServiceAtDefaults` / `policyTeamsSourceLabel` / 状态卡段①的「来源」。 - `host-half.test.mjs`:U1–U8 + U9;分歧审计修复轮追加 **U10(5 条)+ U5 增补(2 条)**;代码评审修复轮再追加 **U11(4 条)+ U12(2 条)+ 🔵#3(2 条)+ 🔵#4(3 条)= 11 条**,并给夹具加了两个把手:`policySeed` 收**原文**字符串(U11 的坏 JSON 要在 `apply` 之前落盘)、settings stub 的 `stopRefusing()`(🔵#4 的「拒注册 → 后来才挂上」);代码评审第 2 轮再追加 **7 条**(🟡#1 catch 支盖戳门 2 条 + 🔵#2 数组 policy 3 条 + 🔵#3 两因并存 2 条),夹具用「日志槽前两行 warn 会抛」让链真的在折叠结果产出**之前**断掉;代码评审第 3 轮再追加 **2 条**(🟡#1 `fold failed` 不回写 + 🟡#2 `detach()` 同步置窗口判据),给 settings stub 加第三个把手 `settingsUpdateThrows`(register 成功而 scope update 抛错 —— `fold failed` 的唯一到达路径),并修掉一处**先于本轮存在**的夹具竞速(h1 的文件轮询读 vs 链尾回写的原子 rename 在 Windows 上偶发 EPERM ⇒ 假红;修法与读数见 verification-log 本轮一节)。 - `README.md` **八处**(评审 🟡#4 当年只列了四处,实施后按实际改动补齐):`:7` 徽章断言数 · `:26` 稳定性行 · 架构图 `:214`(`ST` 节点改写)/ `:227`(**新增的 `PFILE` 边**)/ `:238`(「状态的事实源」一句)· §六 `:1063`–`:1075` 落点说明四条(文件后端 / 折叠并入 / 快路径折叠 / 怎么确认它存在哪)· `:1182` 依赖服务表里 `settings` 行 · `:1199` §十「当前读数」。 - `AGENTS.md` §七(存储落点、折叠与一次性迁移动作;分歧审计修复轮增补「迁移前置」条件)。 - `CHANGELOG.md` / `docs/verification-log.md` / 本档 / `docs/poster/plugin-poster.html` / `docs/team-ledger-and-mode-design-2026-09-26.md`(后两处只更新「当前读数」)。 **架构图说明(分歧审计修复轮 🔵#10)**:图里原有的 `ST --> settings.yaml` 边**保留不动**,另**新增**一条 `PFILE` 边表示插件自己的 `policy.json` 后端 —— 两者是并列的落点,不是替换关系。 --- ## §6 分期(给实施者的 stages) 1. **S1 文件后端**:3.2 路径解析 + 3.3 原子写 + U1/U2/U7。 2. **S2 折叠与可核**:3.1 第 4 档折叠 + 3.4 三态可核行 + U3/U4/U5/U6。 3. **S3 迁移与真机**:3.5 一次性迁移(导入 `threat-intel`)+ 重启后真机读数(团队可见、落点显示为文件)+ U8 + 文档/账本。 --- ## §7 明确不做(避免过度设计) - ✗ **不双写**(服务与文件各写一份)—— 不一致的代价高于收益。 - ✗ **不读宿主配置文件**(`cordis.patch.yml`)—— 迁移一次性手工做,代码不耦合宿主内部结构。 - ✗ **不改宿主 / 不改 profile 加载方式** —— 本设计只动插件自身。 - ⚠ **残留风险 1**:文件与服务分歧按 3.3 的裁决规则处理并留痕(**不做静默合并**);若用户要更强一致性,那是另一个设计题。 - ⚠ **残留风险 2(评审 🟡#3)**:`policy.json` 是**全局单文件**,而本插件跨进程 —— **两个 DSH 进程同时写同一文件无锁、可能丢更新**。本设计**不做文件锁**(代价大于收益),如实声明;缓解 = 原子写 + 每次写入都是「读-改-写整份」,冲突窗口很小,且折叠/迁移路径均有留痕。 - ⚠ **残留风险 3(分歧审计修复轮 🟡#5)**:快路径折叠的前提是「attach 那一刻读到的命名空间还是默认的」。若某个更早的钩子(第三方插件、配置 HMR)已经往命名空间里写过东西,本插件会按 §3.3 判**服务胜**,文件那一份不读也不并 —— 这是**裁定如此**,不是漏读:那一支的可核出口是**折叠裁决 / 归档 / 留痕三件套**,一行不少。真要迁移过去,走 §3.6 的一次性手工导入(并先满足那里的前置条件)。 --- ## §8 代码评审第 4 轮遗留(4 条 🔵,父侧登记为后续项) 评审第 4 轮 **PASS**(零 🔴)。4 条 🔵 的父侧处置: | # | 条目 | 处置 | |---|---|---| | 1 | `syncFileFromService` 的 `foldHappened` **默认取真** = 脚枪(未来漏传即无声退回「无条件盖戳」,正是前两轮刚修掉的形态) | ✅ **已修**(父侧小修,评审后):默认翻到安全侧 `=== true`;两处调用点本就显式传参 ⇒ 行为不变 | | 2 | ✅ **已修(C 批 C3-a)**:快路径归档留痕无条件承诺「随后文件会被改写成合并结果」;「快路径 + `fold failed`」组合里该承诺不兑现,且多出一份冗余 `superseded` 档案(**无数据丢失**,主文件原样保留) | ⏳ 后续项(措辞条件化,或把快路径归档挪到 `update` 成功之后) | | 3 | ✅ **已修(C 批 C3-b)**:`fold failed` 判据只覆盖**晚挂**路径;「快路径 + fold failed」无判据 | ⏳ 后续项(补 `useSettings: true, settingsUpdateThrows: true` + `policySeed` 夹具) | | 4 | ✅ **已修(C 批 C3-c)**:`host-half.test.mjs` 的 `setup()` 形参压成单行约 1900 字符 | ⏳ 后续项(排版) |