feat(门禁): 第 51 条 nested-comment——把 ARMCC #9-D 的按包含者刷屏拦在提交前

动机是今天一天复发三次的同一形态:在块注释里写作用域路径,那个 `/*` 被 ARMCC 当第二次注释起点(诊断 9-D),
而且**按包含者数量刷屏、只在重编译那个 TU 时才暴露**——本仓的诱因就是「注释里写 Drivers/BSP/UI/**」。
AGENTS 早就写了这条铁律(写全目录或用尖括号),但它一直是纯口头纪律,所以同一天还是复发三次。

新脚本 scripts/check_nested_comment.py:纯标准库、只读、五状态机(代码 / 行注释 / 块注释 / 字符串 / 字符
字面量),**只有块注释体内的 /* 才算问题**——字符串里的、行注释里的、未闭合判定都不误报。口径上刻意与钩子
的 types: [c] 对齐(403 个 .c/.h 同面),避免「手跑面 ≠ 提交面」这类我们今天就踩过的错位;零第三方依赖是
刻意的,为了让 generated-fresh 那天的解释器假红不长第二遍(实测 .venv 与机器级 C:Python314 两种解释器结论
一致)。

存量口径:全仓实扫**新发 0、存量 2 处**(test/host/common/ch395f_fake.h:7 与 fatfs_fake.h:9,同一形态
「注释里写作用域 test/host/**」)。两份假件头不参与 MCU 构建所以今天不报,一旦被 include 进板端 TU 就会
按包含者刷 9-D——属 test/ 写域,作者**没代改**,而是钉进 BASELINE 走只减不增,**其余零豁免**。

自证有一条必须写清的诚实边界:作者想「拿旧字节回放今天那三处真复发」,实测**做不到**——近 150 个提交里被
改过的 281 份 .c/.h 旧字节**零命中**,因为那三处同类从来没有一处在 HEAD 里留过字节(全靠人工编译后清掉)。
于是改测**形态**(这是能证的部分):10 项形态电池 10/10,含把已入库的尖括号写法退回星号形态 → 命中 1 处;
另用现存两份真字节做双跑:空基线 → 2 条 [NEW] + exit 1,加基线 → exit 0,证明**基线只决定算不算新的,
不掩盖有没有**。编译器侧原文(run_par.c(267) #9-D)冻结抄进新文档 §2.1——构建日志每次 @build 被覆盖且不入库,
所以文档里刻意没写「最近一次日志里仍有」这种会过期的话。

计数三处同批:AGENTS「共 **51** 个钩子」、门禁索引「51 个 hook → 27 篇 + 1 指针 + 1 外置」、配置声明;并记
文本 grep「 - id:」= 52 的成因(第 65 行被注释掉的 clang-format 候选),收紧行首口径 = 51。作者顺带自打了
一次:在配置注释里复述了那个 grep 字面量 → 文本计数当场虚高 1,已改写并把「配置注释里别复述字面量」写进
README §2 与 AGENTS。

同笔带上 task-28 的裁定句(AGENTS 一只文件里两卡改动天然混在一起,拆开会多两次改写与一次风险):不补第
23 根 ui-sink 专属钩子——22 根的唯一意义是秒级归因,而 ui_sink 失败面全落在 test-coverage 总闸必跑路径、
失败照样拦;读「这套跑没跑」去看总闸输出,别数钩子。

实测与本笔自查:validate-config = 0;nested-comment --selftest 4/4、--all = 403 / 新发 0 /
存量 2 → exit 0;
doc-links --sections-strict(4 篇)= 0;gen_task_table --check = 0;generated-fresh
PASS(5 处存量走 WARN);
check_ids --strict exit 0,**不可判定绝对数全程未被本闸吃掉**(作者开工 1,162、写完新闸与新文档仍 1,162;
现 1,156 是队友还清的);全部改动文件 noBOM / LF / U+FFFD=0。未跑 pre-commit --all-files(只跑本闸)。

**本笔使用 --no-verify 的凭证(唯一允许的两条之一,按 MEMORY 台账例外口径)**:把本批 6 只文件单独
staged 后跑 `pre-commit run --files <这 6 只>` → **13 条 Passed / 1 条 Failed**
唯一失败的是 `mem-budget`
(RAM 92% 阈值,**全树检查**)。本批**零固件源、零 .map 变更**,该红来自其他队友在飞的固件改动造成的 .map
漂移(当前树 Code=231,952 / RW=1,528 / ZI=136,264,在库台账 231,712 / 1,504 / 136,216
差 = run_par setter
与 vol_lock 计数两笔,均未提交);台账回填由我在树能编后统一 `--write`,不在分支上跑(避免多分支同改受管表)。
gate: only mem-budget pending lead ledger backfill。字节卫生本笔自查:6 只文件均 noBOM / L
F / U+FFFD=0,
新脚本纯 ASCII 输出路径无关,AGENTS 与索引两处计数与配置 YAML 解析口径 51 一致。

另收一条今天反复出现的元教训(Lead 与作者各错过一次、方向相反):**`pre-commit run` 与 git 钩子不是同一根
探针**——config 处于 modified-unstaged 时只有后者 [ERROR] 拒绝,前者与 validate-config 都 Passed
。它与「stash
只覆盖已跟踪未暂存」「未跟踪文件永不进 stash」同族:手跑的探针不能替真提交的探针作证。
This commit is contained in:
2026-10-27 03:43:01 +08:00
parent 9a231831bc
commit 1d39571114
6 changed files with 552 additions and 12 deletions
+13 -10
View File
@@ -1,6 +1,6 @@
# 门禁逐项说明(每种门禁一篇)
> 这一目录回答一个问题:**提交时那 50 个钩子到底在干什么、拦谁、放谁、什么时候会骗人。**
> 这一目录回答一个问题:**提交时那 51 个钩子到底在干什么、拦谁、放谁、什么时候会骗人。**
> 每个门禁一篇,八段固定结构:查什么 / 为什么要它 / 触发面 / 报错怎么读 / 好处 /
> **限制与盲区** / 豁免与基线 / 改门禁本身要注意什么。**最后一节是这套文档存在的理由**——
> 一份只看"拦了什么"的门禁文档,会让人以为绿就是对的。
@@ -8,13 +8,15 @@
- 配置事实源:`.pre-commit-config.yaml`。**钩子数的唯一口径 = YAML 解析后的 hook 条目数**,不是文件里的文本行数。
自查(打印声明数;加/删一条钩子后,这个数要与 **AGENTS.md 的「共 N 个钩子」**、**本目录 §2 标题**同时对齐):
`& '.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']))"`
→ **50**(2026-10-26:49 → 50,加的是 `generated-fresh`)。
⚠ **别用 `grep -c` 去数配置里的声明行**:该文件里有一条**注释掉的**候选钩子(clang-format,第 63 行),
文本 grep 会多算 1 = 51——「声明 51 / 文档 50 / 实跑 50」这种三个数不同源的争执就是这么来的。真要 grep,
用只认行首缩进的模式:`grep -Ec "^ +- id: " .pre-commit-config.yaml`(= 50)。
→ **51**(2026-10-26:49 → 50 加 `generated-fresh`;2026-10-27:50 → 51 加 `nested-comment`)。
⚠ **别用 `grep -c` 去数配置里的声明行**:该文件里有一条**注释掉的**候选钩子(clang-format,第 65 行),
文本 grep 会多算 1 = 52——「声明 52 / 文档 51 / 实跑 51」这种三个数不同源的争执就是这么来的。真要 grep,
用只认行首缩进的模式:`grep -Ec "^ +- id: " .pre-commit-config.yaml`(= 51)。
⚠ 反过来的坑也真踩过(2026-10-27):在配置**注释里复述那个 grep 字面量**,会让文本 grep 再虚高 1——
数数只认 YAML 解析,注释与文档里都别写出那个字面量(`docs/门禁说明/nested-comment.md` §8 记了现场)。
实跑侧口径:`scripts/ci_local.py` 的 `[SUMMARY] L0: pre-commit hooks -- passed=A failed=B skipped=C`,
`A+B+C` = **commit 阶段**的钩子数(49),再加 `gitlint` 1 条(它的 commit-msg 阶段声明写在**上游**仓库自带的
钩子清单里,本配置文件里查不到,见 `scripts/ci_local.py` 文件头)→ 49 + 1 = 50 = 声明数,三数同源。
`A+B+C` = **commit 阶段**的钩子数(50),再加 `gitlint` 1 条(它的 commit-msg 阶段声明写在**上游**仓库自带的
钩子清单里,本配置文件里查不到,见 `scripts/ci_local.py` 文件头)→ 50 + 1 = 51 = 声明数,三数同源。
- 通用机制(两阶段、stash 陷阱、`always` 已废、增量拦截三段式、旧 §2.x 查号表):
`docs/代码规范/pre-commit使用说明.md` §2。
- 快速自查(不等提交):`& '.venv\Scripts\python.exe' scripts/ci_local.py`(L0 + L0b + L0.5)。
@@ -25,12 +27,12 @@
|---|---|---|---|
| **改写型** | 不报错,直接改你的文件 | `trailing-whitespace`、`end-of-file-fixer` | 重新 `git add` 被改的文件再提交(它改的是工作区,不是索引) |
| **硬拦截** | 违反即失败,没有存量豁免 | `cppcheck`、`coding-standard`、`doc-links`、`no-mojibake`、`mem-budget`、`ui-golden`、`font-charset`、`lv-conf-sync`、`task-table`、`mem-numbers`、`registration-check`、`cubemx-survival`、`test-suite-switch`、16 个主机单测、官方 5 个、`gitlint` | 只能改代码或改文档;**不要**找豁免开关 |
| **增量拦截** | 规则恒生效,但存量钉在基线/台账里,只许减不许增 | `layer-deps`(3 条,2026-10-25 还清 UI 记录 3 条)、`bsp-err`(38 条)、`id-check`(`scripts/id_known_backlog.json` 台账)、`generated-fresh`(5 处 `docs/images/` 配图存量,脚本内 `BASELINE`) | 报错先分清是**新增**还是**存量搬家**:`--verbose` 看归类,`--prune` 看已还清 |
| **增量拦截** | 规则恒生效,但存量钉在基线/台账里,只许减不许增 | `layer-deps`(3 条,2026-10-25 还清 UI 记录 3 条)、`bsp-err`(38 条)、`id-check`(`scripts/id_known_backlog.json` 台账)、`generated-fresh`(5 处 `docs/images/` 配图存量,脚本内 `BASELINE`)、`nested-comment`(2 处主机侧假件头,`test/host/common/`,脚本内 `BASELINE`) | 报错先分清是**新增**还是**存量搬家**:`--verbose` 看归类,`--prune` 看已还清 |
⚠ 增量拦截最容易误会的一点:**绿灯不等于没问题**,只等于"没有比上次更差"。
每个增量拦截门禁的基线数字都写进它自己的文档,读那篇时第一眼看的就是"现在在册多少条"。
## 2. 索引(50 个 hook → 26 篇 + 1 根只有本表指针(专篇待补)+ 1 根详解在 docs/测试说明/)
## 2. 索引(51 个 hook → 27 篇 + 1 根只有本表指针(专篇待补)+ 1 根详解在 docs/测试说明/)
| 门禁文档 | 覆盖的 hook id | 一句话它在查什么 | 改了哪类文件才会跑 | 文档 |
|---|---|---|---|---|
@@ -45,6 +47,7 @@
| [`mem-budget.md`](mem-budget.md) | `mem-budget` | RAM 总量红线:`RW_IRAM1` / `RW_CCM` 超 92% 即拦 | 代码与配置改动 | 已写 |
| [`task-table.md`](task-table.md) | `task-table` | README/AGENTS 里的任务表是否与 `Src/freertos.c` 一致 | `README.md`、`AGENTS.md`、`Src/freertos.c` | 已写 |
| [`registration-check.md`](registration-check.md) | `registration-check` | 新 `.c` 是否双端注册(Keil 工程 + 模拟器 CMake);UI 页面描述符表是否接线 | `.c`、`.uvprojx`、`CMakeLists.txt` | 已写 |
| [`nested-comment.md`](nested-comment.md) | `nested-comment` | 块注释体内再出现 /*(ARMCC 9-D 嵌套注释;诱因是注释里写路径通配) | 全仓 `.c/.h`(第三方与生成物除外) | 已写 |
| [`generated-fresh.md`](generated-fresh.md) | `generated-fresh` | 生成器幂等性:把生成器重跑到**系统临时目录**,与工作区逐字节必须一致(不一致 = 生成物比生成器旧) | `tools/**`、UI 源码、`ui_menu_tree.txt`、`ui_cn_chars.txt`、`docs/images/*.svg`、两份注册表 | 已写 |
| [`lv-conf-sync.md`](lv-conf-sync.md) | `lv-conf-sync` | 两份 `lv_conf.h`(MCU / 模拟器)的特性子集是否一致 | 两份 lv_conf 或脚本 | 已写 |
| [`cubemx-survival.md`](cubemx-survival.md) | `cubemx-survival` | CubeMX 重生成会不会把定制静默抹掉(L3 审计兜底) | `.ioc`、`Src/`、`Inc/` | 已写 |
@@ -76,7 +79,7 @@
## 4. 现状与下一步
- **本目录 26 篇已全部成文**(2026-10-26 实测口径:`docs/门禁说明/` 下除 README 外 26 篇,与上表「文档」列
- **本目录 27 篇已全部成文**(2026-10-27 实测口径:`docs/门禁说明/` 下除 README 外 27 篇,与上表「文档」列
**一一对应**——「有页无行」与「有行无页」两个差集都是空集;一行覆盖多个 hook 的行(`cppcheck`+`test-cppcheck`、
22 个 `*-unit-test`)按行内列举计。上一句写「23 篇」是存量误差:2026-10-25 连加 `bom-guard` /
`setview-check` / `uinav-selftest` 三根、本轮加 `generated-fresh` 都没同步它(本轮前实有 25 篇)。
+10
View File
@@ -103,6 +103,15 @@
- 要不要给某套补第 23 根,判据现成:pre-commit 4.x 没有 `always: true`,**该模块有没有自己的
`files:` 触发面**决定补了会不会真跑;只为凑数加一根,等于把「快归因面」和「全量把关」两件事混成
一件,还要让钩子计数再改一次;
- **裁定(2026-10-27,Lead):这 18 套(含 `test/host/ui_sink`)不补专属钩子,差异按设计说明记录。**
理由不是省事:22 根专属钩子存在的唯一意义是**秒级归因**;而 `ui_sink` 改动面小,它的失败面全部
落在 `test-coverage` 总闸的必跑路径里,坏了照样拦提交。再加一根只是多一个必须「声明数 / 文档数 /
实跑数」三处同源同步的计数点——本仓 2026-10-26 到 27 一天之内就为 40 / 50 / 51 这三个数不同源
返工过两轮。**要读「这套跑没跑」,读总闸输出,不要来数钩子。** 两个方向的空头账一起堵住:
① 别把没接线的套件算进钩子账(原写「23 个 / 含 ui-sink」正是这么错的;2026-10-27 复核的准数是
22 根 / 40 套 / 18 套无专属钩子);② 也别把总闸的绿读成「每套都被单独归因过」——它只保证
「全跑过 + 覆盖率没回退」。将来确实要给某套补专属钩子,先过上一条判据,并在**同一笔**里改
AGENTS 与门禁索引的计数(口径 = YAML 解析条目数,见 `docs/门禁说明/README.md` §2 首条)。
- 总闸的触发面是 `App/`、`Drivers/BSP/`、`test/host/`、覆盖率脚本与基线,**外加门禁自身**
(`.pre-commit-config.yaml`)——不带上自身就等于谁改坏这根闸的 `files:` 模式它就再也不跑
(同 `lv-conf-sync` / `bsp-err` 的既有做法)。纯 UI/文档改动不触发它,那时确实没有可回归的行为。
@@ -128,5 +137,6 @@
| 日期 | 变更 |
|---|---|
| 2026-10-27 | **裁定入档**(task-28):18 套无专属钩子(含 `ui_sink`)**是设计不是缺口**,不补第 23 根;补钩子的判据、两个方向的空头账、「读总闸输出别数钩子」写进 §6 第 4 条。同日复核配置实数 = 解析 51 条 / 专属单测 22 根 / 套件 40 套 / 无专属钩子 18 套,与 AGENTS 那句一致 |
| 2026-10-26 | **数字与机器面对齐**(task-18 复核带出):实例 16 → **22**(2026-10-25 由 3528644e / 48bc50e7 / ffd462ef / 8c726986 / 201d7132 / 484d0682 各挂一根,§1 表补齐 6 行);§6 第 4 条「12 套无专属钩子 / 总闸 28 套」按实测更正为 **18 套 / 40 套**,并给出「没有专属钩子 ≠ 没被跑」的证据(总闸 rglob 发现规则 + 退出码语义)与补钩子的判据;顺带纠正 AGENTS 的「23 个 / 含 ui-sink」——`ui-sink-unit-test` 在配置历史里从未存在 |
| 2026-10-23 | 首篇逐项文档(16 个实例清单 + 今天两个 runner 需补 include 目录的实测) |
+119
View File
@@ -0,0 +1,119 @@
# 块注释嵌套(nested-comment)— 注释体内不得再出现 /*
> 一句话:C 的块注释**不嵌套**。注释体内再出现一个 /* 时,ARMCC 报诊断 9-D
> 「nested comment is not allowed」。本仓的触发形态极其日常——**在注释里写作用域或通配路径**。
> 脚本 `scripts/check_nested_comment.py`(纯标准库、只读,2026-10-27 接线)。
## 1. 它在查什么
逐字节状态机走 代码 / 行注释 / 块注释 / 字符串 / 字符字面量 五个状态,
**只有在块注释体内**看到的 /* 才算问题,报「文件:行:列 + 上下文指纹 + 修法」。
| 判定 | 形态(本仓真实写法或自测用例) |
|---|---|
| **算** | 注释里写路径通配:`Drivers/BSP/UI/**`、`test/host/**`、`App/algo/*`(那串里的 /* 就是第二次注释的起点) |
| **算** | 块注释内部某行行首又开一个注释(复制粘贴手滑);块注释里用引号包住 /*;块注释里行注释符之后再来 /* |
| **不算** | 字符串字面量里的 /*(`const char *s = "/* 不是注释 */";`) |
| **不算** | 行注释里写 /*(编译器不报,本闸也不报) |
| **不算** | 本仓既有的尖括号写法 `<Drivers/BSP/UI/ui_w_soe.c>`(**推荐改法**) |
## 2. 为什么要它
- **只在重编译那个 TU 时才暴露**:改了某个头里的注释,只有编到它的包含者才看得见;
历史「0 错误 0 警告」不代表现在这份是干净的(与 `bom-guard` 完全同形的坑)。
- **按包含者数量刷屏**:一处嵌套写在被广泛 include 的头里,每个包含者各打一行。
本仓实测一例扩散到 **16 条警告**,把同批 10 条**真**警告挤到看不见——
等于把「能不能看清报错」这件事本身弄坏。
- 本闸**不需要构建**:提交时就报,不必等 @build,也不必去猜是谁的编译单元。
## 2.1 接线当日实测(都可复现)
| 测项 | 结果 |
|---|---|
| 全仓 `--all` 实扫 | **403 个 .c/.h:新发 0、存量 2 处**(都进基线,见 §7),退出码 0 |
| 形态电池(10 项,逐条对齐期望命中数) | **10/10**:5 个真形态全照出,5 个不该报的全安静 |
| `--selftest` 四项 | **4/4**:正例照得出 / 反例不误报 / 基线吞得下 / 只减不增有牙 |
| **真字节回放(空基线)** | 把 §7 那两份假件头当「今天新提交」跑 → 两条 [NEW]、退出码 **1** |
| 同一批真字节 + 已登记基线 | 两条 [BASE]、退出码 **0**(基线只决定「算不算新的」,不决定「有没有」) |
| 形态对照(重建当日那一处) | 尖括号写法 → 0 命中;同一段退回星号形态 → **1 命中,列 29** |
| 已提交历史回放 | 近 150 个提交里被改过的 .c/.h 共 **281 份旧字节:0 命中** ⇒ 这类问题此前从没被机器拦过,全靠人工在编译后清 |
> 编译器侧原文(**冻结引用**)——2026-10-27 02:22 那次 @build 的构建日志:
>
> ```
> ..\App\algo\run_par.c(267): warning: #9-D: nested comment is not allowed
> ```
>
> ⚠ 该日志每次 @build 都被覆盖、且不入库(02:37 已被下一次构建清掉,我写完这段再去查就没了),
> 所以把原文抄在这里存档;当日那三处同类**没有一处在已提交历史里留下字节**(上表最后一行),
> 本闸真正第一次拦住,会是「下一次有人在注释里写星号形态」。
## 3. 触发面
钩子用 `types: [c]`(即 .c/.h),第三方与生成物由配置顶层 exclude 挡掉。
脚本的 `--all` 与钩子喂进来的文件集**刻意同口径**(2026-10-27 实测 403 个文件)——
手跑面与提交面不一致,就会变成「两个数」,本仓已经为这种事返工过好几轮。
单独自查:`& .venv\Scripts\python.exe scripts/check_nested_comment.py --all`
## 4. 报错怎么读
```
[NEW ] App/x/y.c:45:17 块注释体内出现 /*(出现 1 次同形)
上下文:件才能测。 * * ⚠ 仅供 test/host/** 使用,不参与 MCU 构建。 * * 为
修法:注释里别写 /**(C 块注释不嵌套)——路径写全目录就好(Drivers/BSP/UI/),
或按本仓既有做法用尖括号包住(<Drivers/BSP/UI/ui_x.c>)
```
- `[NEW ]` = 基线之外的新命中 → **拦提交**(退出码 1)。
- `[BASE]`(要加 `--verbose` 才逐条打印)= 存量在册,不拦(见 §7)。
- `[TIGHTEN]` = 某条基线本轮实测已还清,该删了。
- 退出码:0 通过或只剩存量;1 有新命中;2 用法或环境错误。
## 5. 好处
- 秒级、**零第三方依赖**(只用标准库)。这点是刻意的:`generated-fresh` 那天栽在
「提交环境里裸 python 不是 .venv、装不到 fontTools」,本闸不碰第三方库,任何解释器
都跑得动——不会再造一根依赖某台机器环境的闸。
- 把「注释里写路径」这种看起来人畜无害的写法变成机器可判,而不是靠 reviewer 眼力。
## 6. 限制与盲区
1. **只管「块注释体内出现 /*」这一件事**。不判注释是否闭合(未闭合是编译错误,编译器自己
会拦,不需要一根只读闸替它操心);不判行注释里的 /*(编译器也不报)。
2. 不做预处理:若把 /* 藏进宏展开里(本仓没有这种写法),本闸照不到。
3. 基线指纹按**内容**不按行号——存量那几行改了行号仍算在册;反过来,同一文件里换一个新
形态会算新发(这条有专门的自测项)。
4. 只覆盖 .c/.h。Python 生成器模板将来若写出 /**,本闸照得到**产物**(产物是 .c/.h),
但不会指出模板——那段链由 `generated-fresh` 保证「脚本与产物同一笔提交」。
5. 编译器侧的真实警告仍以 `@build` 为准:本闸只拦「会让 ARMCC 报 9-D 的那一类」,
编译日志里其他诊断一概不管。
## 7. 豁免与基线(只减不增)
接线当日全仓 `--all` 实扫 **403 个 .c/.h:新发 0、存量 2 处**,都在主机侧假件头里:
| 文件:行 | 形态 | 为什么今天不报 | 还法 |
|---|---|---|---|
| `test/host/common/ch395f_fake.h:7` | 注释里写作用域 `test/host/**` | 该假件不参与 MCU 构建(换 GCC/clang 的 -Wcomment 就会报) | 改成 test/host 目录 这类语义短语 |
| `test/host/common/fatfs_fake.h:9` | 同上形态 | 同上 | 同上 |
两处都属 `test/` 写域,接线时不由本道代改,按既有范式钉进脚本内 `BASELINE`,
**除这两处外零豁免**。一旦有人把这两份假件 include 进板端编译单元,就会按包含者数量刷出
9-D——这正是基线要显式挂着的原因:**不是「没问题」,是「已知待还」**。
还清一条就删那一行(`--prune` 打台账并点名已还清的)。
## 8. 改这套门禁本身
- 加判定形态必须同时加**正例与反例**:`--selftest` 四项(正例照得出 / 反例不误报 /
基线吞得下 / 只减不增有牙)之外,另有 10 项形态电池逐条对齐期望命中数。
- ⚠ **配置文件注释里不要复述声明行的字面量**:接线当天我在 `.pre-commit-config.yaml` 的
注释里写了一遍那个 grep 模式,文本 grep 的计数当场虚高 1(口径与自查命令见
`docs/门禁说明/README.md` §2 首条)。数数交给 YAML 解析,注释只描述。
- 加/删一条钩子要同批改三处:AGENTS.md 的钩子数、本目录 §2 标题、配置声明,外加索引行。
## 9. 变更记录
| 日期 | 变更 |
|---|---|
| 2026-10-27 | 首篇(task-29 接线当日)。当日实测:全仓 403 文件、新发 0、存量 2 处入基线;形态电池 10/10、自测 4/4、真字节空基线回放 2 处 [NEW]。编译器侧原文按 §2.1 **冻结引用**存档——构建日志会被下次构建覆盖,所以不写「最近一次日志里仍有」这种会过期的话 |