# 门禁逐项说明(每种门禁一篇) > 本目录逐项说明各门禁:**声明的 52 个钩子到底在干什么、拦谁、放谁、什么时候会骗人**——其中 3 根慢钩子带 > `stages: [manual]`,一次 `git commit` 只实跑 49 根(口径见下条)。 > 每个门禁一篇,八段固定结构:查什么 / 为什么要它 / 触发面 / 报错怎么读 / 好处 / > **限制与盲区** / 豁免与基线 / 改门禁本身要注意什么。**最后一节是这套文档存在的理由**—— > 一份只看"拦了什么"的门禁文档,会让人以为绿就是对的。 - 配置事实源:`.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']))"` → **51**(2026-10-26:49 → 50 加 `generated-fresh`;2026-10-27:50 → 51 加 `nested-comment`; 2026-10-28:51 → 52 加 `id-check-new`、随即 52 → 51 摘掉 `id-check`(退役全库语料审计);2026-10-29:51 → 52 加 `skill-size`(SKILL.md 写侧上限与 reference 指针,详见 [`skill-size.md`](skill-size.md)); 同日 52 → **51** 退役 `id-check-new`——只查新增行的编号门禁整体退场,脚本与白名单移入 `scripts/archive/id_gate/`,此后**编号形态没有机器面**(留档 [`id-check-new.md`](id-check-new.md)); 2026-10-30:51 → **52** 加 `mermaid-syntax`(mermaid 围栏内确定性语法:坏图渲染失败靠肉眼才逮过, 详见 [`mermaid-syntax.md`](mermaid-syntax.md)))。 ⚠ **别用 `grep -c` 去数配置里的声明行**:该文件里有一条**注释掉的**候选钩子(clang-format,第 65 行), 文本 grep 会多算 1(当前口径 52 会数成 53)——「声明比文档多 1 / 实跑又少一档」这种三个数不同源的争执就是这么来的。真要 grep, 用**锚定行首缩进**的模式(它天然跳过以 # 开头的注释行)2026-10-28 实测直接得 **51**(当时口径)、 2026-10-30 复测得 **52**,均与 YAML 口径一致; 会多算 1(比口径多 1)的是**不锚定行首、按子串**匹配声明行的写法(那条被注释掉的候选也被它算进去)。 ⚠ 反过来的坑也真踩过(2026-10-27):在配置**注释里复述那个 grep 字面量**,会让文本 grep 再虚高 1—— 数数只认 YAML 解析,注释与文档里都别写出那个字面量(`docs/门禁说明/nested-comment.md` §8 记了现场)。 实跑侧口径:`scripts/ci_local.py` 的静态门禁汇总行 `[SUMMARY] …: pre-commit hooks -- passed=A failed=B skipped=C`(方括号里那一段是**层级名**,脚本输出与文档层的语义名正分头迁移 → 抓这行**按 `passed=` 字段匹配**,别把层级名前缀写死进字面量,否则会 grep 不到而误判「没有实跑数」), `A+B+C` = **pre-commit 阶段**的钩子数(48 = 52 − 3 根 `stages: [manual]` − `gitlint`;2026-10-30 加 `mermaid-syntax` 前该式子右侧是 51/47),再加 `gitlint` 1 条(它声明在本配置的 `repo: jorisroovers/gitlint` 条目、`stages: [commit-msg]`(第 70 行),`--hook-stage commit` 的枚举不含它——2026-10-30 实测: `--hook-stage commit` 枚举 **47 行** / `manual` 枚举 **50 行** / YAML 解析声明 **51**(加本钩后 48/51/52), 47 + 3 + 1 = 51,差的那个就是 `gitlint`;旧说法「gitlint 声明在上游清单、本配置里查不到」已于当日随 `stages:` 接线作废) → 48 + 1 = **49 = 一次 `git commit` 实跑的钩子数**;再加 3 根 manual 阶段的慢钩子才是声明数 52,三数同源(2026-10-29 加 `skill-size` 之前这四个数是 51 / 47 / 48;2026-10-30 加 `mermaid-syntax` 后是 52 / 48 / 49)。 枚举自查(不执行任何钩子,`SKIP` 列全部 id):`SKIP=<全部 id> -m pre_commit run --all-files --hook-stage commit` 输出 47 行、`--hook-stage manual` 输出 50 行(47 常规 + 3 慢的)。 - **三根慢钩子在 manual 阶段**:`cppcheck`、`test-cppcheck`、`test-coverage` 带 `stages: [manual]` → `git commit` 阶段**不跑**(实测三根合计约 165 s,占一次带代码提交耗时的约三分之二),只在**远端 CI 的「手动阶段门禁」步** (`.gitea/workflows/ci.yml`,逐根 `--hook-stage manual`)与 `scripts/ci_local.py` 的 **manual 段**里跑。 本地手工复跑:`& '.venv\Scripts\python.exe' -m pre_commit run --all-files --hook-stage manual ` (**一次只能给一个 id**,多 id 实测报 `unrecognized arguments`)。声明数仍是 52,变的是「提交期实跑几根」。 - 通用机制(三个阶段 = `pre-commit` / `commit-msg` / `manual`、stash 陷阱、`always` 已废、增量拦截三段式、旧 §2.x 查号表): `docs/代码规范/pre-commit使用说明.md` §2。 - 快速自查(不等提交):`& '.venv\Scripts\python.exe' scripts/ci_local.py`(静态段 48 根 + manual 段三根 + 提交信息门禁 + 体积门禁 + 主机侧纯逻辑单测;manual 段默认跑)。 ## 1. 三种拦截姿态(先分清这个,报错时才知道该干什么) | 姿态 | 含义 | 有哪些 | 失败时该做什么 | |---|---|---|---| | **改写型** | 不报错,直接改你的文件 | `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`、`mermaid-syntax`(基线为空,先例是已退役的 `id-check-new`「零语料」;唯一排除面 = 外部资料目录,见其专篇 §7)、22 个主机单测、官方 5 个、`gitlint` | 只能改代码或改文档;**不要**找豁免开关 | | **增量拦截** | 规则恒生效,但存量钉在基线/台账里,只许减不许增 | `layer-deps`(3 条,2026-10-25 还清 UI 记录 3 条)、`bsp-err`(38 条)、`generated-fresh`(5 处 `docs/images/` 配图存量,脚本内 `BASELINE`)、`nested-comment`(2 处主机侧假件头,`test/host/common/`,脚本内 `BASELINE`) | 报错先分清是**新增**还是**存量搬家**:`--verbose` 看归类,`--prune` 看已还清 | | **只查新增** | 只看本次改动的增量,没有存量基线,也没有台账 | ~~`id-check-new`~~(**2026-10-29 退役**,脚本在 `scripts/archive/id_gate/`;这一姿态已无现役门禁) | 手工复查时才谈得上:改成语义短语(门禁不再跑,行内 `[编号豁免]` 也随之失效) | ⚠ 增量拦截最容易误会的一点:**绿灯不等于没问题**,只等于"没有比上次更差"。 每个增量拦截门禁的基线数字都写进它自己的文档,读那篇时第一眼看的就是"现在在册多少条"。 ## 2. 索引(52 个 hook → 30 篇,其中 2 篇是已退役门禁的留档 + 1 根只有本表指针(专篇待补)+ 1 根详解在 docs/测试说明/) > 带 `stages: [manual]` 的三根(`cppcheck` / `test-cppcheck` / `test-coverage`)**提交期不跑**,只在远端 CI 的 > 「手动阶段门禁」步与 `scripts/ci_local.py` 的 manual 段跑;下表对应两行的「文档」列已标注。 | 门禁文档 | 覆盖的 hook id | 一句话它在查什么 | 改了哪类文件才会跑 | 文档 | |---|---|---|---|---| | [`bsp-err.md`](bsp-err.md) | `bsp-err` | BSP 的对外 API 面:错误通道类型、超时宏命名、必须写返回值、返回值不得裸丢弃 | `Drivers/BSP/**` 或门禁自身 | 已写 | | [`layer-deps.md`](layer-deps.md) | `layer-deps` | 分层方向:`Drivers/BSP/**` 不得 include `App/**` | `Drivers/BSP/**` | 已写 | | [`id-check.md`](id-check.md) | ~~`id-check`~ | **已退役(2026-10-28)**:全库语料的八项校验,留档备查;接班件也在下一行 | 在时有:几乎所有文本文件 | 已写(退役留档) | | [`id-check-new.md`](id-check-new.md) | ~~`id-check-new`~ | **已退役(2026-10-29,用户裁定)**:只查新增行的编号门禁,留档备查(含退役理由与手工跑法);**编号形态此后无机器面** | 在时有:任意 md/c/h/py/yaml/txt | 已写(退役留档) | | [`doc-links.md`](doc-links.md) | `doc-links` | 文档里的路径引用是否存在;`--sections-strict` 另查章节号是否还存在 | 任意 `.md` | 已写 | | [`mermaid-syntax.md`](mermaid-syntax.md) | `mermaid-syntax` | ` ```mermaid ` 围栏内确定性语法:图类型头 / `-.` 直接跟管道 / subgraph 与 sequence 块的 `end` 配对 | 任意 `.md`(`docs/芯片手册/` 等与 doc-links 同排除面) | 已写 | | [`cppcheck.md`](cppcheck.md) | `cppcheck`、`test-cppcheck` | 静态分析(真机侧与 `test/` 侧两套范围与抑制项) | C/H 源文件 | 已写(`stages: [manual]`) | | [`coding-standard.md`](coding-standard.md) | `coding-standard` | 编码规范 V1.0 的可机械检查子集(命名、头文件保护宏、模块头注释块…) | C/H 源文件 | 已写 | | [`no-mojibake.md`](no-mojibake.md) | `no-mojibake` | 禁 `U+FFFD`——中文被按错误编码重解码后不可逆损坏的痕迹 | 全部文本 | 已写 | | [`mem-numbers.md`](mem-numbers.md) | `mem-numbers` | 文档里引用的 RAM/Flash 数字与最新 `.map` 报告是否一致(体积档) | `docs/内存管理/**`、相关脚本 | 已写 | | [`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`(第三方与生成物除外) | 已写 | | [`skill-size.md`](`skill-size.md`) | `skill-size` | SKILL.md 不超插件写侧上限 65536 字节(**只在写侧校验、加载侧不查**:超限的技能照常能读、每次「生成草案」却被拒、永久卡在「生成中」)+ `reference/` 指针不悬空 + 不带 BOM | `.dsh/skills/**`(filenames 只当触发,判定始终全树扫) | 已写 | | [`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 重生成会不会把定制静默抹掉(人工目检审计兜底) | `.ioc`、`Src/`、`Inc/` | 已写 | | [`test-suite-switch.md`](test-suite-switch.md) | `test-suite-switch` | 板级套件开关必须互斥;全注释 = 产品构建 | `test/board/test_config.h` | 已写 | | [`font-charset.md`](font-charset.md) | `font-charset` | UI 用到的汉字是否都在子集字体里(缺字 = 白框) | `Drivers/BSP/UI/**`、字体脚本 | 已写 | | [`ui-golden.md`](ui-golden.md) | `ui-golden` | UI 像素回归:菜单 8 状态 + 内容页 74 张黄金参考图比对 | UI 源码、模拟器 | 已写 | | [`bom-guard.md`](bom-guard.md) | `bom-guard` | 带 BOM 的文件被编辑工具剥掉 BOM(ARMCC 换解码 → 假语法错) | `Drivers/BSP/UI/**` 等名单内文件 | 已写 | | [`setview-check.md`](setview-check.md) | `setview-check` | 定值查看行表与 `prot_set` 枚举序 / `g_prot_set_meta` 上下限是否同一份口径 | `tools/setview_check/**`、`ui_w_setview.*` | 已写 | | (专篇待补,档位与限度正文在 [`../UI设计/UI_自动化测试流程.md`](../UI设计/UI_自动化测试流程.md) §7.3) | `uinav-selftest` | 板端 UI 导航器认屏快检(黄金参考帧分类;完整闭环档=合入前/上板前手工跑,快检不互替) | `scripts/board_ui_nav.py`、`simulator/tests/ref/*.bin` | 指针 | | [`host-unit-tests.md`](host-unit-tests.md) | 22 个 `*-unit-test` | 主机侧单测(同一机制 22 个实例):逻辑层在无硬件的 PC 上编译并跑断言 | 各自被测模块与其测试目录 | 已写 | | 详解在 [`../测试说明/单元测试/主机单测覆盖率口径.md`](../测试说明/单元测试/主机单测覆盖率口径.md)(口径 / 分母 / 基线 / 盲区都在那篇,本目录不重复) | `test-coverage` | **覆盖率总闸**:跑全部 47 套主机单测 + 已覆盖行只增不减的回退台账 | `App/`、`Drivers/BSP/`、`test/host/`、脚本与基线 | 已写(在 `docs/测试说明/`;`stages: [manual]`) | | [`trailing-whitespace.md`](trailing-whitespace.md) | `trailing-whitespace` | 行尾空白(改写型;CubeMX/Keil 生成文件豁免) | 全部 | 已写 | | [`end-of-file-fixer.md`](end-of-file-fixer.md) | `end-of-file-fixer` | 文件末尾必须恰好一个换行(改写型;同豁免) | 全部 | 已写 | | [`check-added-large-files.md`](check-added-large-files.md) | `check-added-large-files` | 别把大文件(日志/镜像/二进制)误提进仓库 | 全部 | 已写 | | [`check-merge-conflict.md`](check-merge-conflict.md) | `check-merge-conflict` | 别把没解完的冲突标记(`<<<<<<<`)提上去 | 全部 | 已写 | | [`check-yaml.md`](check-yaml.md) | `check-yaml` | YAML 语法是否可解析(门禁配置自己坏了会静默不跑) | `.yml`/`.yaml` | 已写 | | [`gitlint.md`](gitlint.md) | `gitlint` | 提交信息规则:标题长度、**正文每行 ≤80**、空行要求 | 提交信息(commit-msg 阶段) | 已写 | ## 3. 每篇文档的固定八段 1. **它在查什么**——逐条判据,能对着代码复核的粒度; 2. **为什么要它**——它拦下过什么真事,没有它会怎样(没有真事的门禁要老实写"预防性"); 3. **触发面**——`files:` / `exclude:` / `pass_filenames` / 阶段,以及**看不见哪些文件**; 4. **报错怎么读**——样例输出、怎么单独跑、常见"假报警"(含 stash 陷阱); 5. **好处**——它把哪件事从"口头约定"变成了机器判定; 6. **限制与盲区**——什么写法能骗过它、什么错它照不到、判据的启发式在哪里; 7. **豁免与基线**——白名单形状、在册数量、还债口径(只减不增); 8. **改门禁本身**——自测怎么做、改名会波及什么(例如基线键)、为什么不许拿基线洗假阳性。 ## 4. 现状与下一步 - **本目录 30 篇已全部成文**(2026-10-30 加 `mermaid-syntax` 一篇 → 30;2026-10-29 加 `skill-size` 一篇 → 29;2026-10-27 实测当时 27 篇;2026-10-28 当时复核:`docs/门禁说明/` 下除 README 外 28 篇,与上表「文档」列 **一一对应**——「有页无行」与「有行无页」两个差集都是空集;一行覆盖多个 hook 的行(`cppcheck`+`test-cppcheck`、 22 个 `*-unit-test`)按行内列举计。上一句写「23 篇」是存量误差:2026-10-25 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三根、本轮加 `generated-fresh` 都没同步它(本轮前实有 25 篇)。 **加钩子时的三步见下一行**,第 ③ 步的计数与本目录 §2 标题、AGENTS.md 的钩子数必须同一笔改。 - 新增/改动一个门禁时的三步:① 在 `.pre-commit-config.yaml` 落地;② 在本目录**照八段结构 新增或更新一篇**("限制与盲区"一节必须写真实判据的限度,不许留空);③ 在本 README 索引加一行。 - **收敛已完成(2026-10-23)**:`docs/代码规范/pre-commit使用说明.md` 的原 §2「各钩子规则详解」 (27 个小节、约 480 行)已删除,本节改为 `docs/代码规范/pre-commit使用说明.md` §2 —— 只留**跨钩子的 通用机制**(两阶段、stash 陷阱、`always` 已废、改写型与只读型豁免不对称、增量拦截三段式、 没有 `.map` 即跳过、配置自己坏的验收方式、新增门禁三步)与**旧 §2.N → 新文档 查号表**。 同一事实只留一处(`docs/文档规范.md` 的要求)。 - 收敛时按**内容**重指了引用:skill 与 AGENTS 文档表、配置文件头注释、手册自己的文件清单行与 §5.5 里那句"判定见旧小节 2.8"。历史变更记录里的旧编号按"冻结历史不回改"保留,靠查号表读。