# 记忆治理与接续质量 · 专项攻关纲领

> 2026-09-17 定稿 · 用户明确列为**重点攻关项目**
> 上游：`BATTLE-PLAN-20260917.md`（执行顺序权威）、`S10-GAP-INVENTORY-20260917.md`（缺口复核）
> 本文档管辖范围：**记忆正确性治理**（大象）+ **接续材料质量**（白板结构）

---

## 0. 为什么单独开一份文档

用户原话（2026-09-17）：

> "如果老记忆是错的，新记忆更正了；或者新记忆更新了，老记忆不适用，这种情况怎么办？
>  我一直在这个过去的对话和项目文档中提过类似的焦虑，但可能每次 AI 提出的回馈，我都没有及时修改或者没有来得及采纳。"

**这不是新问题，是长期未被处理的欠账。** 现有 12 个记忆工具**没有任何一个**能回答"这条记忆还成立吗"。

---

## 1. 大象的实证：活体标本（不是推测）

**实测位置**：`~/.dsh/memory/workspaces/--D--dsh-auto-memory--/MEMORY.md`（19610 字节 / 203 行）

同一份文件内，三条结论**互相矛盾且并存**：

| 行 | 内容 | 状态 |
|---|---|---|
| **120** | 「逐处判读后结论 —— **真正该解耦的只有 2 处**：`:1993`、`:2037`」 | ❌ **已作废** |
| **161-162** | 「**那个结论划窄了范围**：只枚举了 `boardMode` 一族，**漏掉并行的 `handoffEnabled` 一族**」 | ✅ 修正声明 |
| **183** | 「13 处闸门逐条判归属后：**只需动 4 处**」 | ✅ 现行结论 |

**危害**：行 120 排在**最前、语气最肯定**（"逐处判读后结论"），下一个窗口极可能据此按"2 处"施工，**漏掉两处 handoffEnabled 渲染闸门**——正是用户报障的那条线。

**这证明三件事**：
1. 矛盾确实会产生，且**已经产生**；
2. 修正**确实写了**，但**没有作废旧条目**（append 语义的固有缺陷）；
3. 记忆是**纯文本追加**，无版本、无作废标记、无 supersede 链。

---

## 2. 根因：append-only 与"结论会变"的根本冲突

| 层 | 现有机制 | 为什么不够 |
|---|---|---|
| 日志 | `memory_log_pre` append-only | 追加是**正确**的（日志本就该流水） |
| 项目笔记 | `memory_note_pre` append | ⚠️ **笔记不是流水，是结论**，但用了流水的语义 |
| 用户级 | `memory_user_pre` append | 同上 |
| 白板 | `memory_note_pre(kind=plan)` **replace** | ✅ **唯一有 replace 语义的层** |

**结论**：只有白板支持"重写即替换"。**笔记/用户级只有 append ⇒ 旧结论永远留存 ⇒ 矛盾必然累积。**

而现有"整理"机制（超容量时 AI 折叠成要点）**只在超限时触发**——19610 字节**远未触顶 12000 字符上限**（注：实际已超，见 §2.1），且折叠**不保证识别矛盾**，只做压缩。

### 2.1 容量口径已核实：**未超限，机制正常**（2026-09-17 实测）

| 口径 | 值 |
|---|---|
| `MEMORY.md` 文件字节 | 19610 字节 |
| **实际字符数**（`raw.Length`） | **11956 字符** |
| 默认上限 `noteCapacityChars` | **12000 字符** |
| 判定 | ✅ **未超，余量仅 44 字符** |

**关键**：容量按**字符**计，不按字节。中文字符 UTF-8 占 3 字节 ⇒ 字节数约为字符数的 1.6 倍，故 19610 字节 ≠ 超限。

**⚠️ 但余量只有 44 字符** ⇒ **下一次写入几乎必然触发整理**。
⇒ 这意味着整理机制**即将被实际检验**，其"折叠是否会识别矛盾"的能力（§2 指出的弱点）**马上要在真实数据上见分晓**。
⇒ **建议**：在下次写入前先人工确认行 120 作废标记已加，**避免整理把作废结论折叠成"要点"而失去作废语义**。

*（更正记录：本节初稿曾据字节数误判为"已超限 63%、疑似 bug"，实测后推翻。属推理未取证即下结论，已按纪律更正。）*

---

## 3. 攻关方案（三层，按成本递增）

### 3.1 方案 A（最小可用）· 结论条目带「有效期 + 作废指针」

**不引入新存储**，只在写笔记时约定格式：

```markdown
### 2026-09-17 · 白板线解耦边界
<!-- id: mem_xxx  supersedes: mem_yyy  status: current -->
结论：只需动 4 处...
```

- `id` = 现有锚点（**已有**，`<!-- memory:mem_32hex -->`）
- `supersedes:` = 本条作废了哪些 id（**新字段**）
- `status:` = `current` / `superseded` / `deprecated`（**新字段**）

**检索时**：命中 `status != current` 的条目 ⇒ **降权或标注"（已作废，见 <新id>）"**。

**成本**：改 `memory_note_pre` 写入模板 + `pushL0`/`semSources` 读取时过滤。**零新增文件、零 LLM 调用。**

### 3.2 方案 B（主动巡检）· 矛盾检测 lint

复用已规划的 **M2 lint** 思路（只读、只报告、绝不写盘）：
- 扫描笔记/用户级记忆，找**同一主题下的多条结论**
- 条件触发（非每轮），只在**写入新结论**时检查是否与旧结论冲突
- 输出："检测到可能的结论冲突：`mem_A`（2026-09-16，2 处）vs `mem_B`（2026-09-17，4 处）"

**成本**：中。需定义"同主题"判据（tag / 标题相似度）。**③④ 判据待用户拍板**（已在 BATTLE-PLAN 待决项中）。

### 3.3 方案 C（根治）· 结论层与流水层分离

**识别到根本矛盾**：
> 日志 = 流水（append 正确）；笔记 = 结论（append 错误）。

⇒ **拆分语义**：
- `logs/` 保持 append-only（不动）
- `MEMORY.md` **改为"活结论文档"**：写入新结论时，**同主题旧结论自动移入 `archive/conclusions-<date>.md`**

**这等于把白板的 `replace + archive` 语义推广到笔记层**——而**该机制已经存在且经过验证**（`:1984` PLAN 重写归档、`:2011`）。

**成本**：高，但**复用现成机制**，不是新建。

---

## 4. 用户另一个问题：增删改现状（纠正我此前的浅显回答）

**我此前只答了"没有删除工具"——太浅。** 补全：

| 能力 | 状态 | 位置 |
|---|---|---|
| **增** | ✅ 完整 | `memory_log/note/user_pre` |
| **读** | ✅ 完整 | `memory_read/recall/status_pre` |
| **改（覆盖式）** | ⚠️ **仅白板** | `memory_note_pre(kind=plan)` = replace |
| **改（笔记/用户级）** | ❌ **只能 append，无法改写已有结论** | —— |
| **删（硬删）** | ❌ 无，**且按设计不该有** | —— |
| **作废（软删）** | ❌ **无任何机制** | —— |
| **归档** | ✅ 有 | PLAN 重写、超容量整理 |
| **语义重排** | ✅ **不需要** | `miv` 指纹变才重算，`:4719-4748` |

**关键结论**：
1. **不存在"删记忆搞崩语义库"的风险**——没有硬删，且 miv **不因内容变化全量重建**（只在指纹变时 fail-closed 不复用）。
2. **但有更隐蔽的风险**：**改不了** ⇒ 错结论只能靠"再写一条对的"来对冲 ⇒ **正是一号标本的成因**。

---

## 5. 白板结构评估（用户第二问）

### 5.1 实测现状
| 文件 | 体积 | 行数 | 更新频率 |
|---|---|---|---|
| `PLAN.md` | **1506 字符** | 23 行 | 每次接续重写 |
| `handoff-*.md` | 2922 字节 | ~30 行 | 每次接续新增 |

**用户判断"量肯定是够了"—— 实测支持这个判断**：1506 字符远未触及 3000 预算。

### 5.2 但结构有真问题

**问题一：PLAN.md 是"项目说明书"，不是"交接单"**
现内容 = 项目介绍 + 3.0 历史 + 阶段列表。**这些是"是什么/从哪来"，不是"现在卡在哪、下一步做什么"。**
⇒ 下一个窗口读完知道项目背景，**但不知道手头活的精确状态**。

**问题二：交接账本与 PLAN.md 职责重叠**
PLAN.md 有"遗留与边界（给下一个窗口）"，账本有"进度与下一步"。**两处都写下一步 ⇒ 可能不同步（又一个小号大象）。**

**问题三：账本按时间文件名排列，但内容无"承接关系"**
`handoff-20260917-023635.md` 起头写"# 交接账本 · 2026-09-16 02:36"（**文件名日期与标题日期不一致**，因为文件名用写入时刻、标题用内容时刻）。⇒ **按文件名排序 ≠ 按内容时序**。

**问题四：无"已作废"标记**
老账本里的"下一步"如果已被新账本取代，**老账本无任何标记**，检索到会误导。

### 5.3 结构建议（不增量，只调分工）

| 账户 | 应写什么 | 不应写什么 |
|---|---|---|
| `PLAN.md` | **稳定事实**：这是什么、架构、铁律、边界。低频重写 | 不写"下一步" |
| `handoff-*.md` | **动态状态**：卡在哪、试过什么、下一步（**唯一权威**） | 不重复项目介绍 |
| `MEMORY.md` | **可复用结论** + `supersedes` 链 | 不写流水 |

**即：下一步只在一个地方写。** 这消除问题二/三/四。

---

## 6. 执行顺序（并入 BATTLE-PLAN，不打断 L 层）

用户已批准主线：

```
L3.5（附件路径）→ L4 → L5 → L6 → L7 → 全量回归绿 → 再开 L3.6
```

**本文档的攻关项**（建议编号 **G 系列**，与 L/M/S 并列，独立推进）：

| 项 | 内容 | 依赖 | 优先级 |
|---|---|---|---|
| **G1** | 核实容量口径（§2.1）：19610 字节为何未触发整理 | 无 | 🔴 立即（疑似 bug） |
| **G2** | 一号标本处置：给 MEMORY.md 行 120 补作废标记 | 无 | 🔴 立即（防误用） |
| **G3** | 方案 A：结论条目 `supersedes`/`status` 约定 + 读取时过滤 | 无 | 🟡 **已定案 → 并入 M2（见 §8）** |
| **G4** | 白板分工调整（§5.3）：下一步只在账本写 | 无 | ✅ **已完工**（三层分工已进 GUIDANCE） |
| **G5** | 方案 B：矛盾 lint（复用于 M2） | 无（判据改由"主动触发 + 写入兜底"，见 §8.4） | 🟢 **与 G3 同批 → 并入 M2** |
| **G6** | 方案 C：结论层 replace+archive 语义 | G3 落地后 | 🟢 长期 |

**L3.6**（旧对话可检索化）保持原定位置，不属于本文档管辖。

---

## 7. 待用户拍板（历史记录 · 均已裁定）

1. ~~**G1 是否立即查**~~ → 已查：容量口径**未超限**（§2.1 已更正），非 bug。
2. ~~**G2 是否立即处置**~~ → 已处置（`MEMORY.md.bak-20260917-G2` 留档）。
3. ~~**G3 的 `supersedes` 自动还是手写**~~ → **已拍板：只在结论层（`memory_note_pre`）自动比对**，见 §8.2。
4. ~~**§5.3 分工调整是否同意**~~ → 已同意并落地为 G4（三层分工进 GUIDANCE）。

---

## 8. G3+G5 合并设计（2026-09-18 用户拍板）

**用户原话**：「b➕c 应该才是正路，每次 memory-log 的时候都应该语义臂检索更新纠正记忆」+「(b) 只在 `memory_note_pre`（结论层）时比对」+「标错成 superseded 后，要不要有个撤销通道？**落到文档里，和 M2 一起做**」。

### 8.1 关键前置事实：地基已经存在（本次实测，附代码证据）

**这不是从零造轮子——机制早已建好，只是从来没有被接线。**

| # | 事实 | 证据 |
|---|---|---|
| 1 | **`status` 三值已定义**，与契约 §6 完全一致 | `l0-extract-pre.js:56` `L0_STATUSES = ['current','superseded','retracted']` |
| 2 | **`layer` 五值已定义** | `l0-extract-pre.js:53` `L0_LAYERS = ['user','project','log','reflection','whiteboard']` |
| 3 | **L0 索引已带 status 列，且进版本指纹** | `l0-index-pre.js:62` canonical `[id, l0Hash, layer, status]` → `l0idx_pre_<sha256前32>` |
| 4 | **改 status 会自动失效下游缓存** | 同上：状态进指纹 ⇒ 版本变 ⇒ 缓存 fail-closed 不复用 |
| 5 | **兼容口径已定**：缺失即默认 | `l0-index-pre.js:35` `layer→log` / `status→current`，**不因缺列拒绝整文件**，旧索引照常加载 |
| 6 | ★ **但没有任何地方在写这两个字段** | 全仓写 `status` 只有 2 处：`tier0-catalog-pre.js:283`、`wb-contract-pre.js:186`，**均为硬编码 `'current'`** |

> **结论**：缺口不是"没有机制"，而是「**机制只读不写**」。零处写 `superseded`/`retracted`。
> ⇒ 所以 G3 的工程量远小于方案 A 当时的估计：**不需要改 L0 索引、不需要改版本指纹、不需要动读取侧过滤**——那些全都有了。

### 8.2 触发范围（用户裁定：**只在结论层**）

**只挂 `memory_note_pre`，不挂 `memory_log_pre`。**

理由（与 §2 根因表同源）：
- **日志 = 流水**，append 是**正确**语义，流水里不存在"这条日志作废了另一条日志"的概念；
- **笔记 = 结论**，append 是**错误**语义，才是矛盾的滋生地。

> ⚠️ 用户原话提到"每次 memory-log 的时候" —— 但随后自己修正为「只在 `memory_note_pre`」。
> **以 `memory_note_pre` 为准**，这是更小、更准的落点；日志路径保持零改动（也符合"最热路径不加成本"）。

**链路（四环中三环已有现成机制）**：

```
memory_note_pre(结论) 被调用
   ↓
① 语义臂检索同主题旧条目        ← 已有：recallMemoryPre 的 semSources
   ↓
② 判定"本条取代了哪条"           ← ★ 唯一新增：判定 + 写状态
   ↓
③ 给旧条目写 status='superseded' ← 字段已有，只差写入方
   ↓
④ L0 版本自动变 ⇒ 缓存自动失效   ← 已有：状态进指纹
   ↓
⑤ 旧条目归档                    ← 已有：复用 PLAN 的 replace+archive
```

### 8.3 撤销通道（用户提问，**需要设计**）

**问题**：自动标错 `superseded` 后怎么改回来？

**必须有的理由**：**误判的代价不对称** —— 漏判只是"矛盾继续存在"（回到现状，可接受）；**误判会把一条还有效的结论标成作废，比不改更糟**（下一个窗口会无视它）。所以撤销通道是**必需项**，不是可选项。

**设计（三条，择一或组合，实施前定稿）**：

| 方案 | 形式 | 评价 |
|---|---|---|
| **U1** | `memory_note_pre` 加参数 `restore: [mem_id...]` ⇒ 把指定 id 的状态改回 `current` | 明确、可审计；但用户得先知道 id |
| **U2** | 面板上可点（白板/笔记页签对每条显示 status，点击切换） | 最直观；但有 UI 工作量，且属 S 层（界面层已封存到 3.1） |
| **U3** | 状态变更是**追加一条变更记录**而非原地改 ⇒ 天然可回滚 | 与 append-only 精神一致；但要注意 §3.1 已定"status 是字段不是新状态源" |

**倾向 U1**（成本最低、零 UI、立即可用），U2 留给 S 层。

> **纪律**：撤销必须**留痕**（谁在何时把它改回 `current`），否则又变成"静默改写"——那正是 §1 标本的成因。

### 8.4 与 M2 的关系（用户裁定：**一起做**）

| 项 | 形态 | 写盘？ | 归属 |
|---|---|---|---|
| **M2 lint** | `lintWhiteboardPre(entries)` → 问题清单 | ❌ **绝不写** | 只读体检 |
| **G3 状态写入** | `memory_note_pre` 内的判定 + 写 `status` | ✅ 写（经正常写入通道） | 写入侧 |

**两者共用同一个"同主题判定"能力**，但**边界必须清晰**：

- **M2 只报告**（契约 §6 纪律，违反即把白板变成状态机）
- **G3 才写**，且**只写 `status` 字段，不新建状态源**（S10.4 合规：状态归记忆条目）

> ⚠️ **不要把 G3 的写入能力塞进 M2 的 lint**。M2 报"这条可能过时"，G3 在**下次写入时**决定要不要标。**报告与写入分离**，这是两条纪律的交点。

**③④ 判据问题随之消解**：原 M2 卡在"③什么算概念/几次算反复、④什么算相关"。
⇒ 新方案下，**判据不再需要预先穷举**，改为：**主动触发（用户/模型显式说"这条取代那条"）+ 写入时兜底比对**。
⇒ 故 §7-3 的悬置问题**已解除**，M2 的 ③④ 不再阻塞。

### 8.5 风险与约束（实施前必读）

1. **★ 语义引擎铁律**：语义臂依赖引擎（**JS 内置 = 默认形态；Python = 发烧友进阶项，两者可互换、严禁互相依赖**）。
   ⇒ G3 **必须 fail-soft**：语义引擎不可用时退回**词法臂**，**绝不因此不写状态、更不得让一个引擎的存在成为另一个生效的前提**。
2. **判定阈值要保守**：宁可漏判（回到现状）不可误判（标错作废）。建议**只在高置信度时自动标**，其余**只报告不标**。
3. **`memory_note_pre` 在热路径上**：新增语义检索有成本。需实测单次开销，必要时加**同主题缓存**或只在结论内容显著时触发。
4. **不新增状态源**（S10.4）：`status` 是**已有字段**，G3 只是让它**第一次被写入**。任何"另建一份状态表"的做法都违规。

### 8.6 验收口径（能失败）

- [ ] 写一条新结论、显式声明取代旧条目 ⇒ 旧条目 `status` 变 `superseded`，**且 L0 版本指纹随之变化**。
- [ ] **误标可撤销**：走 U1 改回 `current`，且有留痕。
- [ ] **语义引擎关闭/不可用时**：退回词法臂，行为正常，**不报错、不跳过写入**。
- [ ] `memory_log_pre` 路径**零改动**（断言：日志写入不触发任何 status 写入）。
- [ ] M2 lint 跑完后**文件 mtime 不变**（只读纪律）。
- [ ] 每条都在 `tests/smoke/` 有套件，且**故意改坏实现时会红**。

---

*本文档为专项攻关纲领，执行细节落入 BATTLE-PLAN 的 G 系列。*
