# DRIFT —— 漂移账本

> 起因：2026-10-04 人工核实 7 处"文档与代码不一致"后，把它做成了**可复跑的**门（`tools/docfacts.mjs`）。
> 第一次跑：**45 处硬伤 + 34 处软警告**（范围：moobile 的 19 份"当前态"文档）。
> 本文件讲**方法与政策**；**当前清单由工具打印**，不在这里抄一份（抄一份就会漂一份）。

---

## 1. 为什么会有这个门

`tools/check.mjs` 只能保证 **skill 不撒谎**；而 skill 的事实来自 `docs/**` ——
**docs 撒谎同样致命**，而且是更上游的问题：文档错了，skill、站点、示例会一起错。

实测的样本（人工 7 处 → 工具 79 处）说明它不是偶发：绝大多数来自 **2026-09 那次 vendor 搬家**
（fork 从模块根摊平到 `vendor/rabbita/**`），文档里的路径没跟着改。

---

## 2. 三条设计决定（都是"宁可漏报也不误杀"）

### ① 只查"能从代码算出来"的事实

| 检查 | 真相来源 | 例 |
|---|---|---|
| **D1** patch 系列的长度 | `ls tools/patches/*.patch` | 文档写 33 / 15 / 14，实际 **34** |
| **D2** vendor 基准版本 | `tools/vendor.lock` 的 `RABBITA_VERSION` | 文档写 0.15.4，实际 **0.16.3** |
| **D3** 被引用的文件是否存在 | 文件系统 + 全仓 basename 索引 + **搬家前旧目录表** | `html/attrs.mbt` 在搬家后已不存在 |
| **D5** 同一事实多处矛盾 | 不判断谁对，只点名不一致 | 真机 21 项 vs 27 项 |
| **D7** README 里抄的**库版本** | `moon.mod` 的 `version` | 原先**没有任何门守着**（`readme_probe.py` 只校验 import 路径） |
| **D8** 宿主包 README 抄的**宿主版本** | `npm/moobile-host/package.json` | 同上 |
| **D6** "说未做、其实已做" | **人工台账** `done-claims.txt`（软判据） | `npm/moobile-host/README.md` 说"载荷未做"，而 `payload.mbt` 2026-09 就落地了 |

**D3 的两个补充（都是踩出来的）**：

- **搬家前的旧一级目录表 `STALE_ROOTS`**（`internal` / `dom` / `svg` / `server`）。第一版只查"一级目录在白名单里"的路径，
  于是这几种**旧路径根本不被检查** —— 而它们恰恰是漂移重灾区（`docs/ARCHITECTURE.md` 一处就有 14 个 `internal/**`）。
  教训：白名单机制天然对"已经不该存在的名字"失明。
- **basename 索引**：裸文件名（`App.js`、`create.js`）只要**全仓任何位置**存在就算过；
  一处都找不到才算**软**警告。

**D6 的边界（为什么它只能是软的）**：机器分不清"现在没做"和"**当时**没做" ——
`docs/design/DESIGN-COMPONENT-LIBRARY.md` 里"**当时没做的**：载荷在那个阶段还是不透明的"是**合法**的历史陈述。
所以 D6 带一个历史语境豁免（`当时|那时|起初|当初|原先|此前|曾经|历史上`），其余命中仍需人看一眼。

算不出真相的（例如"某个门的项数"）**绝不猜** —— D5 只报"两处不一致"，因为
**"谁对"要真跑才知道，而"两处不一致"本身就是 bug**。

### ② 两档严重度：硬 / 软

- **硬**：带 `/` 且一级目录在白名单**或搬家前旧目录表**里（参照系确定）+ D1/D2/D5/D7/D8 ⇒ 判红。
- **软**：裸文件名在全仓一处都找不到（`desktop_host_probe.mjs`、`art-base.js`）⇒ 只警告。

为什么分档：裸名多半是行文泛指（`App.js`、`create.js`）。第一版没分档，D3 直接刷出
**248 条**，其中绝大多数是误杀 —— **一条误杀满天飞的门，下场是被关掉。**

### ③ 豁免必须写在文档里

- 行内写 `docfacts:ignore` → 该行跳过；
- `<!-- docfacts:off -->` … `<!-- docfacts:on -->` → 区间跳过。

适用场景：`FORK.md` 的 patch 小节**刻意**用搬家前的路径（patch 文件里就是那些路径），
那是有意为之的历史引用 —— 但它必须**显式标注**，不能靠读者猜，更不能藏在脚本的忽略列表里。

---

## 3. 跑法

```bash
cd skillpress
node tools/docfacts.mjs              # 对账 ../moobile
node tools/docfacts.mjs --root <dir> # 换根目录
node tools/docfacts.mjs --selftest   # 证伪：6 个诱饵必须点名 + 正例必须零发现
```

自检现状：**7/7**（诱饵：patch 数 / vendor 版本 / 路径不存在 / 搬家前的旧一级目录 / 项数矛盾 / 缺席说法）。

**扫描范围**（2026-10-04 两次扩范围，都是"门不扫的地方真的在漂"逼出来的）：

| 范围 | 份数 | 查什么 |
|---|---|---|
| 仓库根的当前态文档 | 19 | D1–D5 / D7 / D8（全套） |
| `examples/apps/<app>/README.md` | 16 | 同上 —— 扩进来立刻就抓到 `canvas-spike` 里"画布画中文没上过真机" |
| `npm/moobile-host/*.js` 的**代码注释** | 9 | **只查 D6** —— 那些文件头在做"验证到哪一步了"的声明，而它们是代码、不在 `docs/**` 里（活样本：`canvas-skia.js` 的文件头比 README 晚改口） |

⚠️ **`docs/STATUS.md` 曾在排除名单里，那是个设计错误**：它自称"现状与分数（唯一来源）"，
是**当前态**文档。排除它 = 让唯一来源免于检查，而实测它恰恰是最陈旧的那一份
（把已在真机量到像素的"文字字形"仍写成未验）。现在它在扫描范围里。
真正算"历史"的只有 `CHANGELOG.md`（按版本记）、`docs/FINDINGS.md`（按轮次记证据）、`PLAN.md`（计划）。

---

## 4. 政策：谁改、按什么顺序

1. **先改文档的家，再改投影。** 门红了先动 moobile 的 docs；不许只改 skill 让它变绿。
2. **一处事实只在一处写。** 数字（版本、项数、patch 数）只写在 `docs/STATUS.md`，
   别处一律给指针 —— 这是 D1/D2 会反复红的根因。
3. **不许把忽略当修复。** `docfacts:ignore` 只用于"**故意**引用旧布局"的情形，
   并在同一行写清为什么。
4. **改完重跑门**（`--selftest` 也跑：门的洞和文档的洞一样会退化）。

---

## 5. 已知的边界（别当成已覆盖）

| 边界 | 说明 |
|---|---|
| 结构速览类内容 | `DEV.md` 的项目结构图是**手画的**，且已经过时（还画着 `internal/`）。本门抓不到 —— v1 想法：让它变成**生成区**（复用仓库自己那套"MIGRATION.md 两区分家"的手法） |
| 渲染型/叙述型事实 | "某个功能支持不支持"这类只能靠 `claims.txt`（G7）与人工复核 |
| 记录类文档 | `CHANGELOG` / `FINDINGS` / `STATUS` / `PLAN` **刻意不扫** —— 历史日志允许留旧数字，这正是它们存在的意义 |
| 项数的真假 | D5 只说"矛盾"，不说"谁对" |
