docs(skill): stale-doc-count-sweep 收进第四轮实测教训(双源互证 / 伪命中 / 口径错)

- 新增「各计数权威口径速查表」:黄金参考张数、已绑叶数、钩子数、字库字数、字形数、
  菜单节点与叶子,每行给至少两个独立来源与已知伪命中的换算式。
- 真值步骤升级为「先剔伪命中,再双源互证」:菜单 #page= 首条是文件头注释示例行
  (计数 29 对注册表 28)、字库 bitmap_index 314 条含 notdef 故字形数 313、钩子数
  只认 YAML 解析后的条目数(grep 声明行会多算一条注释掉的候选)。
- 分类从四类增到五类:新增「数字对但口径描述错」——必须另起一笔只改口径句,混进
  数字同步那笔会让复核者以为只是顺手改措辞。
- 命中面明确宽于文档目录:含 .pre-commit-config.yaml 钩子显示名、AGENTS.md、
  skills 正文自带的计数与脚本路径。
- 新增两条硬步骤:「一笔只装一类事」、「提交后再按全部旧形态复扫归零」;并纠正
  变更记录新行位置(本仓按日期升序 → 追加为末行,不是插表顶)。
- 编号门禁口径同步到只查新增行的那道(全库语料审计已退役进归档目录)。
- 修正该 skill 自身一处错命令:把自检旗标写成了无连字符形态,实测报
  unrecognized arguments(rc=2);改成正确旗标后内建正反例自检通过。
This commit is contained in:
2026-10-28 16:40:15 +08:00
parent 6b2c6ced1b
commit b3336ab0c3
+83 -54
View File
@@ -1,61 +1,75 @@
---
name: stale-doc-count-sweep
description: 全仓陈旧计数同步 playbook——某个事实数(黄金参考页数、菜单树叶数、钩子数等)变了之后,从权威来源实测真值 → 全形态扫描命中点并逐条分类(当前态改 / 变更记录历史行只加当日新行 / 同数不同义误报不动 / 带日期戳的测量只加「当时」锁口径)→ 逐条字面替换且每处断言命中数 → 被改文件全文通读核对各小节 → 复扫残留当前态命中归零 → 跑门禁分笔提交。
whenToUse: 某个计数/数字变了要跨 md/yaml/py 同步、用户说「同步一下 / 全仓改一遍 / 这个页数(叶数、钩子数)不对」、发现文档数字与实测不符、或同一份文档两节数字/顺序对不上要先核口径时。
description: 全仓陈旧计数同步 playbook——某个事实数(黄金参考页数/张数、菜单树叶数与已绑叶数、字库字数与字形数、提交钩子数等)变了之后:每个数先剔伪命中再双源互证定真值 → 全形态全命中面扫描(含 .pre-commit 钩子显示名与 .dsh/skills 正文)→ 逐条分类(当前态改 / 变更记录只追加当日新行 / 同数不同义不动 / 带日期戳的测量只加「当时」/ 数字对但口径描述错另起一笔)→ 每处先读现文再字面替换并断言命中数 → 全文通读核对各小节 → 提交后再按全部旧形态复扫归零 → 跑门禁分笔提交。
whenToUse: 某个计数/数字变了要跨 md/yaml/py/txt 同步、用户说「同步一下 / 全仓改一遍 / 这个页数(张数、叶数、字形数、钩子数)不对」、怀疑某个数是伪命中或口径错了(含 notdef 吗、含 ASCII 吗、是张数还是页数)、发现文档数字与实测不符、或同一份文档两节数字/顺序对不上要先核口径时。
---
# 陈旧计数全仓同步
适用:**一个事实数变了**(黄金参考页数 27→40→47、菜单树叶数、钩子数…),仓库里多处引用它的当前态表述要跟着变,而历史记录、同数异义、带日期戳的测量**不能**跟着变。
适用:**一个事实数变了**(黄金参考 27→40→47→53→60、已绑叶数 22→28、字库 185/280→218/313、钩子 51→52→51…),仓库里多处引用它的当前态表述要跟着变,而历史记录、同数异义、带日期戳的测量**不能**跟着变。
同一流程在本仓已跑过两次(`b24b2b5f` 27→40、13 个文件;`31ca4a1f` 40→47、11 个文件)——照下面的序列逐字复跑,别临场重新发明分类。
本仓已跑过四轮,照序列复跑、别临场重新发明分类:`b24b2b5f`(27→40,13 文件)、`31ca4a1f`(40→47,11 文件)、UI 补齐批(`94b0d790` 落地 → `cbb4fdb8` 产品需求同步 → `11d60057` 架构总览补漏 → `6daa01ae` 字体**口径**更正)、钩子一增一减(`1668842d`,52→51)。
## 各计数的权威口径速查(本仓实测值,动手前先对表)
| 计数 | 真值来源(**至少两处互证**) | 已知伪命中 / 换算 |
|---|---|---|
| 内容页黄金参考**张数** | `simulator/tests/ref/` 下 `shot_*.bin` 实数(实测 60)= `simulator/tests/check_content_pages.py` 的 `PAGES` 条目数(实测 60) | 分页页要**单独再拍一张**(id 加 `_p2`,如 `runreport_p2`)→ **张数 = 页面个数 + 分页多出的张数**;两个数分开报,不许互相冒充(53 张时代的一个 12 行分页页会让 55 页变 56 张这类差 1) |
| 已绑内容页**叶数** | `Drivers/BSP/UI/ui_page_registry.h` 的 `#define UI_PAGE_MAP_COUNT 28u`(由 `tools/menu_gen/gen_menu.py` 生成) | `grep -c '#page=' ui_menu_tree.txt` 得 **29**:第 5 行是菜单文件**头部注释里的示例行** → 剔掉注释/模板行后的 28 才是真值 |
| 提交**钩子数** | YAML 解析后的条目数(实测 **51**):自查命令见 `docs/门禁说明/README.md` §2 首条;与 AGENTS.md「共 N 个钩子」、该目录 §2 标题**三数同源** | `grep -c` 配置里的声明行 = 52(多算一条**被注释掉的** clang-format 候选);在配置**注释里复述那个 grep 字面量**会让文本计数再虚高 1 |
| 字库**唯一字数** | `ui_cn_chars.txt` 唯一码点数(实测 218,其中 ASCII 区 **0** 个) | `make_font.py` 的 `-r 0x20-0x7E` 是 ASCII 子集参数,**不算进字数** |
| **字形数** | `Drivers/BSP/UI/ui_font_cn_12.c` 里 `.bitmap_index` 出现次数 **314 条含 notdef → 313 字形**;换算式 = 218 汉字/符号 + ASCII 95 = 313 | 直接报 314 就是把 notdef 当字形;文档必须写出「不含 ASCII」「含 notdef 共 314 条」这类口径句 |
| 菜单**节点数 / 叶子数** | `ui_menu_tree.txt` 头部规则:缩进即层级(每级 2 空格)、`#` 开头为注释、空行忽略;叶子 = **不存在更深缩进后继行**的行 | 42 是节点行数、33 才是叶子;`未绑 = 待做 + 不做` 要与分类表逐项闭合 |
## Steps
1. **实测真值,不抄任何二手数字**(不抄用户口述、不抄另一处文档)。
1. **实测真值:先剔伪命中,再双源互证**(不抄用户口述、不抄另一处文档)。逐数走上面那张表;两个来源对不上时**先找分歧根因**(把全部命中逐条打印出来看),不要取第一个、不要取平均,并在结论里写明分歧与取舍(实测:菜单 `#page=` 计数 29 对 注册表 28,根因是头部注释示例行)。
```bash
cd "<repo>"
ls simulator/tests/ref/*.bin | wc -l # 内容页黄金参考页数(唯一判据 = 入库的 shot_*.bin 实数)
ls simulator/tests/ref/shot_*.bin | wc -l # 张数(判据 = 入库实数)
awk '/^PAGES = \[/,/^\]/' simulator/tests/check_content_pages.py | grep -cE '^ *\("' # 页数(第二个来源,两者必须一致)
grep -n '#page=' ui_menu_tree.txt | head -3 # 先看首条是不是注释示例行,再定叶数
grep -rn 'UI_PAGE_MAP_COUNT' Drivers/BSP/UI/ui_page_registry.h # 已绑叶数(生成物)
grep -c '\.bitmap_index' Drivers/BSP/UI/ui_font_cn_12.c # 314 = 含 notdef;字形 = 该数 - 1
& '.venv\Scripts\python.exe' -c "import yaml;d=yaml.safe_load(open('.pre-commit-config.yaml',encoding='utf-8'));print(sum(len(r['hooks']) for r in d['repos']))" # 钩子数口径
```
菜单树叶数这类**来源自带口径规则**的数,先读来源文件头再计数:
⚠ **权威脚本输出乱码时**(`check_font_charset.py` 在 GBK 代码页下整行打印成 U+FFFD 替换字符串 + `: PASS`,数字全读不出来),**改为直接数源文件里符号出现次数**(`.bitmap_index` / `.range_start` / 码点唯一化)——不许臆读乱码,也不许因此退回去抄文档里的旧数。
2. **全形态 + 全命中面扫描**。一个数一次列出它全部历史形态,别只扫当前旧值:本轮 = `122/217`、`179/274`、`185/280`、`47 → 53`、`53 → 60`、`PAGES = 53`、`UI_PAGE_MAP_COUNT = 22`、`已绑 22 叶`、`未绑 11`、`内容页 53 页`。
```bash
sed -n 1,12p ui_menu_tree.txt # 文件头写明「缩进即层级,每级 2 空格;# 开头为注释;空行忽略」
grep -rn --include=*.md --include=*.py --include=*.yaml --include=*.txt --include=*.c --include=*.h \
-E '122 唯一字|217 字形|179 汉字|274 字形|185 唯一字|280 字形|47 → 53|53 → 60|内容页 53|PAGES = 53|UI_PAGE_MAP_COUNT = 22|已绑 22|未绑 11' . \
| grep -v -E '^\./(ci-artifacts|\.mnemon|\.dsh/runs)/'
```
叶子判据 = **不存在更深缩进后继行**的行。把「节点行数」「叶子总数」「已绑」「未绑」四个数一次报全,并让
`未绑 = 待做 + 不做` 与分类表逐项闭合。实测量到 42 个节点行、33 个叶子、已绑 21、未绑 12(= 待做 7 + 不做 5)。
**命中面比「文档」宽**,至少包含:`docs/**`、`AGENTS.md`、`.pre-commit-config.yaml`(**钩子 `name:` 显示名内嵌页数**:`name: UI 像素回归(菜单 8 状态 + 内容页 60 页 黄金参考)` 就是命中点)、`scripts/*.py`、`simulator/**/README.md`、以及 **`.dsh/skills/*/SKILL.md`**(本窗 `font-regen` 正文自带 179/274 计数,直到 step 150 才发现;编号门禁退役那轮 `id-family-semantic-migration`、`task-worktree` 两个 skill 正文与 `scripts/README.md` 同样是引用面)。
2. **全形态扫描**,一个形态都不许只扫一种写法:
先列出 `文件:行:全文`,**判定前必须看整行**——`cut -c1-120` 会截掉后半句(那里常写着日期戳或口径)。
```bash
grep -rn --include=*.md --include=*.py --include=*.yaml --include=*.txt \
-E '40 *(页|张|帧|个页面)|内容页.{0,6}40|黄金.{0,6}40' .
```
先列出 `文件:行:全文`,**判定前必须看整行**——`| cut -c1-120` 会截掉后半句(那里常写着日期戳或口径)。
3. **逐条分类,四类各走各的路**(这是本 playbook 的核心,别用一条规则套全部命中):
3. **逐条分类,五类各走各的路**(本 playbook 的核心,别用一条规则套全部命中):
| 类别 | 判定 | 动作 |
|---|---|---|
| 当前态陈述 | 描述「现在是多少」 | 改成实测真值 |
| 变更记录历史行 | 形如「27 → 40 页」的当日事实 | **原样保留**;只在该文件 §变更记录**顶部加一行**当日的「40 → 47」(含判据与实测来源) |
| 变更记录历史行 | 形如「47 → 53 页」的当日事实 | **原样冻结不回改**;在该文件 §变更记录**追加当日新行**(本仓变更记录表按日期升序 → 新行是**末行**,不是表顶;含判据与实测来源) |
| 同数不同义 | `最坏路径 < 40KB` 这类内存/绘制缓冲红线 | **不动**,并在结论里写明「为什么不动」 |
| 带日期戳的测量 | 「2026-10-26 实跑 …覆盖 40 页中的」 | **不臆改新数**,只补「当时」两字锁住口径;新数留待下次实跑回填 |
| 带日期戳的测量 | 「2026-10-26 实跑 …覆盖当时 40 页中的」 | **不臆改新数**,只补「当时」两字锁口径;新数留待下次实跑回填 |
| 数字对但**口径描述错** | 数没错、句子把口径讲错了(实测:写「218 字(含 ASCII/符号,共 313 字形)」,而 `ui_cn_chars.txt` 的 218 个码点**全为非 ASCII**) | **另起一笔只改口径的提交**(`6daa01ae`),不与数字同步混笔;这类命中在任何数字 diff 里都看不见,只能把口径句逐句读一遍 |
拿不准就回到第 2 步把整行打印出来再判;判错历史行 = 把当天事实改写成假账。
拿不准就回到第 2 步把整行打印出来再判;判错历史行 = 把当天事实改成假账。
4. **逐条字面替换,每处断言命中数**(弃用整文件正则批处理)。脚本写成临时文件再跑(内嵌 heredoc/模板字符串里的反引号会截断整脚本),按**字节**替换以保 BOM 与行尾不动:
4. **每处先读现文,再逐条字面替换并断言命中数**(弃用整文件正则批处理)。本窗两度返工都是没先读现文:step 45 断言 `HIT 0` 抛错(目标串与现文不一致),step 44 是脚本语法错。脚本写成临时文件再跑(内嵌 heredoc/模板字符串里的反引号会截断整脚本),按**字节**替换以保 BOM 与行尾不动:
```python
import pathlib, sys
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
JOBS = [ # (文件, old, new, 期望命中数)
('AGENTS.md', '内容页 40 页黄金参考', '内容页 47 页黄金参考', 1),
('docs/UI设计/UI_自动化测试流程.md', '该集合现已扩至 40 页', '该集合现已扩至 47 页', 1),
('AGENTS.md', '内容页 53 页黄金参考', '内容页 60 页黄金参考', 1),
('.pre-commit-config.yaml', '内容页 53 页 黄金参考', '内容页 60 页 黄金参考', 1),
('docs/架构设计/项目架构总览.md', '122 字/217 字形', '218 字/313 字形', 1),
('docs/UI设计/UI_自动化测试流程.md', '文本断言 **310 条**、覆盖 40 页中的', '文本断言 **310 条**、覆盖当时 40 页中的', 1),
]
for path, old, new, expect in JOBS:
@@ -63,55 +77,70 @@ whenToUse: 某个计数/数字变了要跨 md/yaml/py 同步、用户说「同
o, n = old.encode('utf-8'), new.encode('utf-8')
got = raw.count(o)
assert got == expect, f'{path}: 命中 {got},期望 {expect}(未写入)'
assert o != n, f'{path}: old 与 new 相同'
p.write_bytes(raw.replace(o, n))
print(f'OK {path}: {old} -> {new} x{got}')
```
每行打印 `old -> new xN`,收口时这串输出就是「改了哪几处」的账。脚本报错/没编译过,**先 `git diff --stat` 确认零写入**再换写法(不要在半落地状态上继续叠改)。
每行打印 `old -> new xN`,收口时这串输出就是「改了哪几处」的账。脚本报错/没编译过,**先 `git diff --stat` 确认零写入**再换写法(不要在半落地状态上叠改)。
5. **被改文件全文通读,核对其余小节**——不是只看 diff。检查同一文档内互相引用的小节(典型:`docs/任务规划/README.md` §1 当前执行表 vs §2 全项目顺序表;`UI补齐.md` §1 口径 vs §2 清单):顺序、计数、完成状态必须一致。若发现上一笔自己写进文档的数**口径就错了**(例如把 42 个节点行当叶子数写成「未绑 21 叶」),明确认错并改正,同时把**可复核的重算方法**写进文档(「按缩进判叶子:有更深缩进后继者即非叶子」),防止再错。
- **反引号**:TS 侧先用可见占位符(如 `~TB~`)写文本,落盘前再还原成真反引号 —— `const BT = String.fromCharCode(96); const q = s => s.split(TOKEN).join(BT)`(本窗实测:直接把带反引号的串塞进模板字符串/heredoc,整批零落地或把变更记录行写坏)。用 `tools.edit` 批量改时同样先 `await tools.read` 现文再传 `old_string`;批量里若某条 `old_string` 与 `new_string` 相同会报 `must differ` —— **只有该条作废、其余已落盘**,别整批重跑。
- **历史行被写坏**(反引号及紧随其后的字符经工具传递被吞 → 变更记录行只剩半行、尾部出现 `??` 这类残迹):立刻按**无反引号兜底写法整行重写**——裸词 `accident / oper / alarm / start / yk`、页面绑定写成不带反引号的 `#page=`——而不是拿同样带反引号的串再改一遍(实测连返工三次:step 81→82→83→85)。
6. **复扫残留**,排除历史行与误报后,当前态命中必须为 **0**:
5. **一笔只装一类事**。数字同步一笔(`cbb4fdb8`:11 处 + 变更记录一行);口径更正另起一笔(`6daa01ae`);提交后复扫逮到的漏网当前态再补独立一笔(`11d60057`)。钩子一增一减写成一个当日行的两个阶段(51 → 52 加 `id-check-new`、随即 52 → 51 摘 `id-check`),别写两行互相打脸。提交说明如实写(「先认一笔自己的错:上一笔的未绑 21 叶是探针错」),别把口径修正包装成「补充说明」。
6. **被改文件全文通读,核对其余小节**——不是只看 diff。同一文档内互相引用的小节必须一致(本窗产品需求:§2.4 立项未排、§5.14 FR-SOE-3、§8.2 绑定现状、§8.3 已绑分页表、§9.1 差距表、§9.2 顺序表六处同时引用这四个数;`docs/任务规划/README.md` §1 当前执行表 vs §2 全项目顺序表)。若发现上一笔自己写进文档的数**口径就错了**,明确认错并改正,同时把**可复核的重算方法**写进文档(「按缩进判叶子:有更深缩进后继者即非叶子」),防止再错。
7. **提交后再复扫一遍**(收尾硬步骤,不许省,也不许只在提交前扫)。按第 2 步的**全部**旧形态整仓再扫一次,逐条分类为 当前态 / 历史行 / 同数不同义:本窗 `cbb4fdb8` 提交后复扫得 14 条命中,12 条以上都在变更记录历史行(`47 → 53`、`53 → 60`、`122/217` 冻结),**唯一残留的当前态命中**是 `docs/架构设计/项目架构总览.md` §3「界面层」那行 → 单独补一笔 `11d60057`。说不出归属的就是漏改。
```bash
grep -rn --include=*.md --include=*.py --include=*.yaml -E '40 (页|张)|40 个内容页' \
AGENTS.md .pre-commit-config.yaml docs scripts | grep -v '27 → 40'
grep -rn -E '22 叶|未绑 11|11 叶|53 页|122 唯一字|217 字形|185 唯一字|280 字形|内容页 53|PAGES = 53|UI_PAGE_MAP_COUNT = 22' \
. | grep -v -E '^\./(ci-artifacts|\.mnemon|\.dsh/runs)/'
```
剩下每一条都要能说出归属(历史行 / 误报 / 已加「当时」)。说不出来的就是漏改。
7. **跑门禁,分笔提交**(`git commit` 属 Ask first,须用户明确指示后再做):
8. **跑门禁、分笔提交**(`git commit` 属 Ask first,须用户明确指示后再做):
```bash
./.venv/Scripts/python.exe scripts/check_doc_links.py --all
./.venv/Scripts/python.exe scripts/ci_local.py # 或 pre-commit run --files <被改文件>
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all --sections-strict # 期望「文档路径检查通过(N 个文档)」
& '.venv\Scripts\python.exe' scripts/check_ids_new.py --self-test # 新增行编号门禁(旧 check_ids.py 已退役进 scripts/archive/id_gate/)
& '.venv\Scripts\python.exe' scripts/ci_local.py # 或 pre-commit run --files <被改文件>
```
PowerShell 形态把 `./.venv/Scripts/python.exe` 换成 `& '.venv\Scripts\python.exe'`。提交信息正文长,一律 `git commit -F <UTF-8 无 BOM 文件>`,别用 `-m`(中文长标题易超 100 字符、终端代码页会转码)。
一笔只装一类事:计数同步一笔、口径/登记簿修正一笔。提交说明如实写(如「先认一笔自己的错:上一笔的未绑 21 叶是探针错」),别把口径修正写成「补充说明」。
PowerShell 形态把 `./.venv/Scripts/python.exe` 换成 `& '.venv\Scripts\python.exe'`。提交信息一律 `git commit -F <UTF-8 无 BOM 文件>`,别用 `-m`(中文长标题易超 100 字符、终端代码页会转码);标题 ≤100、**正文每行 ≤80**(实测把正文压到 66/78 字符才过 gitlint)。
## Pitfalls
- **照抄用户给的数**:用户说「从 40 扩到 47」也要自己数 `ref/*.bin`;数出来不一定是 47。真值只认权威来源实测。
- **把节点行当叶子数**:42 是 `ui_menu_tree.txt` 的节点行数,33 才是叶子;文件头写着「缩进即层级」,不读规则就数错(实测把错数写进两份文档,下一轮才自查出来)。
- **整文件正则批处理**:第一次尝试的正则没编译过、整批零落地。改法 = 逐条字面替换 + 每处 `assert count == expect`,宁可慢也要每处可审计。
- **反引号截断**:脚本里含 `ui-golden`、`#page=` 这类反引号时,经 TS 模板字符串/内嵌 heredoc 传递会被截断,表现为「整批零落地」。把脚本落成文件再执行,或用 `chr(96)` / `String.fromCharCode(96)` 拼。
- **只改记得的那节**:上一笔只改 §2 与开篇、漏改 §1,用户当场问出「§1 说 16 叶、§2 说剩 3 项,自相矛盾」。改完必须全文通读同一文档的所有小节。
- **历史行被当陈旧值改掉**:变更记录里的「27 → 40」是当天事实,改了就是伪造历史;正确动作是另加当日新行。
- **误报跟着改**:「< 40KB」是绘制缓冲红线,与页数无关,改了就是引入错误。
- **带日期戳的测量臆改新数**:「2026-10-26 实跑取数 …覆盖 40 页」改成 47 就是把历史测量冒充现值;只加「当时」。
- **把伪命中计进真值**:`grep '#page='` 的首条是菜单文件头部注释里的示例行,29 里含它;真值是注册表 `UI_PAGE_MAP_COUNT` 的 28。任何「数出现次数」式计数,先打印全部命中逐条看,剔注释行/模板行/示例行。
- **字形数没剔 notdef**:`.bitmap_index` 314 条里第 1 条是 notdef,字形数是 **313**;照抄 314 就把占位符当成了字形。
- **数字对但口径描述错**:218 这个数两处都对,错在「含 ASCII/符号」这句口径(实测 218 个码点全为非 ASCII)。它不进任何数字 diff,只能逐句读口径句;且必须**另起一笔**,混进数字同步那笔会让 review 者以为只是顺手改措辞。
- **权威脚本乱码就臆读或退回抄文档**:Windows 代码页下 `font-charset` 的整行输出退化成 U+FFFD 替换字符时,改数源文件符号出现次数,别猜、别抄旧文档。
- **钩子数用 `grep` 声明行**:多算一条注释掉的候选钩子(51 变 52);反过来在配置注释里复述那个 grep 字面量会让文本计数再虚高 1。口径只认 YAML 解析后的条目数,且要与 AGENTS.md、`docs/门禁说明/README.md` §2 标题、`ci_local.py` 实跑 passed+failed+skipped 三数同源。
- **张数与页数互相冒充**:一个 12 行分页页第 2 页要单独拍一张,于是「+2 页」可能等于「+3 张」。两个数分别报、分别换算,别互相顶替。
- **只扫 docs/**:漏 `.pre-commit-config.yaml` 钩子 `name:` 显示名里的页数、AGENTS.md 钩子清单、`.dsh/skills/*/SKILL.md` 自带的计数与脚本路径(skill 正文也是命中面:font-regen 的 179/274 本窗最后才改)。
- **提交完就收工**:第 7 步的提交后复扫才是把漏网当前态(架构总览 §3)揪出来的那一步;漏了它,仓库里就留下一行「当前态写旧数」直到别人再扫。
- **变更记录新行插到表顶**:本仓三份文档(产品需求 §12、架构总览 §10、`docs/门禁说明/ui-golden.md`)的变更记录都按日期升序 → 新行**追加为末行**。插表顶会把首行「初版」挤成第二行,读起来像被改过序。
- **历史行被当陈旧值改掉**:「47 → 53 页」是当天事实,改了就是伪造历史;正确动作是加当日新行。**历史行被写坏**时按无反引号兜底写法整行重写,别用同样写法再改一遍。
- **照抄用户给的数**:用户说「从 40 扩到 47」也要自己数 `ref/*.bin`;数出来不一定是 47。
- **把节点行当叶子数**:42 是 `ui_menu_tree.txt` 的节点行数、33 才是叶子;不读文件头规则就数错(实测把错数写进两份文档,下一轮才自查出来)。
- **整文件正则批处理**:第一次尝试的正则没编译过、整批零落地。改法 = 逐条字面替换 + 每处 `assert count == expect`。
- **反引号截断**:脚本里含 `ui-golden`、`#page=` 这类反引号时,经 TS 模板字符串/内嵌 heredoc 传递会被截断,表现为「整批零落地」或「历史行写坏」。落成脚本文件再执行,或用 `chr(96)` / `String.fromCharCode(96)` 拼。
- **只改记得的那节**:上一笔只改 §2 与开篇、漏改 §1,用户当场问出「§1 说 16 叶、§2 说剩 3 项,自相矛盾」。改完必须全文通读同一文档所有小节。
- **误报跟着改**:「< 40KB」是绘制缓冲红线,与页数无关。
- **带日期戳的测量臆改新数**:「2026-10-26 实跑 …覆盖 40 页」改成 47 就是把历史测量冒充现值;只加「当时」。
- **用截断的 grep 输出判定**:`cut -c` 截掉的后半句常含日期戳/口径,据此分类必错。
- **新写文本撞编号门禁**:新增的「单字母+数字」裸形(如 `SD2506`、`CP56Time2a`)会把 `id-check` 的不可判定引用绝对数推高(实测 1156 → 1161,上限只剩 5)。收尾看门禁输出的 `und` 计数,涨了先把自己这批文本逐处归因,别去放宽 `noise_literals`。
- **新写文本撞编号门禁**:新增的「单字母+数字」裸形(如 `SD2506`、`CP56Time2a`)会被 `id-check-new`(只查 `git diff` 新增行、零语料)拦下;旧的八项全库校验 `id-check` 已于 2026-10-28 退役进 `scripts/archive/id_gate/`。撞到就把自己这批文本逐处归因或改形态描述,别去放宽白名单。
## Verification
提交前逐条打勾:
提交前逐条打勾;回报时**逐处列文件 + 命中数断言**,不要笼统说「已全部更新」。
- [ ] 真值来自本轮实测命令的输出(`ref/*.bin` 计数 / 按来源规则重算的叶数),并在结论里贴出该数字
- [ ] 分类表逐条列出:改了哪些当前态、哪些历史行保留、哪些误报不动(附理由)、哪些只加「当时」
- [ ] 替换脚本打印了每处 `old -> new xN`,且每处 `assert` 通过(无半落地状态;报错时已确认零写入)
- [ ] 被改文件已全文通读,同文档各小节(§1/§2)与配套 README/清单的顺序、计数一致;口径闭合(如 `未绑 = 待做 + 不做`)
- [ ] 复扫输出中当前态残留 = **0**,每条剩余命中都能归类
- [ ] `check_doc_links.py --all` 打印「文档路径检查通过(N 个文档)」;`ci_local.py` / `pre-commit` 全绿(`passed=N failed=0`、RC=0)
- [ ] 每个数都写了**两个独立来源**且数值一致(`ref/shot_*.bin` 实数 = `PAGES` 条目数;`UI_PAGE_MAP_COUNT` = 剔伪命中后的 `#page=` 计数),并在结论里贴出该数字与伪命中取舍理由
- [ ] 字形数说明**是否剔除 notdef**并给出条目数→字形数的换算(314 − 1 = 313);字数说明**是否含 ASCII**,且该口径句与实测一致
- [ ] 分类表逐条列出:改了哪些当前态、哪些历史行保留、哪些误报不动(附理由)、哪些只加「当时」、哪些属**口径错**并已另起一笔
- [ ] 替换脚本打印了每处 `old -> new xN` 且每处 `assert` 通过(无半落地;报错时已 `git diff --stat` 确认零写入)
- [ ] 命中面覆盖 `.pre-commit-config.yaml` 钩子 `name:`、AGENTS.md、`docs/**`、`scripts/**`、`.dsh/skills/*/SKILL.md`,逐条在改动清单里出现
- [ ] 被改文件已全文通读,同文档各小节与配套 README/清单的顺序、计数、完成状态一致;口径闭合(如 `未绑 = 待做 + 不做`)
- [ ] 变更记录:历史行原文未动,只新增当日一行(升为末行);钩子一增一减写成同一行的两个阶段
- [ ] **提交后**又按全部旧形态复扫一次,当前态残留 = **0**,每条剩余命中都能归类(历史行 / 误报 / 已加「当时」);发现的漏网当前态已单独补一笔
- [ ] `check_doc_links.py --all --sections-strict` 打印「文档路径检查通过(N 个文档)」;`ci_local.py` / `pre-commit` 全绿(RC=0、`failed=0`);BOM 与原文件一致、无 `EF BF BD`
- [ ] 分笔提交完成,回报 commit 号 + 每笔改了哪些文件 + 没动的那几类及理由