docs(门禁说明): 手册 §2 收敛为通用机制与查号表,逐项详解归一到 23 篇

用户一句"现在门禁都在干什么,我一概不知"引出的收尾。原手册 §2 有 27 个逐钩子小节,只回答
"拦了什么、怎么配",回答不了"什么时候会骗我";docs/门禁说明/ 已把逐项详解(含"限制与盲区"
一节)建好,本笔做收敛:同一事实只留一处(docs/文档规范.md 的要求)。

**先搬后删**(否则收敛等于丢事实):旧 §2 里独有条目逐篇折回,共 18 处插入、15 篇受益。折回的
东西包括:六套主机单测的来历与用例规模(13/36/82/46/32/149 断言,含 nand-port 的"与旧算法
对照")与"改动公共骨架 unit_test_harness.py 会同时触发 16 个钩子";UI 页面**两层注册**(构建
注册 vs 框架注册:枚举 + s_defs[] 描述符表)与"豁免不整目录一刀切";lv-conf-sync 首次落地清掉
26 个 LVGL 9.3 不存在的选项、3 处重复定义、10 项隐式开关显式化,以及 ALLOW_DIFF 端专属白名单
与必须穿过 lv_conf_internal.h 里那层 KCONFIG 包裹;cubemx-survival 的 16 项清单、断言前先剥
C 注释(本仓注释刻意引用历史代码,不剥就恒定误报)、旧手册"自造标记区"建议与看门狗文档实测
教训之间的优先级;id-check 顺带收编 check_test_traceability.py、裸形同字母多义属**显式承认的
盲区**、--update-baseline 只许涨;layer-deps 的注入自证与报错时三条修法(注入回调 / 上层递数据
/ 才谈 ALLOW);coding-standard **nand_ftl.* 也在排除清单**(我先前那篇漏了);test-suite-switch
的剥注释判定与派生宏白名单;mem-budget 两类软提醒不拦、退出码 2 是解析失败不是通过;doc-links
的排除目录与状态词 ±40 字符距离;task-table 改栈三步;gitlint 报错样例;no-mojibake 的修复路径。

**顺带抓到旧文档两处错**(记进文档,不悄悄改):一是 check-added-large-files 的阈值旧手册写
"单个文件 > 1MB",按本机安装的钩子源码实测是 --maxkb 默认 **500**(500 KB)——已更正,并把
"本目录任何默认值都以实测或安装的钩子源码为准"写进 end-of-file-fixer 当反面教材;二是
**ui-golden 在旧手册里连小节都没有**,只活在配置注释里:除了"有基线不等于有门禁",这是另一种
失踪——有钩子、没文档。

手册 §2 改写为三节:迁移说明 / **旧编号 → 新文档 查号表**(历史引用靠它读,旧号按"冻结历史不
回改"保留)/ **跨钩子通用机制**:两阶段与换机必须重装 hooks 才带 commit-msg、stash 陷阱、
always: true 已废、改写型与只读型豁免不对称、增量拦截三段式且刻意没有 --write、没有 .map 是
"跳过"不是"通过"、改配置的验收是看目标钩子出现在输出里且不是 Skipped、新增门禁三步必须做
注入自证且"限制与盲区"不许留空,另有快速自查入口。手册 707 行 → 273 行。

引用**按内容**重指(只核编号存在与否不够,同号小节会被当本文自引用静默放过):编号规范、
scripts/README(顺带更正那里过时的 warn-only 说法,早已关闸为增量拦截)、RAM 使用分析与优化
方案、测试开发标准流程、lvgl-simulator skill、AGENTS 文档表(拆成"每门禁一篇详解"与"通用
机制"两行)、.pre-commit-config.yaml 头注释、docs/README.md 描述、手册自身 §5.5 与 §6 的两处
内部引用。另:表格里的裸节号会被 doc-links 判成本文自引用,查号表统一写成不带 § 的形态。

写说明过程又被自家门禁拦一次:gitlint 篇里引用报错样例时写了两个规则号字面量,被判形态不合规
→ 改成中文描述,没有往台账里塞 reason 解决。

验证:纯文档与注释改动,不动代码,Program Size 不变,故不跑 @build/@test;
check_doc_links --all --sections-strict **171 篇 0 问题**;id-check 增量拦截通过;
pre-commit run **RC=0 / 13 项 Passed**;28 个改动文件字节卫生 **0 异常**(无 BOM、无 CR、
无 U+FFFD、均 LF、无行尾空白)。
This commit is contained in:
2026-10-23 11:44:21 +08:00
parent 871ace422b
commit 6822726b2c
28 changed files with 202 additions and 483 deletions
+1 -1
View File
@@ -165,7 +165,7 @@ docs/
| 文档 | 内容 |
|---|---|
| [`嵌入式C语言代码规范_V1.0.md`](代码规范/嵌入式C语言代码规范_V1.0.md) | 命名/格式/头文件/注释规范(含机械检查项) |
| [`pre-commit使用说明.md`](代码规范/pre-commit使用说明.md) | 提交门禁配置、检查范围与排除项 |
| [`pre-commit使用说明.md`](代码规范/pre-commit使用说明.md) | 提交门禁的部署、通用机制与范围口径(逐项详解见下行) |
| [`cppcheck使用说明.md`](代码规范/cppcheck使用说明.md) | cppcheck 配置与告警处置 |
| [`../门禁说明/README.md`](门禁说明/README.md) | **每个提交门禁一篇文档**:查什么、拦下过什么真事、**限制与盲区**、豁免与基线口径 |
+38 -471
View File
@@ -59,483 +59,50 @@ git commit
---
## 2. 各钩子规则详解
## 2. 通用机制与索引(逐项详解已迁往 `docs/门禁说明/`)
> ⚠ **逐项详解已迁往 `docs/门禁说明/`(每种门禁一篇,含"限制与盲区"一节)**。本节保留作为
> 历史详解与速查用,新增门禁请直接在 `docs/门禁说明/` 落篇;两处的收敛(同一事实只留一处)
> 排在 `docs/门禁说明/README.md` §4 的下一步里。
> **2026-10-23 迁移**:原本节 27 个"逐钩子详解"小节,已全部迁往 `docs/门禁说明/`——**每种门禁
> 一篇**,八段固定结构(查什么 / 为什么要它 / 触发面 / 报错怎么读 / 好处 / **限制与盲区** /
> 豁免与基线口径 / 改门禁本身)。39 个 hook 的总览与"三种拦截姿态" → `docs/门禁说明/README.md`。
> 搬走的原因:只写"拦了什么"的文档会让人以为绿就是对的,而"什么时候会骗人"必须有归处。
> 同一事实只在一处详述(`docs/文档规范.md` 同此要求),所以本节不再重复逐钩子表格。
### 2.1 旧小节 → 新文档 对照(历史引用查号用)
### 2.1 trailing-whitespace — 行尾空格清理
| 项 | 说明 |
| 旧小节编号(原手册 §2.N,已停用) | 现在住在 |
|---|---|
| **触发条件** | 文件任意行末尾含空格或 Tab |
| **处理方式** | **自动修复**(直接改写文件,删除行尾空白) |
| **影响范围** | 项目自有代码;**豁免 CubeMX / Keil 生成文件**(见下方"豁免清单") |
| **典型报错** | `Trim Trailing Whitespace` → `Fixing path/to/file.c` |
| **修复方式** | 无需手动修复,钩子已自动改写;重新 `git add` 即可 |
| 2.1 / 2.2 | `docs/门禁说明/trailing-whitespace.md`、`docs/门禁说明/end-of-file-fixer.md` |
| 2.3 / 2.4 / 2.5 | `docs/门禁说明/check-added-large-files.md`、`check-merge-conflict.md`、`check-yaml.md` |
| 2.6 | `docs/门禁说明/cppcheck.md`(含 `test-cppcheck`) |
| 2.7 / 2.8 | `docs/门禁说明/no-mojibake.md`、`coding-standard.md` |
| 2.9 / 2.10 / 2.11 | `docs/门禁说明/doc-links.md`、`task-table.md`、`mem-numbers.md` |
| 2.12 / 2.13 / 2.14 | `docs/门禁说明/cubemx-survival.md`、`test-suite-switch.md`、`gitlint.md` |
| 2.15 | `docs/门禁说明/lv-conf-sync.md` |
| 2.16 / 2.17 / 2.20 / 2.21 / 2.22 / 2.23 | `docs/门禁说明/host-unit-tests.md`(16 个 `*-unit-test` 合一篇,含各套件来历与用例规模) |
| 2.18 / 2.19 | `docs/门禁说明/registration-check.md`、`mem-budget.md` |
| 2.24 / 2.25 / 2.26 / 2.27 | `docs/门禁说明/font-charset.md`、`id-check.md`、`layer-deps.md`、`bsp-err.md` |
> **注意**:首次对全库运行时,可能批量修改大量文件的行尾空格。这是正常行为,不会改变代码逻辑。
这些编号本身**不再新增引用**(历史变更记录里的旧编号按"冻结历史不回改"保留,按上表查号即可)。
> **豁免清单(2026-10-09 定,`trailing-whitespace` 与 `end-of-file-fixer` 共用同一份,
> 配置里以 YAML 锚点 `&cubemx-generated` 定义)**:`Src/*.{c,h}`、`Inc/*.{c,h}`、
> `Drivers/CMSIS/`、`Drivers/STM32F4xx_HAL_Driver/`、`Middlewares/`、
> `MDK-ARM/*.{uvprojx,uvoptx,sct,map}`、`.mxproject`、`*.ioc`。
> 理由:这些文件的内容事实源是 `.ioc` 与工具链,每次重生成都按工具自己的风格写回
> (行尾空白、文件末尾不带换行)——2026-10-09 实测每次重生成后 `Src/`/`Inc/` 下 10+ 个
> 文件各被补 1 行末尾空行,纯属提交抖动。
> ⚠ **只豁免"改写型"钩子**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` /
> `mem-numbers` 等**只读检查仍覆盖** `Src/`、`Inc/`(存活审计必须能看到这些文件)。
### 2.2 跨钩子的通用机制(这些事实仍归本节)
### 2.2 end-of-file-fixer — 文件尾换行
| 项 | 说明 |
| 机制 | 口径 |
|---|---|
| **触发条件** | 文件最后一行不是换行符(`\n`) |
| **处理方式** | **自动修复**(在文件尾部追加换行) |
| **影响范围** | 项目自有代码;**豁免 CubeMX / Keil 生成文件**(同 2.1 豁免清单) |
| **典型报错** | `Fix End of Files` → `Fixing path/to/file.c` |
| **修复方式** | 无需手动修复,钩子已自动改写;重新 `git add` 即可 |
### 2.3 check-added-large-files — 大文件拦截
| 项 | 说明 |
|---|---|
| **触发条件** | 单个文件 > 1MB(默认阈值) |
| **处理方式** | **阻止提交**(需手动修复) |
| **典型报错** | `ilerge file added: path/to/file.bin` |
| **修复方式** | 确认文件是否应提交;若为构建产物则加入 `.gitignore`,若为必要大文件用 `--no-verify` 跳过 |
### 2.4 check-merge-conflict — 冲突标记拦截
| 项 | 说明 |
|---|---|
| **触发条件** | 文件含 `<<<<<<<`、`=======`、`>>>>>>>` 冲突标记 |
| **处理方式** | **阻止提交** |
| **修复方式** | 手动解决冲突后重新提交 |
### 2.5 check-yaml — YAML 语法校验
| 项 | 说明 |
|---|---|
| **触发条件** | `.yaml` / `.yml` 文件语法错误 |
| **处理方式** | **阻止提交** |
| **检查对象** | 主要校验 `.pre-commit-config.yaml` 本身 |
| **修复方式** | 修正 YAML 缩进或语法后重新提交 |
### 2.6 cppcheck — C 静态分析
cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法分析和数据流推理发现潜在缺陷。本项目开启 `warning` / `performance` / `portability` / `style` 四个类别,任一类别发现问题即**阻止提交**(`--error-exitcode=1`)。
| 项 | 说明 |
|---|---|
| **触发条件** | 自有代码 .c 文件(App/、Src/、Inc/、Drivers/BSP/,第三方库除外)含潜在缺陷、冗余代码或可疑写法 |
| **处理方式** | **阻止提交**(必须修复) |
| **修复方式** | 按报告修复代码;确属误报用行内抑制 `// cppcheck-suppress` 或全局 `--suppress` |
| **典型报错** | `[path/to/file.c:123]: (style) variableScope The scope of the variable ... can be reduced` |
| **增量速度** | 单文件约 1~2 秒;全库约 5 秒 |
> **完整规则清单(style 类别 95 项详解)、配置参数、YAML 引号陷阱、误报抑制方法见 `docs/代码规范/cppcheck使用说明.md`。**
### 2.7 no-mojibake — U+FFFD 乱码拦截
| 项 | 说明 |
|---|---|
| **触发条件** | C 文件含 `EF BF BD`(U+FFFD 替换符,表示不可逆的编码损坏) |
| **处理方式** | **阻止提交**(不可逆,必须修复) |
| **检测原理** | `scripts/check_mojibake.py` 以二进制读取文件,检测 `0xEF 0xBF 0xBD` 字节序列 |
| **典型报错** | `U+FFFD replacement chars found (encoding corrupted, irreversible!): path/to/file.c` |
| **修复方式** | 从 git 历史中找到最后一个干净版本,手动恢复损坏区域 |
> ⚠️ **编码铁律**:U+FFFD 是不可逆损坏(无法从替换符反推原始中文字符)。若发现新文件出现 U+FFFD,必须排查写入方式(禁用 Get-Content / Set-Content 重写中文文件)。
### 2.8 coding-standard — C 代码规范检查
依据 `docs/代码规范/嵌入式C语言代码规范_V1.0.md`,用 `scripts/check_coding_standard.py` 对**可机械化校验**的规则做提交前拦截(纯 Python 正则扫描,无依赖)。
| 项 | 说明 |
|---|---|
| **触发条件** | 自有代码 .c/.h 文件(App/、Drivers/BSP/,第三方与 CubeMX 生成文件除外)违反下列规则 |
| **处理方式** | **阻止提交**(必须修复) |
| **典型报错** | `L123 [CODE-RULE-03 3.4.1] 宏名须全大写+下划线: net_htonl` |
**检查项(S 编号与脚本输出一致)**:
| 编号 | 规范条款 | 规则 |
|---|---|---|
| CODE-RULE-01 | 2.3.1 | .h 文件必须有 `XXX_H` 格式头文件保护宏(禁止 `__` 前缀) |
| CODE-RULE-02 | 5.2 | 文件开头必须有文件头注释(含 模块名称/作者) |
| CODE-RULE-03 | 3.4.1 | `#define` 宏名必须全大写+下划线+数字 |
| CODE-RULE-04 | 3.5.1 | typedef 类型名必须以 `_t` 结尾 |
| CODE-RULE-05 | 6.3.2 | switch 必须包含 default 分支 |
| CODE-RULE-06 | 7.5 | 禁用 strcpy/strcat/sprintf/gets/scanf/strtok(改用安全版本) |
| CODE-RULE-07 | 6.3.3 | 禁止 goto |
| CODE-RULE-08 | 4.1.1 | 禁止 Tab 缩进(4 空格) |
| CODE-RULE-09 | 5.3 | 函数定义必须有函数注释(含「函数功能」) |
| CODE-RULE-10 | 3.3.1 | 函数名必须全小写(HAL_ 厂商 API 回调名豁免) |
| CODE-RULE-11 | 2.3.5 | 禁止相对/绝对路径 include(2026-10-11 细化判定形态:禁的是"用路径找文件"——引号 include 带 `/`、任意形态带 `..` / `\` / 绝对路径或盘符;`<sys/stat.h>` 这类**尖括号系统头允许带子目录**,由 `-I` 解析。原实现把任何含 `/` 的 include 一律判违规,误伤模拟器侧 2 处;全仓库复核:细化后仅这 2 处项目代码判定发生变化,其余 100 处均在已排除的 `Lib/lvgl/`) |
| CODE-RULE-12 | 7.6 | 禁止可变参数函数(`...`) |
| CODE-RULE-13 | 7.7 | 禁止递归函数 |
| CODE-RULE-15 | 6.1.4 | 文件作用域指针变量必须初始化为 NULL |
| CODE-RULE-16 | 3.2.2 | 文件作用域非 static 全局变量必须 `g_` 前缀 |
| CODE-RULE-17 | 2.2 | 非 static 函数必须在配对 .h 中声明(HAL_ 豁免;无配对头则跳过) |
| CODE-RULE-18 | 6.3.1 | 禁止 Yoda 条件(字面量/常量必须在运算符右侧) |
| CODE-RULE-19 | 4.4.2 | 函数定义左大括号必须与右括号同行 |
**不在检查范围**(语义类规则,无法机械可靠判定):函数长度/单一职责、参数数量、错误处理、注释掉的代码等,仍需人工评审把关。
**排除文件**(与 cppcheck 钩子范围一致,另加):`Src/`、`Inc/`(CubeMX 生成,重新生成即覆盖)、`Drivers/BSP/GD5F2GQ5UE/nand_ftl.*`(dhara/FatFS 接口适配层,函数名由外部 API 固定)。
### 2.9 doc-links — 文档链接存在性(D-3/D-7)
用 `scripts/check_doc_links.py` 校验 Markdown 文档里引用的仓库内路径是否真实存在(纯 Python 标准库,无依赖)。
| 项 | 说明 |
|---|---|
| **触发条件** | 改动任意 `.md` 文件,且其中链接指向不存在的仓库内路径 |
| **处理方式** | **阻止提交**(改路径,或按规范加 `[计划]` / 状态词) |
| **检查范围** | `[文本](相对路径)` 链接(相对当前文档解析)+ 反引号内 `` `docs/xxx.md` `` 仓库根相对路径 |
| **典型报错** | `[示例] docs/架构设计/图形绘制流程.md:73 [markdown 链接] images/RemoDispBus帧格式.svg` |
**豁免**(不算失效):`[计划]` 前缀;该行含 `[示例]` 标记(引用报错样例/日志片段时用);引用**附近 ±40 字符**内的状态词(未创建/尚未创建/已删除/待排期);占位符(`*`、`<模块>`、`NN_`、`${...}`);**行内代码 span 里的链接写法**(`![标题](相对路径.svg)` 这类语法示例);外部 URL 与纯锚点;`docs/芯片手册/` 等外部资料目录。
**排除文件**:`.opencode/node_modules/`、`Lib/lvgl/`、`MDK-ARM/RTE/`、`docs/芯片手册/`。
(`simulator/` 已于 2026-10-11 从排除列表移除——其下 3 个 README 是本项目文档,见 §5。)
```powershell
# 全库扫描(开发时自检)
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all
```
> 规范依据:[`docs/文档规范.md`](../文档规范.md) §3(链接规范)、§4(门禁)。
### 2.10 task-table — FreeRTOS 任务表一致性(D-2/D-7)
用 `scripts/gen_task_table.py` 以 `Src/freertos.c` 的 `osThreadAttr_t` 为**唯一事实源**,校验 `README.md` / `AGENTS.md` 中的任务表是否同步(防止改栈后文档失真——2026-09 曾两张表都比源码落后一大截)。
| 项 | 说明 |
|---|---|
| **触发条件** | 改动 `README.md` / `AGENTS.md` / `Src/freertos.c`,且受管表格区与源码不一致 |
| **处理方式** | **阻止提交**;跑 `--write` 由脚本自动重写表格 |
| **受管区** | `<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->` ~ `<!-- TASK_TABLE:END -->` 之间 |
| **典型报错** | `[FAIL] 以下文档的任务表与 Src/freertos.c 不一致:README.md` |
```powershell
& '.venv\Scripts\python.exe' scripts/gen_task_table.py --check # 检查(钩子用)
& '.venv\Scripts\python.exe' scripts/gen_task_table.py --write # 自动同步表格
& '.venv\Scripts\python.exe' scripts/gen_task_table.py --diff # 只看期望表格
```
> ⚠️ **改栈流程**:先改 `Src/freertos.c` → 跑 `--write` 同步文档 → 提交。钩子报不一致时**不要手改表格**。
> 脚本按原换行符 / BOM 状态回写,不会造成整文件行尾漂移。
### 2.11 mem-numbers — 内存/Flash 数字一致性(D-7 体积档)
用 `scripts/check_mem_numbers.py` 以 `.map` 为**唯一事实源**,校验文档里的内存/Flash 数字。
文档中手抄的 `RW_IRAM1 82,048 B / 62.6%` 这类数字会随代码变化而失真——实测 2026-09-26 复核时
文档仍写 `82,048 / 62.6%`,实际 `.map` 已是 `87,040 / 66.4%`。
| 项 | 说明 |
|---|---|
| **触发条件** | 改动 `docs/内存管理/*.md` / `App`·`Src`·`Inc` 源码 / 本脚本 |
| **处理方式** | **阻止提交**;受管区跑 `--write` 自动同步;散句陈旧数字需人工复核 |
| **受管区** | `<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->` ~ `<!-- MEM_SNAPSHOT:END -->` |
| **典型报错** | `[FAIL] 以下文档的内存快照与 .map 不一致:docs/内存管理/RAM使用分析与优化方案.md` |
| **前置** | 需存在 `.map`(`@build` 产物);缺失时脚本报 `[FAIL] 找不到 .map 事实源` 并提示先构建 |
```powershell
& '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --scan # 钩子用(含陈旧散句扫描)
& '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --write # 同步受管快照
& '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --diff # 只看期望快照
```
**陈旧数字扫描**:从 `docs/内存管理/data/ram_report_*.md` 历史快照提取「曾为区域用量、现已变化」
的值,正文若仍以现状口吻引用即告警。含「历史/旧/曾/快照/`data/`/实施时」等词的行使行豁免;
讲任务栈/内存池/峰值水位的行也跳过(数字语义不同)。
> ⚠️ **设计取舍**:朴素"扫所有 4~7 位数字"实测 **45 处误报**(栈配置 `4096`、堆 `49,152`、
> 峰值 `1184` 都是合法值)。改为"只报历史快照里出现的、当前已变的区域值"后 **0 误报**。
> 宁可窄而准——吵的门禁会被绕过。
### 2.12 cubemx-survival — CubeMX 重生成存活审计
用 `scripts/check_cubemx_survival.py` 核对"重生成易失清单",防止 CubeMX 重新生成时
**静默覆盖**手工定制。`.ioc` 的 `ProjectManager.KeepUserCode=true` **只**保护
`/* USER CODE BEGIN/END */` 标记区内的内容——标记之外的一切都会被模板重写,
且多数**不报编译错误**,只在真出故障时才暴露(2026-09-29 实测一次重生成覆盖 4 处,
含 `HardFault_Handler` 崩溃采集被打回空 `while(1)`、两个 FreeRTOS 加固宏被整块删除)。
| 项 | 说明 |
|---|---|
| **触发条件** | 改动 `STM32F407-Demo.ioc` / `Src/*.{c,h}` / `Inc/*.h` / `App/util/task_watchdog.*` / `MDK-ARM/*.uvprojx` / 本脚本 |
| **处理方式** | **阻止提交**;清单项缺失或基线漂移即 FAIL |
| **检查内容** | 16 项:IWDG 延后启动、崩溃采集×2、栈溢出检测、堆失败钩子、断言改造×2、看门狗模块与工程挂载、复位原因锁存、HAL 驱动与使能宏、`.ioc` 关键项×3 |
| **基线文件** | `.cubemx_survival_baseline.json`(各项内容哈希;`--write` 重建) |
| **防误报** | 断言前**剥掉 C 注释**——本项目多处注释刻意引用历史代码(如"原为 `HAL_IWDG_Init(&hiwdg)` 直呼"),不过滤会恒定误报 |
```powershell
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py # 审计报告(人读,含修复提示)
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py --check # 门禁模式(钩子用)
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py --write # 重建基线(确认改动无误后)
& '.venv\Scripts\python.exe' scripts/check_cubemx_survival.py --report # 只打印报告,不比对基线
```
> **推荐工作流**:**每次 CubeMX 重生成后立刻跑一次**(不带参数)——它会把
> "被覆盖了哪几处"直接列出并附修复提示,比事后靠故障现象反推快得多。
>
> **标记区外的存量**以 WARN 提示(`Src/stm32f4xx_it.c` 的 3 个 Fault 处理函数、
> `Src/freertos.c` 的栈注释)——这些 CubeMX 不保护,只能人工盯;建议后续
> 仿照 `HardFault_Handler` **自造 `USER CODE` 标记区**把它们升级为受保护内容。
>
> 方法论(L1 写 `.ioc` > L2 搬进 `USER CODE` 区 > L3 审计兜底)详见
> [`../子系统设计/看门狗设计说明.md`](../子系统设计/看门狗设计说明.md) §10.2。
### 2.13 test-suite-switch — 测试套件开关=产品构建
用 `scripts/check_test_suite.py` 检查 `test/board/test_config.h` 的 `TEST_SUITE_*` 开关
是否已全部注释(= 产品构建),防止**测试固件被静默提交**。
| 项 | 说明 |
|---|---|
| **触发条件** | 改动 `test/board/test_config.h`,且其中有 `TEST_SUITE_*` 处于**启用**状态 |
| **处理方式** | **阻止提交**(注释掉开关,或显式放行测试构建) |
| **典型报错** | `[FAIL] 默认要求产品构建:请注释掉 test/board/test_config.h 中的 TEST_SUITE_* 开关。` |
| **放行方式** | `python scripts/check_test_suite.py --allow-test-build` 或 `ALLOW_TEST_BUILD=1 git commit ...`(放行后仍打 `[WARN]` 横幅提醒) |
**为什么需要这个钩子**(2026-10-01 实测):
`test_config.h` 的 `TEST_SUITE_CH395F/STORAGE/GD5F/RTC` 决定固件里编入哪个测试套件,
**全部注释 = 产品构建**——但此前 `grep TEST_SUITE scripts/*.py` **零命中**,没有任何门禁
检查它。实测已提交的 HEAD 里 `TEST_SUITE_CH395F` 就是开启状态(`.map` 含 34 处 test 代码),
即仓库里躺着的是**测试固件**而非产品固件,纯靠开发者记得改回去。
**判定实现**:先剥掉块注释与行注释再匹配 `#define`,所以注释掉的开关
(`//#define TEST_SUITE_GD5F`)不会误判;同文件派生的 `TEST_SUITE_SELECTED` /
`TEST_SUITE_NAME` 由**白名单**排除(它们是开关的推导结果,非用户可选项)。
```powershell
& '.venv\Scripts\python.exe' scripts/check_test_suite.py # 门禁判定
& '.venv\Scripts\python.exe' scripts/check_test_suite.py --allow-test-build # 显式放行
```
> 套件互斥(一次最多启用一个)由 `test_config.h:45` 的 `#error` 在**编译期**兜底;
> 本钩子在**提交期**再拦一道(`#error` 只在真正构建时暴露,测试固件仍可能被提交)。
### 2.14 gitlint — Commit Message 格式
| 项 | 说明 |
|---|---|
| **触发条件** | commit message 标题超 100 字符 / **正文单行超 80 字符** / 其他格式规则 |
| **处理方式** | **阻止提交**(commit-msg 阶段) |
| **当前规则** | 宽松:不强制 Conventional Commits,兼容中文标题。配置源是仓库根 `.gitlint`(**不是** hook args):`ignore=body-is-missing`(**不强制写正文**)+ `[title-max-length] line-length=100`;正文行宽走 gitlint 默认 **80**(未关) |
| **典型报错** | `T1 Title exceeds max length (159>100)`、`B4 Line too long (87>80)` |
| **修复方式** | 标题压到 100 字符内(中文长标题极易超限);长说明一律用**短标题 + `git commit -F <文件>` 多行正文**,正文每行 ≤80 字符。消息文件必须 **UTF-8 无 BOM**——用 `-F 文件` 而非 `-m "…"`,防终端代码页转码破坏中文 |
### 2.15 lv-conf-sync — 两份 `lv_conf.h` 特性子集一致性(2026-10-11 加)
| 项 | 说明 |
|---|---|
| **背景** | `Lib/lv_conf.h`(MCU)与 `simulator/lv_conf.h`(PC)是**两份独立文件**,长期漂移:模拟器早已 I1-only,MCU 端却让 11 个 `LV_DRAW_SW_SUPPORT_*`、5 个未用控件类、`LV_USE_THEME_SIMPLE`、`LV_USE_FLEX/GRID` 靠 `lv_conf_internal.h` 默认值隐式生效 → 固件白链 **64,576 B 死代码**;更糟的是模拟器 golden 回归跑的是另一套特性子集,**验证不到固件实际编译的配置** |
| **触发条件** | `Lib/lv_conf.h` 或 `scripts/check_lv_conf_sync.py` 变动 |
| **判定口径** | 比对**生效值**:文件里没定义的宏,取 `Lib/lvgl/src/lv_conf_internal.h` 的兜底默认。⚠ 该文件每个选项都先套一层 `#ifdef LV_KCONFIG_PRESENT` → `CONFIG_LV_*` 分支**不是**默认值,脚本取块内**非 `CONFIG_` 的最后一个字面量**(首版按"紧跟 `#ifndef` 的那行"取,结果整表都是 `CONFIG_*`,教训) |
| **四类问题** | `MISMATCH` 生效值不同 → FAIL;`INVALID` 写了 LVGL 不认的宏名 → FAIL(`LV_USE_THEME_BASIC` 同类);`DUPLICATE` 同文件定义两次 → FAIL(后者生效,极易误读);`IMPLICIT` 一端靠默认值 → 警告(默认值随 LVGL 版本变,建议显式固化) |
| **端专属白名单** | SDL2 驱动 / 演示工程 / include 替换 / 调试辅助(`LV_USE_OBJ_NAME` 等)/ 断言(固件关、模拟器开)/ `LV_USE_SNAPSHOT`(模拟器 `--screenshot` 需要)——见脚本 `IGNORE_PREFIX` 与 `ALLOW_DIFF`(后者必须写理由) |
| **典型报错** | `FAIL: N 项不一致` + `[MISMATCH] LV_USE_OBSERVER MCU=1(默认) vs SIM=0(显式)` |
| **修复方式** | 把两端拉到同一生效值(**优先显式写死**,别依赖默认),或在 `ALLOW_DIFF` 里登记理由 |
| **触发范围** | `Lib/lv_conf.h`、**`simulator/lv_conf.h`** 或脚本自身变动 → 两端任一改动都会跑。⚠ 早期版本只能由 MCU 端触发,因为顶层 `exclude` 整目录挡掉了 `simulator/`(已删,见 §5);pre-commit 4.x 又移除了 `always: true`(实测对 `local` 钩子只告警并跳过),所以没有"总是跑"的兜底写法——**必须让文件模式覆盖两端** |
| **首次落地实测** | 两端共清掉 **26 个 9.3 不存在的选项**(LVGL 8 / 9.4 模板残留)、3 处同文件重复定义,并把 10 项 MCU 端靠默认值隐式生效的开关显式固化为 0(含默认 =1 的 `LV_USE_OBSERVER` / `LV_USE_DRAW_PXP` / `LV_USE_IMAGEBUTTON`)。本次清理对镜像体积**中性**(那些 TU 本就不在 uvprojx,或编成 0 字节空对象),价值在防漂移与可读性 |
### 2.16 sha256-unit-test — SHA-256 主机侧单测(2026-10-11 加,N-19)
| 项 | 说明 |
|---|---|
| **触发条件** | `App/util/sha256.*`、`test/host/sha256/**` 或 `test/host/common/unit_test_harness.py` 变动 |
| **处理方式** | **阻止提交**(单测失败即拦截) |
| **被测对象** | `App/util/sha256.c/h`(FIPS 180-4 实现,零堆分配)——FTP `pwd_hash` 校验的地基 |
| **用例** | 13 例:FIPS 180-4 标准向量(空串 / `abc` / 56 B 补位跨界 / 1 MB)+ `hex_eq` 非法输入 |
| **为什么需要** | 哈希算错是"正确口令被拒 / 错误口令放行"的**静默语义错误**,板端只见 `530`/`230` 看不出根因;主机侧毫秒级钉死。零平台依赖,无桩 |
| **运行器** | `test/host/sha256/run_unit_test.py`(MSVC `cl.exe`,L0.5) |
### 2.17 ftp-auth-unit-test — FTP 鉴权状态机主机侧单测(2026-10-11 加,N-19)
| 项 | 说明 |
|---|---|
| **触发条件** | `Drivers/BSP/NET/lftpd/lftpd_auth.*`、`App/util/sha256.*`、`test/host/ftp_auth/**` 或 `test/host/common/unit_test_harness.py` 变动 |
| **处理方式** | **阻止提交**(单测失败即拦截) |
| **被测对象** | `Drivers/BSP/NET/lftpd/lftpd_auth.c/h`(纯函数鉴权状态机,tick 由调用方参数注入,零平台依赖) |
| **用例** | 36 例:连续 5 败锁定 / 60 s 解锁 / 成功清零 / tick 32 位回绕 |
| **为什么需要** | 锁定 60 s / 解锁 / 清零 / 回绕是**防口令爆破契约核心**——板端每验一条要真等 60 s、回绕要跑满 49.7 天;纯函数设计(tick 注入)就是为让这些分支主机侧毫秒级穷举 |
| **运行器** | `test/host/ftp_auth/run_unit_test.py`(MSVC `cl.exe`,L0.5) |
### 2.18 registration-check — 双端注册反向校验 + UI 页面接线(2026-10-12 加,N-23;U4 补强 2026-10-15)
| 项 | 说明 |
|---|---|
| **触发条件** | 任意 `.c` 文件 + `*.uvprojx` + `CMakeLists.txt` + 脚本自身变动(pre-commit 4.x 无 `always: true`,`files:` 必须覆盖全部触发面) |
| **处理方式** | **阻止提交**(存在**新**缺口或幽灵注册即拦截);存量缺口走白名单只 `[WARN]` 不拦 |
| **检查对象** | 扫描 `App/`、`Drivers/BSP/`、`Src/` 下全部 `.c`(排除第三方),对照两份注册表:`MDK-ARM/STM32F407-Demo.uvprojx` 的 `<FilePath>` + `simulator/CMakeLists.txt` 的显式源列表(先展开 `${NETCFG_DIR}` 等变量) |
| **四类缺口** | ① 存在但未进 uvprojx(固件不编译)② 存在但未进 CMakeLists(模拟器不编译,**仅当本应进模拟器时**才算)③ 注册了但文件不存在(**幽灵注册**,改名/删除后残留)④ UI 页面模块未接入描述符表(见下行,U4) |
| **UI 页面门禁(U4,2026-10-15 补)** | UI 页面的"注册"有**两层**:**构建注册**(uvprojx + CMakeLists,①② 类)与**框架注册**(`ui_content_page.h` 枚举 + `ui_content_page.c` 的 `s_defs[]` 描述符表)。①②③ 原本只照得出前者——实测"页面已进两端构建但没接 `s_defs[]`"时钩子**全绿**:desc 无人引用被链接器整段剥离,两端编译链接全过、菜单永远打不开这一页,是又一形态"写了却从不生效"的死代码。故补 ④:凡 `ui_page_*.c` 定义 `ui_page_<name>_desc`,该符号必须在 `Drivers/BSP/UI/` 内(除该页自身 `.c`/`.h`)被引用,否则 `[GAP]` FAIL。**新增 UI 页面必须过本钩子**即此机械口径;U3 落地生成式 `ui_page_registry.c` 后规则依旧成立(不定义自身 desc 的 `ui_page_*.c` 不判为页面模块)。⚠ 本钩子照不出**枚举值漏加**(`ui_content_page.h` 的 `content_page_id_t`)与**菜单树漏挂节点**——前者是 `s_defs[]` 下标依据(漏加则表项编译不过,由 `@build` 兜底),后者归 U3 生成器 + golden 回归 |
| **豁免机制** | MCU-only 文件不进模拟器是**正确**的(模拟器另有 `*_sim.c` 替身或无对应)。豁免用**显式清单 + 每行理由**(命名模式 `*_mcu.c` + 个别点名),**不整目录一刀切**(N-23 教训正是目录级盲区);唯一目录级豁免 `Src/`(CubeMX 生成,MCU 专属)→ 见脚本 `SIM_EXEMPT` |
| **存量白名单** | `KNOWN_GAPS` 登记 N-23 存量缺口(`App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c`,Lead 处置中)→ 命中打 `[WARN]` 不拦,只对新缺口 FAIL,故钩子可立即上线不拦现有提交;`--strict` 关白名单供审计 |
| **为什么需要** | 新 `.c` 必须手动注册两处才会被编译;历史上 `rs485_task.c`/`lftpd_log.c` **两处都没注册** = 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效(出处审查报告 N-06 残余);④ 的出处是报告 §6.5 U4"把注册检查算作 UI 页面门禁一环"(2026-10-15 实测 ①②③ 对未接线页面全绿,故补规则) |
| **运行器** | `scripts/check_registration.py`(纯标准库;退出码 0=一致 / 1=有缺口 / 2=环境错误) |
### 2.19 mem-budget — RAM 预算 92% 阈值门禁(2026-10-12 加,N-27 三件套)
| 项 | 说明 |
|---|---|
| **触发条件** | 全部自研代码目录(`App/` `Drivers/BSP/` `Src/` `Inc/`)+ `Lib/lv_conf.h` + `test/board/` + `scripts/ram_baseline.*` + `docs/内存管理/` + 本配置文件 |
| **处理方式** | **阻止提交**(RW_IRAM1 / RW_CCM 任一区占比 >92%,或台账受管区与 `.map` 漂移);无 `.map` 自适应跳过(同 `mem-numbers` 在 CI 的行为,盲区登记 `docs/任务规划/测试与架构专项.md` A-5 残余) |
| **检查逻辑** | 解析 `.map` 执行区占比 → 超阈值即 FAIL 并附 **Top-10 RW+ZI 对象表**(定位增量元凶);同时校验 `docs/内存管理/RAM预算台账.md` 受管区与 `.map` 一致(漂移 → 提示 `--write`) |
| **软提醒**(不拦) | tsv 末行落后于 `.map`(忘跑 `--update`);`.map` 早于最新源码(忘 `@build`) |
| **配套三件套** | `scripts/ram_baseline.py`(门禁/趋势/受管区生成)+ `scripts/ram_baseline.tsv`(每次 `@build` 后 `--update` 追加的趋势基线,`--trend` 高亮 RW+ZI 增长 >1 KB 的对象)+ `docs/内存管理/RAM预算台账.md`(子系统配额 + CCM 台账 + 超配额流程) |
| **为什么需要** | 2026-09 体系评审提出的"超阈值即失败 + Top-10 附表"(现沉淀在 `docs/问题记录/项目审查与优化建议报告.md` §5.5 闭环说明)此前**无任何机械检查**——`mem-numbers` 只防"文档数字漂移"不防"占用超阈值";B4/B5 要新建任务与缓冲,红线必须先机械化(登记簿 N-27 前半 / W1-0) |
| **运行器** | `scripts/ram_baseline.py --gate`(纯标准库,复用 `ram_report.py` 的 `.map` 解析口径;退出码 0=通过/跳过 / 1=超阈值或漂移 / 2=解析失败) |
### 2.20 iec104-unit-test — IEC 60870-5-104 协议栈主机侧单测(2026-10-12 加,B4)
| 项 | 说明 |
|---|---|
| **触发条件** | `App/protocol/iec104/iec104_(asdu|pts|link).*`、数据源依赖 `App/algo/(prot_set|prot_elm|phasor|sample_ring|adc_input).*`、`test/host/iec104/**` 或 `test/host/common/unit_test_harness.py` 变动 |
| **处理方式** | **阻止提交**(单测失败即拦截) |
| **被测对象** | `App/protocol/iec104/` 三模块(ASDU 编解码 / 点表+数据源 vtable / APCI+链路 FSM)——纯逻辑、tick 注入、发送经回调注入,零 I/O(A-4 约束 1) |
| **用例** | 82 例:CP56 往返 / APCI 半帧粘帧坏帧 / STARTDT-STOPDT / 总召分片+RBT / COV 死区 / k=12 窗满 / w=8 触发 S / t1-t2-t3 / N(R) 倒退判故障 / 遥控两段式 / 定值越界 PN / C_CS 对时 / 未知类型-地址错误应答 / 周期上送 |
| **为什么需要** | 板端复现这些分支要真主站 + 真等待(t1=15 s、k 窗满要发满 12 帧);link 层 tick 注入 + pts vtable 抽象就是为 L0.5 毫秒级穷举设计;数据源依赖进触发面——测试直接 `#include` 这些生产源,签名变了立刻暴露 |
| **运行器** | `test/host/iec104/run_unit_test.py`(MSVC `cl.exe`,L0.5) |
### 2.21 freq-unit-test — 频率测量层主机侧单测(2026-10-12 加,B8)
| 项 | 说明 |
|---|---|
| **触发条件** | `App/algo/freq.*`、`App/algo/phasor.*`、`App/algo/sample_ring.*`、`test/host/freq/**` 或 `test/host/common/unit_test_harness.py` 变动 |
| **处理方式** | **阻止提交**(单测失败即拦截) |
| **被测对象** | `App/algo/freq.c/h`(L2.5 测频:共轭乘积相位增量 + 解缠绕 + 自适应窗长 K_eff 对齐负序泄漏纹波周期 + 假值钳制带 35~65 Hz + 幅值门 valid=0)——纯合成信号、零 I/O |
| **用例** | 46 断言:J1 扫频 45~55 Hz ≤0.02 Hz(实测 0.0101)/ J2 噪声 1% ≤0.10 Hz / J3 启动收敛 ≤40 ms / J4 阶跃 50→51 ≤120 ms(精度优先于响应,见 `docs/算法/采集与算法层设计说明.md` §7.2)/ J5 浮空 valid=0 不发散 / J6 双跳变钳制恢复 |
| **为什么需要** | 测频精度是 OF/UF 判据的地基;板端无标准源照不出来(规划 §3.3 L3-5 注入验收归 B10);定长窗负序泄漏纹波(0.317 Hz)这类坑只有扫频合成信号才能暴露 |
| **运行器** | `test/host/freq/run_unit_test.py`(MSVC `cl.exe`,L0.5) |
### 2.22 soe-unit-test — SOE 动作记录编解码主机侧单测(2026-10-12 加,B9)
| 项 | 说明 |
|---|---|
| **触发条件** | `App/log/soe_rec.*`、`App/log/soe_store.*`、`test/host/soe/**` 或 `test/host/common/unit_test_harness.py` 变动 |
| **处理方式** | **阻止提交**(单测失败即拦截) |
| **被测对象** | `App/log/soe_rec.c/h`(64 B 定长 SOE 记录纯编解码:类型/elm_id/IV 位/seq u32/CP56Time2a/epoch_s/value/sum8)——`soe_store` 板端胶水 SIM_EXEMPT |
| **用例** | 32 断言:编解码往返 / 截断与坏校验和拒绝 / IV 位(无链路时标)/ seq 连续性 / 类型枚举(元件动作·复归、压板出·入、采样坏·复)双类型断言 |
| **为什么需要** | 记录格式是**磁盘上的字节级契约**——编解码错了要 FTP 下载 + 解码才看得见,板端日志照不出来;与 `/log/run.log` 同款定长 64 B 语义 |
| **运行器** | `test/host/soe/run_unit_test.py`(MSVC `cl.exe`,L0.5) |
### 2.23 nand-port-unit-test — dhara NAND 移植层主机侧单测(2026-10-15 加,N-21)
| 项 | 说明 |
|---|---|
| **触发条件** | `Drivers/BSP/GD5F2GQ5UE/nand_ftl.(c\|h)`、`gd5f2gq5ue.h`、`Lib/dhara/(map\|journal\|nand\|error\|bytes).(c\|h)`、`test/host/nand_port/**` 或 `test/host/common/unit_test_harness.py` 变动 |
| **处理方式** | **阻止提交**(单测失败即拦截) |
| **被测对象** | `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` 的 dhara NAND HAL 六个入口(`is_bad` / `mark_bad` / `erase` / `prog` / `is_free` / `read` / `copy`)——纯地址算术 + 边界判定;测试单元 `#include` 真实 `nand_ftl.c` + 真实 `Lib/dhara/journal.c` / `map.c`,芯片侧换成 **RAM 假芯片**(32 块 × 64 页 × 2048 B,两段式读缓存、NOR 叠加编程、双编程违规计数、访问次数记账) |
| **用例** | 149 断言:整页扫描(页首 64 B 全 0xFF + 第 1000 字节有数据 / 页尾单字节 / 第 64 字节)+ **旧算法对照**(内联"只看页首 64 B"的旧实现,同一页答"空闲"而新实现答"非空")+ 六个入口的越界守卫(`DHARA_PAGE_NONE` / `total_pages` / `num_blocks`,断言 `-1` + `DHARA_E_CORRUPT_MAP` + **零芯片访问 / 零标坏块**)+ 正常往返 + 故障注入(ECC / read_from_cache / program_load / program_exec / erase 失败)+ dhara map 级"写入 → 重新 resume"场景(用 64 B 列读指纹证明 resume 真走了 is_free,再断言全程**双编程计数为 0**) |
| **为什么需要** | N-12 / N-13 两类缺陷真机**极难复现**:is_free 误判要恰好撞上"页首 0xFF、后段有数据"的页形态,越界地址要哨兵从别处漏进来;两者发作后都只表现为"某次复位后丢数据",无从定位。移植层是纯逻辑 + 假芯片,能钉死真机极难复现的越界/误判路径,且"越界不产生任何芯片访问"这类判据在板端**根本拿不到观测面** |
| **运行器** | `test/host/nand_port/run_unit_test.py`(MSVC `cl.exe`,L0.5);套件说明见 `test/host/README.md` |
### 2.24 font-charset — UI 字符集差集检查(2026-10-15 加,报告 §6.5 U4 残余)
| 项 | 说明 |
|---|---|
| **触发条件** | `Drivers/BSP/UI/` 下任意文件(含字体 `ui_font_cn_12.c` 自身)、`App/task/ui_task.(c\|h)`、`ui_cn_chars.txt`、`tools/font_gen/`(两个生成器)或本脚本自身变动(pre-commit 4.x 无 `always: true`,`files:` 必须覆盖全部触发面) |
| **处理方式** | **阻止提交**(存在"白框方向"缺口即 FAIL);字符集**冗余**字 `[WARN]` 不拦(只费 Flash 不致白框,`--strict` 升级为 FAIL 供审计/瘦身排期) |
| **判定口径** | 两段链条:**A.** UI 源码在用字符 vs `ui_cn_chars.txt`——源码有、txt 无 → FAIL(字体必然无此字形);txt 有、源码无 → WARN。**B.** `ui_cn_chars.txt` vs `ui_font_cn_12.c` 的 cmap——txt 有、字体无 → FAIL(`lv_font_conv` 对缺字形/低码位**静默丢字**)。A 过 + B 过 ⟹ 字体 ⊇ 源码每个在用字 |
| **单一规则源** | 本脚本**不自创第二套解析规则**:字符集提取复用 `tools/font_gen/extract_cn.py` 的 `SCAN` 清单 + `collect()` 状态机(跳注释/字符常量、只取双引号串内非 ASCII),cmap 覆盖判定复用 `tools/font_gen/make_font.py` 的 `verify_coverage()`(含稀疏 `unicode_list` 与补回字形的单码位条目),importlib 直载零副作用——生成期校验与提交期校验**同源**,杜绝两套规则漂移(§5.5 同类教训) |
| **典型报错** | `[GAP] 字体缺字 1 个(在字符集、不在 Drivers/BSP/UI/ui_font_cn_12.c 的 cmap -> lv_font_conv 静默丢字,上机白框): 压(U+538B)`,附 font-regen 四步修复提示 |
| **修复方式** | 按 **font-regen** skill 重跑 `extract_cn.py` → `make_font.py` → diff 检查,`ui_cn_chars.txt` 与 `ui_font_cn_12.c` 随代码一起提交 |
| **为什么需要** | `ui_font_cn_12.c` 是**子集**字体,只含 txt 列出的码位;`make_font.py` 的 `verify_coverage` 只在**生成期**校验"txt ⊆ cmap"——改完 UI 字符串忘了重跑 font-regen 就提交,缺字上机渲染为空白(白框),无日志无报错、只能肉眼截图发现。font-regen skill 的约定此前**纯靠自律**,本钩子把它机械化(U4 三钩子至此齐:golden ✅、字符集差集 ✅、注册检查算作 UI 门禁 ✅ → §2.18 ④) |
| **首次落地实测(牙齿)** | 注入均打在**临时副本**(`--chars` / `--font` 覆盖,不碰仓库文件):① txt 删 ▶ → FAIL 点名 `▶(U+25B6)`;② 字体副本删 U+25B6 单码位 cmap 条目 → FAIL 同字;③ 字体副本删 U+538B 的稀疏 `unicode_list` 偏移 → FAIL 点名 `压(U+538B)`;基线 PASS。落地时 txt 内有 **7 个历史冗余字**(上/常/异/片/电/芯/重,源码已不用)→ 仅 WARN 不拦,下次 font-regen 顺带瘦身 |
| **运行器** | `scripts/check_font_charset.py`(纯标准库;`--chars` / `--font` 指临时副本做注入测试、`--strict` 升级冗余字;退出码 0=通过 / 1=白框缺口 / 2=环境错误) |
### 2.25 id-check — 编号门禁(2026-10-20 加,`docs/编号规范.md` §8)
| 项 | 说明 |
|---|---|
| **触发条件** | 任意 `.md` / `.c` / `.h` / `.py` / `.yaml` / `.json` / `.txt` 或本配置文件变动(pre-commit 4.x 无 `always: true`,`files:` 按扩展名覆盖全触发面);**`pass_filenames: false`**——悬空引用 / 孤儿条目 / 重复定义是**跨文件**判定,只喂变更文件必然判错,故每次恒扫全库(实测 536 个文件 1.1 s,够快) |
| **处理方式** | **2026-10-21 已关闸为【增量拦截】**:`entry` 带 `--strict`,只拦**台账之外**的新错误级问题;存量 710 处按 `scripts/id_known_backlog.json` 的指纹豁免(指纹 = 文件 + 校验号 + 字面量 + 计数,**故意不含行号**——行号会漂,用它当指纹台账天天假新增)。台账只减不增:`--prune-backlog` 收已还清的,条目变多或出现新字面量都当场拦;`--strict` 缺台账直接返回 2,**不退化成一次性拦死存量**(那会逼出 `--no-verify`)。另有一根恒生效的线:可判定比例跌破 `id_registry.json` 基线(82.6%)→ 任何模式都 FAIL |
| **八项校验**([示例] 本行引用的是各种非法形态示例,不是真实编号) | ①未登记前缀 ②形态不合规(裸形 `S3` / 空格形 `Trap 07` / 中文形 `陷阱 1` / 补零错 / 档位词带后缀 / 大写后缀 / 缺模块码)③悬空引用 ④重复定义 ⑤冻结前缀越游标 ⑥文档内编号未紧邻文档名 ⑦孤儿条目(告警)⑧可判定比例(只许涨不许跌) |
| **配置两份** | `scripts/id_registry.json`:前缀族 → 属主登记处 / 形态正则 / 是否冻结 / 游标,**逐项来自 `docs/编号规范.md` §4 总表**,游标取「规范记载值」与「全库实测最大号」的较大者,偏差写进 `cursor_note`(实测偏差见规范 §8.2);`scripts/id_aliases.json`:旧号 → 新号 / 作废标记(照搬 `section_aliases.json` 的成功范式) |
| **判定口径** | 「定义」= 字面量出现在该族**属主登记处文档**里(不依赖表格行首等易漂移形态)→ §5.5「销项 = 删行 + 清全库引用」只做一半,校验 ③ 立刻报(`N-31`/`N-32` 正是这条)。「重复定义」= 字面量出现在**定义位**(表格首格 / 标题行 / 列表项行首),正文提到不算,否则任何交叉引用都会误判成事故。多族同形时的定族顺序:所在文档即属主 > 紧邻属主线索 > **定义位优先**(`docs/编号规范.md` §4.3 冻结快照族 `N-RPT`/`D-RPT`/`S-RPT`/`R-RPT`/`F-RPT`/`P-AUDIT`/`A-AUDIT`/`B-AUDIT` **不参与**——否则已销项的登记簿 `N-21` 会被审计报告同号接管,门禁反过来教人补错文档名,实测把校验 6 从 266 撑到 1437、可判定比例假崩到 68.7%)> 判悬空的族 > 全局族;小写子项后缀(`-b`/`-c`)在母号在册时视为已定义(`subitem_ok`),差异仍由双向追溯如实报 |
| **顺带收编**([示例] 本行引用了非法形态示例号)| `scripts/check_test_traceability.py`(TC 用例双向追溯)由本钩子一并调用:正则补齐 `TC-[A-Z]{1,4}-\d{3,4}[a-z]?` 与大写后缀 / 无模块码形态(旧 `TC-[A-Za-z0-9]+-\d+` **整条漏扫** `TC-ADCIN-01A`),比对范围从「`test/board/*.c` ↔ `*_Test_Guide.md`」扩到 `test/host/` 一侧 + 7 篇「单元测试说明.md」(差异从 33 处涨到 326 处——**原口径漏的正是主机侧单测这一半**),另加 `--board-only` 回退旧口径。**2026-10-21 同口径改写**:差异分三档 `DOC-ONLY`(文档声明而代码没有,真断链 38)/ `CODE-ONLY`(代码有、母号不在登记处且号旁无意图,真未登记 7)/ `IN-CODE`(靠号旁写着意图放过 238,**弱于文档的保证,单列示数不算过关**);另修 `TC_RE` 模块码上界 `{0,4}`→`{0,8}`(批 2 单词化后代码侧一度从 556 缩到 217) |。**批 5(2026-10-21)再补口径**:变更记录行与 `[示例]`/`[计划]` 行不参与双向比对(冻结历史按 §8.1 不回改、旧号靠别名解析),`DOC-ONLY` 38 → 29 且交集 260 不变;被豁免的号进 `frozen_ids`,`--verbose` 列出——**豁免不等于已对应** |
| **最新实测(2026-10-21,B 层口径后)** | 8322 处编号字面量 / 可判定 **82.71%**(6883/8322);错误级 711 = ②398 + ③44 + ⑥269,告警级 ③223 + ⑦100。⚠ **与 2026-10-20 的 1312 不可直接比**:其中 504 处属**口径修正**(主机侧单测 118 号实测在代码目录外零消费者,改认 `test/host/<目录>/*` 为属主登记处;子项后缀母号在册即算已定义),74 处属我修的**注册表漏配属主**(活登记处在 `docs/任务规划/README.md`,`N-21` 在其中出现 2 次而我只登记了 `登记簿.md`),**真正变好 0 处**——完整对照见 `docs/编号规范.md` §8.1 口径修正记录 |
| **有牙实测(牙齿)**([示例] 下列字面量是注入用的假编号) | 注入打在临时档(`git add -N` 进索引,跑完 `git rm --cached` + 删除,`git status` 干净):`Z-99`→①、裸形 `S3`→②、`TC-NET-9999`→③、第二篇属主文档重复定义 `DP-01`→④、`E-70`→⑤(`E-xx` 冻结于 `E-63`)、裸引 `D-7`→⑥;六项分别被对应校验点名,`--strict` 退出码 1 |
| **显式盲区(不是"检查通过")** | ① 裸形同字母多义(§1.2:`P1` 五义 / `L1` 四义 / `U1` 四义 / `D7` 三义)与元器件位号同形(§4.4)→ 裸形族**只在属主登记处文档内**判越游标,文档外降告警;② 圈号编号(①②③…)与正常列举无法机械区分,不入判定;③ `docs/芯片手册/`、`docs/硬件平台/` 整体排除(位号 `B1`/`C1`/`L1` 与编号同形,是撞形根源)。前两块收口属 §9 阶段 1-②/1-③ 迁移批次,落地后再扩判定范围——盲区登记在 `id_registry.json` 的 `blind_spots` / `unmanaged_bare` |
| **运行器** | `scripts/check_ids.py`(纯标准库;`--strict` 拦提交、`--update-baseline` 抬基线(**只许涨**,跌则拒绝写入)、`--only 1,3,5` 跑部分、`--top N` / `--verbose` 出细节、`--no-trace` 跳过 TC 追溯;退出码 0=通过或仅告警 / 1=错误或比例跌破基线 / 2=环境错误) |
### 2.26 layer-deps — 分层方向:BSP 不得反向 include App(2026-10-22 加,报告 §0.4 结构债 S-1)
| 项 | 说明 |
|---|---|
| **为什么需要** | AGENTS.md 与本仓分层图一直写着"六层单向依赖",但**从来没有机械检查**——`Drivers/BSP/` 里可以随手 `#include "cfg_file.h"` 而无人报警。后果可测量:2026-09 审查报告记"NETCFG 一处",2026-10-22 实测已长到 **8 个文件 9 处**。边界的静默腐蚀只能靠门禁止住 |
| **判据** | 扫 `Drivers/BSP/**.{c,h}` 的 `#include "..."`,按 `App/**` 的头文件名索引解析(同名头按字面路径消歧)。命中后分三类:**ALLOW 豁免**(横切叶子能力:`App/log/console_log.h` 日志后端、`App/util/sys_clock.h` 时间源、`App/util/sha256.h` 纯算法——任何层都要用、不依赖上层服务)/**BASELINE 存量**(2026-10-22 清点的 6 条:`cfg_file.h`×2、`vol_lock.h`×1、`sys_log.h`×3)/**新增 → FAIL** |
| **增量拦截语义** | 与 `id-check` 同构:**基线只减不增**,脚本没有 `--write` 往里加。修完一条跑 `--prune`,它会点名"这条已消失,请从 `BASELINE` 删除"→ "还欠几条"变成可见进度而不是一句口号 |
| **修法(报错时给用户的三条路)** | ① 把上层能力**注入**给 BSP(函数指针/回调,参照 `net_cfg_set_save_req_cb` / `net_cfg_request_apply` 的既有先例);② 由上层把数据取好**传进来**(参数/结构体),BSP 不自己去拿;③ 若它其实是叶子工具(无状态、不依赖上层服务),才可加进 `ALLOW` 并注明理由 |
| **不是缺陷清单** | 本钩子只拦**静态 include 方向**。**运行时的属主纪律本来就没破**——UI 只投递 `net_cfg_save_request()`,真正写盘在 defaultTask;模拟器也早在主机侧跑整条配置链。别把它读成"以前会写出错" |
| **配置** | `files: ^Drivers/BSP/\|^scripts/check_layer_deps\.py$\|^\.pre-commit-config\.yaml$`、`pass_filenames: false`(要跨文件建 App 头索引,恒扫全库;实测 83 个 BSP 源文件,毫秒级) |
| **运行器** | `scripts/check_layer_deps.py`(纯标准库;`--verbose` 列全部边与判定、`--prune` 只报可删基线;退出码 0=通过 / 1=新增反向依赖) |
| **实测自证** | 落地时做过一次"故意违规"验证:临时给 `Drivers/BSP/RS485/rs485.c` 加一行 `#include "cfg_file.h"` → 钩子当场 FAIL 并点名该文件与该头(退出码 1),删掉后转绿。**没有当场触发过的门禁等于没有门禁** |
### 2.27 bsp-err — BSP 错误码与超时命名约定(2026-10-23 加,报告 S-5)
| 项 | 说明 |
|---|---|
| **为什么需要** | 报告原文说"无统一错误码",实测更准的说法是**六套并存**:0/−1(SD2506、NETCFG、nand_ftl)、
自研枚举(`CH395F_STATUS_*`)、芯片寄存器原生码(`CH395F_ERR_*`,与前一套同模块并存)、
`HAL_StatusTypeDef`(`rs485.h` 曾直接暴露)、`bool`,外加第三方 dhara 的出参 `dhara_error_t`
与 FatFs 的 `FRESULT`。超时侧更乱:**同名形状装三种东西**——真毫秒但带不带 `_MS` 后缀不统一(`SD2506_I2C_TIMEOUT_MS` vs
`GD5F_SPI_TIMEOUT`,后者逐行核过也是毫秒)、以及叫 `*_TIMEOUT` 其实是**返回码**的,`osWaitForever` 与 `HAL_MAX_DELAY` 又**同为 0xFFFFFFFFU**(值同域不同,
跨层转手既不报错也不变慢)。**没有机械检查的约定等于没有约定**(分层方向那条的教训) |
| **约定正文在哪** | `Drivers/BSP/bsp_err.h`(**header-only,无 `.c`** → 不占体积、不需双端注册)。
`0 = BSP_OK`、负值失败、`BSP_ERR_CAN_RETRY(e)` 把"能不能重试"变成跨模块可判定(这是本契约
真正值钱的一条:以前"可重试"只写在个别注释里) |
| **四条检查** | **`err-type`**(错误通道不外泄)公开 API 不得用 `HAL_StatusTypeDef` / `bool` 当错误通道(`is_/has_/can_/valid`
这类**谓词**白名单放行——第一版误判了 7 个正当谓词,我们把规则改窄,**不拿基线去洗规则**);
**`timeout-name`**(超时宏不取负值)`*_TIMEOUT` 宏不得取负值(名字叫 TIMEOUT 却是返回码 → 一律改 `BSP_ERR_TIMEOUT`);
**`return-doc`**(公开函数必须写返回值)非 void 的公开 API 必须有「返回值:」注释(约定要能被读到才谈得上审查;匹配时**忽略空格**,
因为规范 V1.0 允许列对齐写法「返 回 值:」——门禁去规定排版会把合规注释判成违规,实测就有两条因为
**函数说明**里顺口出现"返回值"三个字而误判通过);
**`consume-result`**(返回值不得裸丢弃)返回错误码的函数,调用点必须**接住返回值**或**显式 `(void)` 丢弃**(裸调用即违规)。must-use
名单**自动取**、不手写:① 返回 `bsp_err_t` 的公开 API;② 返回 `int`/`int32_t` 且文档块出现"错误码 /
失败 / BSP_ERR / `_FAIL`"的函数;③ 已知返回 `HAL_StatusTypeDef` 的 HAL 传输/读写族。**名单必须同时
覆盖 `.c` 里的 static 定义**——只扫头文件时 `gd5f_set_feature` 这类 static 原语在名单之外,等于只守
跨编译单元的边界、看不见编译单元内部的丢弃(实测漏掉后靠人工才发现一处 ECC 丢弃)。豁免两类形状:
**谓词**(`is_/has_/can_/should_/need_/valid_`)与 **setter**(`xxx_set_yyy`,实跑误判出 `net_set_errno`
八处)——同 `err-type` 的原则:**规则按真实反例收窄,不拿基线洗假阳性**。已知限度:只认**单行整句**的裸调用
(`foo(...);` 一行写完,行尾注释允许),跨行写的调用暂测不到 |
| **范围** | 只扫 `git ls-files` 里 `Drivers/BSP/**/*.h`(对外 API 面);**lftpd 按第三方排除**——
它的 `226/425/450/550` 是 FTP 协议码不是内部错误码;**`net_*` BSD 兼容层不纳入**(用户裁定
2026-10-23):调用方要的就是"返回字节数 + 每任务 errno 槽"那个形状,模拟器的 Winsock
后端也照同一形状实现 |
| **增量拦截** | 存量 **38 条**钉 `BASELINE`(首清点 19,已还 18(首清点 19 条:2 个 `*_BUSY_TIMEOUT = -2` + 16 个缺「返回值:」+ 1 个 bool;**现仅剩
`bool ui_content_page_open()` 一条**——它需要改签名并处理 3 个调用点,与"还注释"不是一档事)。**`consume-result` 是同日新加的一条,上线即暴露 37 条存量丢弃**(`gd5f2gq5ue.c` 29 处:14 处
丢弃 `HAL_SPI_*` 状态 + 15 处丢弃自家原语;`NET/net_socket.c` 8、`UI/ui_net_sink.c` 4 处 `net_close`、
`RS485/rs485.c` 2、`nand_ftl.c` 1)——这些是**覆盖面扩大而新暴露的历史债**,按增量拦截的老规矩入基线
(不算"新增违规"),修一条删一行。**只减不增**、脚本没有 `--write`;修完跑 `--prune` 会点名"这条已消失,
请从基线删除" |
| **配置** | `files: ^Drivers/BSP/\|^scripts/check_bsp_err\.py$\|^\.pre-commit-config\.yaml$`、
`pass_filenames: false`(恒扫全库,要按 git 清单建公开头索引) |
| **为什么 `consume-result` 只能自己做**(两条实测负结果) | ① `__attribute__((warn_unused_result))` **在 ARMCC v5 上
不生效**:属性挂上、调用点裸调不接返回值,构建仍 0 错 0 警告。② **cppcheck 也照不出来**:按本仓自己的
`--enable=warning,style,performance,portability` 跑 `gd5f2gq5ue.c`,对 14 处丢弃 `HAL_SPI_*` 返回值、以及
"未初始化局部量直接进 `P_FAIL` 判定"都**一条不报**(它假定 `gd5f_read_status(&status)` 会写出参,而失败
路径根本不写)。**所以"丢弃底层传输的返回值"今天没有任何工具会响**,只能靠本脚本 + 书写约定"故意丢弃
必须写 `(void)`";评审时看到既没 `(void)` 也没接住的调用就是漏判嫌疑 |
| **踩坑提醒** | pre-commit 会 **stash 未暂存改动**再跑钩子。所以"改代码 + 同时更新基线"要在
一次提交里**把改动 `git add` 了再跑**,否则钩子看到的是没有你改动的树,会假报警 |
| **检查项叫什么** | 四项用自描述标签(就是基线键的第二字段,格式 `文件|标签|名字`):
`err-type` 错误通道不外泄、`timeout-name` 超时宏不取负值、`return-doc` 公开函数必须写返回值、
`consume-result` 返回值不得裸丢弃。**2026-10-23 之前这四项叫 R1/R2/R3/R4,已整体改名**——
`R1~R8` 在本仓是"专项风险"的裸形编号(`scripts/id_registry.json` 已登记该族,保护规划与看门狗
的风险表都在用)、**带连字符的那一族是审查报告的可靠性 / 内存优化项编号**(见同一裸形四种意思;而编号门禁因为 `docs/编号规范.md` §4.3 已把这些裸形声明为
"同号不同义"、走 `collides_with` 白名单,**所以这种撞法不会报**(§1 的"连字符承载语义"
一行正是这么写的),等于机器放行、人看不懂(对照:`R1`=err-type、`R2`=timeout-name、
| `R3`=return-doc、`R4`=consume-result;已提交的变更记录里出现的老名按冻结历史不回改)。 |
| **运行器** | `scripts/check_bsp_err.py`(纯标准库;`--verbose` 列全部命中归类、`--prune` 只报
可删基线、`--emit-baseline` 建基线用;退出码 0/1/2) |
| **两个阶段** | pre-commit 阶段 38 个钩子 + commit-msg 阶段 `gitlint`;`default_install_hook_types: [pre-commit, commit-msg]` 是显式声明的——换机后 `pre-commit install` 才会带上 commit-msg 钩子 |
| **stash 陷阱** | pre-commit 跑钩子前会 stash 未暂存改动,钩子看到的是"没有你中间状态"的树。**改代码 + 同时更新基线/受管区的,必须先 `git add` 再跑钩子**,否则会拿到假报警(本仓踩过,`docs/门禁说明/bsp-err.md` §4 记了现场) |
| **`always: true` 已废** | pre-commit 4.x 对 `local` 钩子里这个键**只告警 `Unexpected key(s)` 然后忽略** → "想总是跑"只能把 `files:` 写全(教训 → §5.5) |
| **改写型与只读型的豁免不对称** | YAML 锚点 `&cubemx-generated` 只豁免 `trailing-whitespace` 与 `end-of-file-fixer`;`cppcheck` / `no-mojibake` / `cubemx-survival` / `task-table` / `mem-numbers` **仍覆盖** `Src/`、`Inc/` |
| **增量拦截三段式** | 规则恒生效 + 存量钉在基线/台账 + `--prune` 点名"已还清、请删行";**脚本刻意没有 `--write`**(不提供"一条命令把现状洗成合法"的入口)。使用者:`layer-deps`(6)、`bsp-err`(38)、`id-check`(台账 640 处,随 `--prune-backlog` 变动) |
| **没有 `.map` 即跳过** | `mem-numbers` / `mem-budget` 在无 Keil 构建的机器(含远端 CI)上是**跳过**,不是"通过"——别把跳过读成有余量 |
| **配置自己坏 = 全部静默不跑** | 改完 `.pre-commit-config.yaml` 的验收不是过 `check-yaml`,而是实跑 `-m pre_commit run --all-files`,确认目标钩子**出现在输出里**且状态不是 `Skipped` |
| **范围策略** | 第三方与生成物只靠 `.gitignore` 与顶层 `exclude`(不在各钩子里重复写目录名);权威口径 → `AGENTS.md` 提交门禁一节 + §5.5 |
| **新增一个门禁的三步** | ① 配置落地并**做一次注入自证**(故意违反一次,看它是否当场 FAIL);② 在 `docs/门禁说明/` 落一篇,**"限制与盲区"一节不许留空**;③ 在 `docs/门禁说明/README.md` 索引加一行 |
### 2.3 快速自查入口
- 提交前等价自查(L0 + L0b + L0.5):`scripts/ci_local.py`(用 `.venv` 解释器跑)。
- 单独跑某一条:`-m pre_commit run <hook-id> --files <改动的文件>`;全库:`--all-files`。
- 临时跳过只在紧急情况,且必须在提交说明里写明跳过了什么、为什么、随后怎么补(`--no-verify` 会跳过全部 39 条,包括 `gitlint`)。
---
@@ -654,7 +221,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
**纳入后的实测收益(当天)**:`cppcheck` 立刻揪出 `sim_syslog.c` 一处**真实空指针解引用**(`if (p_epoch == NULL || …) { *p_epoch = 0u; … }`——判空分支里紧接着解引用),另有 2 处 `localtime_s` 未初始化告警、1 处 `px` 可加 `const`;`coding-standard` 报出 61 处(44 处函数左大括号 Allman 风格、8 处缺函数注释、3 处缺文件头注释、2 处 `__` 前缀保护宏、2 处 `sprintf`、2 处 CODE-RULE-11 误报),已全部处理。
**顺带修掉的规则缺陷**:CODE-RULE-11 原实现把**任何含 `/` 的 include** 判违规,误伤 `<sys/stat.h>` 这类系统头;已按 include 形态细化,判定见 §2.8。
**顺带修掉的规则缺陷**:CODE-RULE-11 原实现把**任何含 `/` 的 include** 判违规,误伤 `<sys/stat.h>` 这类系统头;已按 include 形态细化,判定见 `docs/门禁说明/coding-standard.md` §1。
**教训**:`exclude` 只写**真正不该进钩子的路径**。已被 `.gitignore` 覆盖的生成物不需要再挡一遍——写上去只会连带豁免同一目录里的自研代码。
@@ -664,7 +231,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
| 文件 | 位置 | 说明 |
|---|---|---|
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**39 个钩子**,2026-10-23 清点;本文 §2 详解非单测类——N-19 新增的两个单测钩子见 §2.16/§2.17,N-23 双端注册反向校验(含 U4 补强的 UI 页面描述符表接线 ④)见 §2.18,N-27 RAM 预算门禁见 §2.19,B4 IEC104 协议栈单测见 §2.20,B8 频率测量单测见 §2.21,B9 SOE 动作记录单测见 §2.22,N-21 dhara NAND 移植层单测见 §2.23,U4 残余字符集差集检查见 §2.24,编号门禁八项校验见 §2.25,分层方向门禁见 §2.26,BSP 错误码与超时命名约定门禁见 §2.27,其余主机侧单测见 AGENTS.md 门禁条;主机侧单测共 16 个;UI 门禁另有 `font-charset` 与 `ui-golden`) |
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**39 个钩子**,2026-10-23 清点)。逐项详解在 `docs/门禁说明/`(索引 `docs/门禁说明/README.md`;主机侧单测 16 个合一篇,含各套件来历;UI 侧另有 `font-charset` 与 `ui-golden` 两篇——后者此前**在旧手册里连小节都没有**,只活在配置注释里,本次补齐) |
| `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) |
| `.cubemx_survival_baseline.json` | 仓库根 | CubeMX 易失定制基线(哈希;`--write` 重建) |
| `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 |
@@ -215,7 +215,7 @@ C2 dhara 失败类 +84;RO 另 +208,全是 C2 的失败类名表)→ `ER_IR
> 对象级对照(`.map` 组件表,Code+RO+RW):`blend_to_*` 40,582 → 1,338(其中 `blend_to_i1` 自身 5,272 → 1,338——它按**源**格式的分支也一并消失);控件类 13,397 → 0;`lv_theme_simple` 866 → 0;`lv_draw_sw_transform` 4,340 → 598;`lv_theme_mono` 1,400 → 1,036;镜像内对象数 227 → 212。实测比原报告估算(54,630 B)多省约 10 KB,差额正是这些"锚点消失后连带消失的分支"。
> ⚠ **配套本地补丁**:`lv_draw_sw_blend_to_i1.c` 的 `lv_color16_from_u16` 上游 9.3.0 漏了定义处守卫(唯一使用点全在 `#if LV_DRAW_SW_SUPPORT_RGB565_SWAPPED` 内)→ 关掉该格式后 ARMCC 报 `#177-D declared but never referenced`(破坏"0 警告"门禁)。已按使用点补同一守卫,升级 LVGL 时若上游已修可去掉。
> 验证:ui-verify 五步全过——**11 页内容黄金参考在裁剪前生成、裁剪后逐字节一致**,菜单 8 状态像素一致,LVGL 池水位 5,440 → 5,368 B(−72 B,样式表更小,一次性残留仍 80 B);`@build` 0 错 0 警;`@test` 真机 PASS。详见 `../UI设计/UI_设计架构.md` §4.1.4 第 10 条。
> **长效防复发**:已建门禁 `scripts/check_lv_conf_sync.py`(pre-commit 钩子 `lv-conf-sync`)——比对两份 `lv_conf.h` 的**生效值**(未定义则取 `lv_conf_internal.h` 兜底默认),并抓无效宏名与重复定义;首次跑揪出两端共 26 个 9.3 不存在的选项、3 处重复定义、10 项靠默认值隐式生效的开关,已清理(本次清理对镜像体积中性)。详解见 `../代码规范/pre-commit使用说明.md` §2.15。
> **长效防复发**:已建门禁 `scripts/check_lv_conf_sync.py`(pre-commit 钩子 `lv-conf-sync`)——比对两份 `lv_conf.h` 的**生效值**(未定义则取 `lv_conf_internal.h` 兜底默认),并抓无效宏名与重复定义;首次跑揪出两端共 26 个 9.3 不存在的选项、3 处重复定义、10 项靠默认值隐式生效的开关,已清理(本次清理对镜像体积中性)。详解见 `docs/门禁说明/lv-conf-sync.md`(含四类判定、端专属白名单与首次落地实测)。
![Flash 布局](../images/内存布局_FLASH.svg)
@@ -382,7 +382,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
- `--update` 有**误刷保护**(先跑文本层断言,失败即中止)——**未有意改视觉而 FAIL = bug,修代码,禁止刷参考掩盖**;
- 刷新参考后必须重跑 `regen_ref_png.py` 重生成目检 PNG(否则旧 PNG 看似乱码);
- 门禁常量漂移:先确认新值合理(残留分解见 `菜单页面设计.md` §9.5)→ 更新常量 + 在 `UI_自动化测试流程.md` 变更记录登记;
- **UI 新增中文文案提交前必须先跑 font-regen**(extract_cn → make_font → diff 检查):字符集 `ui_cn_chars.txt` 缺字则上机显示空白——**2026-10-15 起该约定已入门禁**(`font-charset` 钩子:源码在用字符 ∉ txt 或 txt ∉ 字体 cmap → 提交 FAIL,详见 `../代码规范/pre-commit使用说明.md` §2.24)。
- **UI 新增中文文案提交前必须先跑 font-regen**(extract_cn → make_font → diff 检查):字符集 `ui_cn_chars.txt` 缺字则上机显示空白——**2026-10-15 起该约定已入门禁**(`font-charset` 钩子:源码在用字符 ∉ txt 或 txt ∉ 字体 cmap → 提交 FAIL,详见 `docs/门禁说明/font-charset.md`)。
---
+1 -1
View File
@@ -3,7 +3,7 @@
> **适用对象**:本仓库全部 Markdown 文档、C/Python/脚本源码注释、提交信息中出现的**实体编号**(`E-44`、`N-21`、`TC-NET-601`、`FR-STO-1`、`Trap 07` 这类"字母 + 数字"的条目身份标识)。
> **不适用**:C 语言标识符(`g_`/`s_`/`PS_*`/`TEST_SUITE_*`)、元器件位号(`U1`/`C12`/`R3`)、章节号(`§3.2`,归 [`docs/文档规范.md`](文档规范.md) §3.5 管)——见下文「划界」一节。
> **强制程度**:§2~§5 为**强制**(由 `scripts/check_ids.py` 机械校验,§8 已落地 warn-only);§6~§10 为**约定 + 迁移计划**。
> **相关文档**:[`docs/文档规范.md`](文档规范.md)(链接/章节/门禁总规范)、[`docs/任务规划/README.md`](任务规划/README.md) §0(本规范的登记簿摘要)、[`docs/测试说明/测试文档模板规范.md`](测试说明/测试文档模板规范.md) §6(测试侧编号细则)、[`docs/代码规范/pre-commit使用说明.md`](代码规范/pre-commit使用说明.md) §2.25(`id-check` 钩子)。
> **相关文档**:[`docs/文档规范.md`](文档规范.md)(链接/章节/门禁总规范)、[`docs/任务规划/README.md`](任务规划/README.md) §0(本规范的登记簿摘要)、[`docs/测试说明/测试文档模板规范.md`](测试说明/测试文档模板规范.md) §6(测试侧编号细则)、[`docs/门禁说明/id-check.md`](门禁说明/id-check.md)(`id-check` 钩子逐项详解,含限制与盲区)。
> **状态**:**§8 门禁已落地**(warn-only,含 `scripts/id_registry.json` / `scripts/id_aliases.json`,实测基线见 §8.1);§4 的目标前缀与 §7 的迁移映射在阶段 1 落地前**尚未生效**,落地进度见 §9。
---
+9 -4
View File
@@ -6,7 +6,8 @@
> 一份只看"拦了什么"的门禁文档,会让人以为绿就是对的。
- 配置事实源:`.pre-commit-config.yaml`(39 个 `- id:`,2026-10-23 清点)。
- 通用机制与整体口径:`docs/代码规范/pre-commit使用说明.md`(stages、范围排除、stash 陷阱等)。
- 通用机制(两阶段、stash 陷阱、`always` 已废、增量拦截三段式、旧 §2.x 查号表):
`docs/代码规范/pre-commit使用说明.md` §2。
- 快速自查(不等提交):`& '.venv\Scripts\python.exe' scripts/ci_local.py`(L0 + L0b + L0.5)。
## 1. 三种拦截姿态(先分清这个,报错时才知道该干什么)
@@ -64,6 +65,10 @@
- **本目录 23 篇已全部成文**(上表「文档」列 = 覆盖的 hook id,全部可达)。
- 新增/改动一个门禁时的三步:① 在 `.pre-commit-config.yaml` 落地;② 在本目录**照八段结构
新增或更新一篇**("限制与盲区"一节必须写真实判据的限度,不许留空);③ 在本 README 索引加一行。
- **下一步(尚未做)**:把 `docs/代码规范/pre-commit使用说明.md` 的 §2「各钩子规则详解」收敛为
"通用机制 + 总览 + 指向本目录",同一事实只留一处(`docs/文档规范.md` 的要求)。收敛时要
按**内容**重指全库对 §2.x 的既有引用——只核编号存在与否不够(同号小节会静默放过)。
- **收敛已完成(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"。历史变更记录里的旧编号按"冻结历史不回改"保留,靠查号表读。
@@ -8,6 +8,9 @@
- **新增**(不是修改)文件的体积;超阈值即 FAIL。已入库大文件被**修改**不报——它只挡"新拉进来一个东西"。
- **阈值的正确口径(本次核对更正)**:官方钩子的参数是 `--maxkb`,**默认 500**(即 500 KB)。
原手册这里写的是"单个文件 > 1MB",**是错的**(已按本机安装的钩子源码更正)。
## 2. 为什么要它
- 本仓天然产出巨大的文件:`MDK-ARM/**/*.map`、`.htm`、`.hex`、`build_log.txt`、串口与自检日志、
+9
View File
@@ -73,6 +73,15 @@ Drivers/BSP/X/x.c:42 [CODE-RULE-09] 函数 foo_bar 缺少函数注释(应含
6. 逐文件 + 正则形状:把违规写进字符串字面量、注释、宏展开里就看不见;反过来说,宏里真正的
违规代码也可能看不见。
补录(判据细节,迁移自原手册):
- 报错形态:`L123 [CODE-RULE-03 3.4.1] 宏名须全大写+下划线: net_htonl`(规则号与规范条款号一起给)。
- CODE-RULE-11 细化过的判定形态(2026-10-11):禁的是**用路径找文件**——引号 include 带斜杠、
任意形态带 `..` 或反斜杠、绝对路径或盘符。
- **`Drivers/BSP/GD5F2GQ5UE/nand_ftl.*` 也在排除清单里**(dhara/FatFs 接口适配层,函数名与形态由
第三方接口决定,不该按我们的命名规范判它)。
- 明确列为"不在检查范围"的语义类条款:函数长度与单一职责、参数数量、错误处理方式、被注释掉的代码。
## 7. 豁免与基线
- **没有基线**:硬拦截,新写的代码必须合规;因此**放宽任何一条都会立刻降低约束**,需谨慎。
+5
View File
@@ -36,6 +36,11 @@
- UI 生成文件(`ui_font_cn_12`、`ui_menu_tab`)**纳入受检**——生成器按工程约定产出,不是第三方。
- 只在**本机装了 cppcheck** 时有效;CI 不跑构建类门禁(见 `docs/CI/README.md`)。
- 速度口径(原手册实测):**单文件约 1~2 秒、全库约 5 秒**,所以它能逐文件传参而不拖慢提交。
- style 类别 95 项的逐项详解、配置参数与 YAML 引号陷阱 → `docs/代码规范/cppcheck使用说明.md`;
本门只管钩子这一层。
- 典型报错形态:`[path/to/file.c:123]: (style) variableScope The scope of the variable ... can be reduced`。
## 4. 报错怎么读
```
+15
View File
@@ -13,6 +13,12 @@
2. **漂移检测**:把关键位置哈希写进 `.cubemx_survival_baseline.json`,`--check` 逐项比对,
任一项变化即 FAIL。
- **清单项共 16 项**:IWDG 延后启动、崩溃采集 ×2、栈溢出检测、堆失败钩子、断言改造 ×2、看门狗模块
与工程挂载、复位原因锁存、HAL 驱动与使能宏、`.ioc` 关键项 ×3 等(清单本体在脚本里)。
- 触发面还包括 `App/util/task_watchdog.*` 与 `MDK-ARM/*.uvprojx`(后者只跑 `must_contain`,见 §3)。
- **防误报实现**:断言之前先**剥掉 C 注释**——本仓多处注释刻意引用被替换掉的历史代码(例如
"原为 `HAL_IWDG_Init(&hiwdg)` 直呼"),不过滤就会恒定误报。
## 2. 为什么要它
- 实测记录:`.ioc` 里 `ProjectManager.KeepUserCode=true` **只**保护 `USER CODE BEGIN/END` 之间;
@@ -20,6 +26,9 @@
空 `while(1)`、加固宏被整块删除——**不报错,只在真崩的时候才发现没日志**。
- AGENTS 把防丢失排成三层(L1 写进 `.ioc` → L2 搬进 `USER CODE` 区 → **L3 本门当审计兜底**)。
- 那一次被覆盖的具体内容:`HardFault_Handler` 的崩溃采集**被打回空 `while(1)`**、**两个 FreeRTOS
加固宏被整块删除**——两处都不报编译错,只在真出故障时才暴露。
## 3. 触发面 / 看不见什么
- `MDK-ARM/*.uvprojx` **不参与漂移检测**(`no_baseline=True`):该文件每次 `@build` 后都被 UV4
@@ -54,6 +63,12 @@ CubeMX 重生成存活审计.......Failed
**`--write` 必须与"确认过的改动"写在同一个提交里并说明。**
6. 它不判断定制**是否仍然正确**,只判断"还在不在原来的位置"。
- 标记区**外**的存量只给 WARN(本仓现存:`Src/stm32f4xx_it.c` 的 3 个 Fault 处理函数、
`Src/freertos.c` 的栈注释)——CubeMX 不保护它们,只能人工盯。
- 原手册在这里建议"仿照 `HardFault_Handler` 自造 `USER CODE` 标记区"把它们升级成受保护内容;
⚠ 而 `docs/子系统设计/看门狗设计说明.md` §10 记的实测教训是**自造标记区会被模板换代抹掉**。
两条不打架但优先级不同:**能落 `.ioc` 或官方标记区,就别自造标记**。
## 7. 豁免与基线
- 基线 = `.cubemx_survival_baseline.json`,语义与 `bsp-err`/`layer-deps` 的"只减不增"**相反**:
+8
View File
@@ -68,6 +68,14 @@ D 类刻意只校验**紧邻写法**:文档名与 `§` 之间只允许空白
5. 豁免清单本身可被滥用:加个 `[示例]` 就安静了。**只有引用报错样例/日志片段才该用它。**
6. 只管 `.md`:代码注释里的文档引用不在范围内(那边靠人工与 `bsp-err` 之类的文档检查间接覆盖)。
补录(豁免与排除清单,迁移自原手册):
- 排除目录:`.opencode/node_modules/`、`Lib/lvgl/`、`MDK-ARM/RTE/`、`docs/芯片手册/`;
`simulator/` 已于 2026-10-11 **从排除列表移除**(其下三个 README 是本项目文档)。
- 状态词豁免的判定距离是**引用附近 ±40 字符**(未创建 / 尚未创建 / 已删除 / 待排期)。
- 规范依据 → `docs/文档规范.md` §3(链接规范)与 §4(门禁)。
- 全库自检:`python scripts/check_doc_links.py --all`(不带 `--sections-strict` 时只查路径与命名)。
## 7. 豁免与基线
- 没有"基线"概念:硬拦截,新写的引用必须合法。
+4
View File
@@ -40,6 +40,10 @@ Files were modified by this hook.
只覆盖 C/H——文档侧的字节卫生目前靠人工(本目录作者每次交付前逐文件核 BOM/CR/`EF BF BD`);
3. 只处理本次涉及的文件,历史文件不会被批量整理(避免一次性巨大 diff)。
- 迁移时发现的一条**已更正的旧错误**值得记在这里:原手册把 `check-added-large-files` 的阈值写成 1 MB,
实际默认是 500 KB(`--maxkb` 默认 500)→ 见 `docs/门禁说明/check-added-large-files.md` §1。
提醒:**本目录任何"默认值"都应以安装的钩子源码或实测为准**,不要抄旧文档。
## 7. 豁免与基线 / 8. 改这个门禁本身
- 无基线。若真有"某类文件必须无末尾换行"的需求,加豁免并写明原因,别用 `--no-verify` 绕过。
+10
View File
@@ -17,11 +17,21 @@
A 通过 + B 通过 ⟹ 字体覆盖"源码在用"与"刻意保留"的每一个字。
`KEEP_EXTRA` 的存在理由:防止将来 `extract_cn.py` 按"源码已不用"静默删掉**已落盘记录里还在用**的字。
- **单一规则源**:本脚本不自创第二套"从源码里找汉字"的规则,而是复用 `tools/font_gen/extract_cn.py`
的 `SCAN` 清单与 `collect()` 状态机(跳过注释与字符常量、只取双引号字符串)。**两处各写一套解析,
就会出现"门禁说够、生成器说不够"**。
- 典型报错形态:`[GAP] 字体缺字 1 个(在字符集、不在 ui_font_cn_12.c 的 cmap -> lv_font_conv
静默丢字,上机白框)`,并点名码位(含 U+ 码点)。
## 2. 为什么要它
- 生成期的 `verify_coverage()` 只校验"txt ⊆ 字体 cmap",**管不到"源码用到的字有没有进 txt"**;
改了 UI 字符串却忘跑生成链时,此前提交期没有任何门禁能拦——只能靠肉眼截图发现白框。
- **有牙实测(首次落地)**:注入都打在**临时副本**上(用 `--chars` / `--font` 覆盖,不碰仓库文件):
① 从字符集清单里删掉一个符号 → 第一段链条 FAIL 并点名该码位;② 从字体副本里删掉同一码位 →
第二段链条 FAIL。两段各测一次才算证明链条闭合。
## 3. 触发面
- 只跑改动的 `.c`/`.h`(`types: [c]`)。
+4
View File
@@ -42,6 +42,10 @@ gitlint..............................................................Failed
- 稳妥写法:正文落成 UTF-8 无 BOM、每行 ≤78 字符的临时文件,再 `git commit -F`;提交前自查
有没有超 80 的行(本目录作者在实践中就是先做这一步再提交)。
- 典型报错(本仓最常撞的两条):标题超长(例:159 > 100)、正文单行超长(例:87 > 80)。
- 口径是**宽松**:不强制 Conventional Commits、兼容中文标题;配置源是仓库根 `.gitlint`,
**不是** hook args(改规则去改那个文件,不是在 yaml 里加 args)。
## 5. 好处
- `git log` 在任何终端宽度都可读;长说明(含实测数字与踩坑)能完整留在历史里,这是本仓历史
+15
View File
@@ -30,6 +30,21 @@
共同机制:`run_unit_test.py` 在宿主编译器上编译被测 `.c` + 该目录的 `stub/` 替身,跑断言并汇总
用例数(各模块的 `TC-HOST-<模块码>` 号段见 `docs/编号规范.md`)。
各套件的来历与牙齿(迁移自原手册;2026-10-11 至 10-15 分批挂闸):
| 套件(登记出处) | 规模 | 为什么非要有 |
|---|---|---|
| `sha256`(N-19) | 13 例 | FIPS 180-4 标准向量(空串 / `abc` / 56 B 补位跨界 / 1 MB)+ `hex_eq` 非法输入。哈希算错是"正确口令被拒 / 错误口令放行"的静默语义错误,板端只见 `530`/`230` 看不出根因 |
| `ftp-auth`(N-19) | 36 例 | 连续 5 败锁定 / 60 s 解锁 / 成功清零 / tick 32 位回绕。板端每验一条要真等 60 s、回绕要跑满 49.7 天;纯函数(tick 由参数注入)就是为了让这些分支能毫秒级穷举 |
| `iec104`(B4) | 82 例 | CP56 往返 / APCI 半帧粘帧坏帧 / STARTDT-STOPDT / 总召分片 / COV 死区 / k=12 窗满 / w=8 触发 S / 三个超时定时器 / N(R) 倒退。板端复现要真主站 + 真等待(t1=15 s、窗满要发满 12 帧) |
| `freq`(B8) | 46 断言 | 扫频 45~55 Hz 误差 ≤0.02 Hz(实测 0.0101)/ 1% 噪声 ≤0.10 Hz / 启动收敛 ≤40 ms / 阶跃 ≤120 ms。板端无标准源照不出来;定长窗的负序泄漏纹波(0.317 Hz)只有合成扫频能暴露 |
| `soe`(B9) | 32 断言 | 64 B 定长记录编解码往返 / 截断与坏校验和拒绝 / IV 位 / seq 连续性 / 类型枚举。记录格式是**磁盘上的字节级契约**,错了要 FTP 下载再解码才看得见 |
| `nand-port`(N-21) | 149 断言 | 整页扫描的空闲判定(页首 64 B 全 0xFF 而第 1000 字节有数据等形态)+ **与旧算法对照**(旧实现只看页首,同一页答"空闲")。这类缺陷真机极难复现,发作后只表现为"某次复位后丢数据" |
- 运行器统一用 MSVC `cl.exe`(远端 CI 的 L0.5 层);公共骨架是
`test/host/common/unit_test_harness.py` —— **改动这个骨架会同时触发 16 个钩子**
(每个钩子的 `files:` 都把它写进触发面,这是有意的)。
## 2. 为什么要它
- 状态机、编解码、换算这类逻辑,在真机上验证成本极高(要么构造故障、要么等复现);
+9
View File
@@ -78,6 +78,15 @@
7. 校验 8 只许涨不许跌:好处是防退化,代价是**顺手写一个未登记族的号就会红**,哪怕与本次改动无关。
8. 它不懂"这个号该不该存在"。新增一族编号是否合理,仍是 `docs/编号规范.md` 的人工判断。
- **裸形多义是显式承认的盲区**(`docs/编号规范.md` §1 与 §4.4):同一个裸形字母在本仓可能同时是风险号、
档位、层号、元器件位号。所以裸形族**只在属主登记处文档内**判"越游标",文档外降级处理。本目录
`bsp-err.md` §6 记的那件事正是这个洞的产物。
- 本钩子**顺带收编** `scripts/check_test_traceability.py`(TC 用例双向追溯),它不是独立钩子;
改 TC 相关正则时两个脚本要一起看(历史上批量改名后就出现过正则上界没跟上、整批号不再被匹配)。
- **有牙实测的做法**:把假编号注入**临时档**(`git add -N` 进索引,跑完 `git rm --cached` 并删除,
保证 `git status` 干净),看它是否被点名。
- `--update-baseline` **只许涨**:可判定比例下跌时拒绝写入——防止用抬基线掩盖退化。
## 7. 豁免与台账口径
- 存量:`scripts/id_known_backlog.json`(指纹级);**只减不增**,收紧用 `--prune-backlog`。
+13
View File
@@ -32,6 +32,9 @@
- **口径漂移是可测量的证据**:2026-09 审查报告记"NETCFG 一处",2026-10-22 实测已长到
**8 个文件 9 处**。边界的静默腐蚀只能靠门禁止住——先钉基线,再只拦新增。
- **实测自证(落地当天)**:临时给 `Drivers/BSP/RS485/rs485.c` 加一行 `#include "cfg_file.h"` →
钩子当场 FAIL 并点名该文件与该头(退出码 1),删掉即恢复。**挂任何门禁前都该做一次这种注入自证**。
## 3. 触发面
- `pass_filenames: false`:**每次都全库扫**(反向依赖是全局事实,只看暂存文件会漏)。
@@ -48,6 +51,16 @@
- 退出码 0 通过 / 1 有新增 / 2 用法或环境错误。
- ⚠ pre-commit 会 stash 未暂存改动:改完要**先 `git add` 再跑钩子**,否则看到的是旧树。
报错时给的是三条修法(按优先级,脚本失败信息里也写着):
1. 把上层能力**注入**给 BSP(函数指针 / 回调)——参照既有先例 `net_cfg_set_save_req_cb`、
`net_cfg_request_apply`;
2. 由上层把数据**递进来**(UI 页面要读记录,就让上层读好再把缓冲传进来);
3. 确认它其实是横切叶子能力 → 才谈 `ALLOW`(见 §7)。
⚠ 本门**不是运行时缺陷清单**:运行时属主纪律本来就没破(UI 只投递 `net_cfg_save_request()`,
真正写盘在 defaultTask;模拟器也早在主机侧跑整套逻辑)。它拦的是**静态 include 方向**这件事本身。
## 5. 好处
- 把"分层"从 PPT 变成一条命令的事实;债务总量随时可查(`--verbose` 就是清单)。
+12
View File
@@ -25,11 +25,20 @@
**实测 64,576 B 死代码(旧镜像的 23.5%)**。
- 更糟的一面:`ui-golden` 跑的是模拟器那份配置 → 特性子集不一致时,**回归验证不到固件真实行为**。
- **首次落地实测(2026-10-11)**:两端共清掉 **26 个 LVGL 9.3 根本不存在的选项**(LVGL 8 / 9.4 模板
残留)与 3 处同文件重复定义,并把 10 项 MCU 端靠默认值隐式生效的开关**显式写成 0**(含默认值为 1 的那批)。
- `lv_conf_internal.h` 里每个选项外面还套一层 `#ifdef LV_KCONFIG_PRESENT` —— 取默认值必须穿过这层,
否则整表都会被算成"未定义"。
## 3. 触发面
- `files:` 覆盖两份 `lv_conf.h` 与本脚本;`--mcu/--sim/--internal/--lvgl-src` 可换输入(测脚本自己用)。
- `--list` 打印全量对照表,是回答"到底哪些宏在两边不一样"的唯一可靠入口。
- 触发面覆盖**两端任一**改动(`Lib/lv_conf.h`、`simulator/lv_conf.h` 或脚本自身)。⚠ 早期版本只能由
MCU 端触发,因为顶层 `exclude` 把 `simulator/` 整目录挡掉了 —— 这正是"pre-commit 4.x 没有
`always: true`,触发面必须写全"的典型案例(教训登记在 `docs/代码规范/pre-commit使用说明.md` §5.5)。
## 4. 报错怎么读
```
@@ -61,6 +70,9 @@
- 无基线(硬拦截)。两边确实要不同时,**唯一正确做法是先问"为什么模拟器要偏离固件"**——
答案通常是"模拟器有桩/无硬件",那属于平台后端差异,不该藏在 `lv_conf.h` 里。
- 另有**端专属白名单 `ALLOW_DIFF`**:SDL2 驱动、演示工程、include 替换、调试辅助(`LV_USE_OBJ_NAME`
等)、断言(固件关 / 模拟器开)、`LV_USE_SNAPSHOT`(模拟器截图回归要用)。登记必须写理由。
## 8. 改这个门禁本身
- 扩充受检宏清单要同步 `--list` 输出;新增 INVALID 词表时要跟着 LVGL 版本走,否则会把合法宏
+6
View File
@@ -48,6 +48,12 @@ RAM 预算门禁 FAIL:RW_CCM 占用 92.3% > 92%(阈值 92%)
`--trend -n 12` 看趋势。**`--gate` 不修改任何东西**,要同步台账用 `--write`。
- 报 FAIL 的正确动作是**回去减**(缩缓冲/挪区段/减日志调用点),不是抬阈值。
- 两类**软提醒不拦提交**:`tsv` 末行落后于 `.map`(忘了跑 `--update`)、`.map` 早于最新源码(忘了 `@build`)。
- 退出码:0 = 通过或跳过 / 1 = 超阈值或台账漂移 / 2 = **解析失败**(`.map` 读不出结构,不等于"没问题")。
- `.map` 解析口径复用 `scripts/ram_report.py`,两个脚本不会各说各话。
- 为什么需要(背景):2026-09 体系评审提出的"超阈值即失败 + Top-10 附表"此前**没有任何机械检查**
——`mem-numbers` 只核数字对不对,不判是否超限。
## 5. 好处
- 总量红线机械化,红线以下谁都能放心加东西;
+5
View File
@@ -37,6 +37,11 @@ Forbid U+FFFD mojibake.......Failed
- 处置:**立刻用 Edit 工具只修坏行**;不要 `git checkout`(会丢掉本次全部未提交工作)。
- 若整文件已损坏且未提交,唯一救援是编辑器/系统的本地历史(未跟踪副本),否则只能重写该文件。
- 典型报错:`U+FFFD replacement chars found (encoding corrupted, irreversible!): path/to/file.c`。
- 修复路径:从 git 历史里找到**最后一个干净版本**,只把损坏区域搬回来(整文件 checkout 会连本次
改动一起丢掉,属 AGENTS 的 Never)。
- 修完必须回头查**写入方式**(`Get-Content` / `Set-Content` / `cmd` 重写含中文文件),否则修好还会再来。
## 5. 好处
- 一条命令、零依赖、零误报地挡住不可逆损失;成本几乎为零(17 行、不解析语义)。
+9
View File
@@ -18,6 +18,10 @@
| ③ | 注册了但文件不存在(反向) | 幽灵注册(改名/删除后残留条目) |
| ④ | `Drivers/BSP/UI/ui_page_*.c` 定义了 `ui_page_<name>_desc`,但该符号在 UI 目录内除自身 `.c/.h` 外无人引用 | 页面没接进 `ui_content_page.c` 的 `s_defs[]` 描述符表:两端**编译链接全过**(无人引用的 desc 被链接器整段剥离),菜单永远打不开这一页 |
- **UI 页面的"注册"有两层**,第 ④ 类只管第二层:第一层是**构建注册**(uvprojx + CMakeLists,
即 ①② 类);第二层是**框架注册**(`ui_content_page.h` 的枚举 + `ui_content_page.c` 的 `s_defs[]`
描述符表)。只做第一层 → 编译链接全过,但菜单里就是没有这一页。
## 2. 为什么要它
- 真事故:`App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c` 历史上**两处都没注册**,
@@ -64,6 +68,11 @@
- 无"只减不增"的基线,而是**逐条带理由的已知缺口清单**(`KNOWN_GAPS` 形态):新增条目必须写
理由,性质上比基线更容易被滥用,改动时要看得到理由文本。
- 豁免用**显式清单 + 每行理由**(命名模式 `*_mcu.c` 加个别点名),**不整目录一刀切**——一刀切等于
把该目录所有漏注册都免了。
- 存量缺口走白名单只打 `[WARN]` 不拦:`KNOWN_GAPS` 里登记着 `App/task/rs485_task.c`、
`Drivers/BSP/NET/lftpd/lftpd_log.c`(Lead 处置中)。
## 8. 改这个门禁本身
- 加新的注册面(例如新增一套第三方构建脚本)要同步"正向 + 反向"两向检查,只查单向会漏幽灵注册;
+5
View File
@@ -38,6 +38,11 @@ FreeRTOS 任务表一致性.......Failed
- 同步:`& '.venv\Scripts\python.exe' scripts/gen_task_table.py --write`(只改受管区,两处文档一起改);
只看期望值:`--diff`。
- 典型报错:`[FAIL] 以下文档的任务表与 Src/freertos.c 不一致:README.md`。
- **改栈只有一条路**:先改 `Src/freertos.c` → 跑 `--write` 同步两处文档 → 提交。钩子报不一致时
**不要手改表格**——手改会被下一次 `--write` 覆盖,还掩盖了"源码其实没改"这个真问题。
- 脚本按文件原有换行符与 BOM 状态回写,不会造成整文件行尾漂移。
## 5. 好处
- 抬栈 / 加任务时,表格跟着源码走,评审看 diff 就知道动了哪个任务的栈;
+7
View File
@@ -51,6 +51,13 @@
4. `ENABLE_DESTRUCTIVE_TESTS` 的开/关**不由本门判定**(那是破坏性代价,属 Ask-first 事项,
见 `docs/测试说明/存储维护与容量恢复.md`)。
- **判定实现**:先剥掉块注释与行注释再匹配 `#define`,所以注释掉的开关不会误判;同文件派生出的
`TEST_SUITE_SELECTED` / `TEST_SUITE_NAME` 由白名单排除(它们是开关的推导结果,不是可选项)。
**新增派生宏要同时进白名单**,否则会被当成"启用了一个套件"(假阳性)。
- 当时论证"此前没有任何门禁"的方式很朴素:`grep TEST_SUITE scripts/*.py` **零命中**。
- 与编译期 `#error` 互斥检查是两道闸:`#error` 只在真构建时暴露,测试固件照样可能被提交,
所以提交期再拦一道。
## 7. 豁免与基线
- 无基线。唯一的"放行"是显式声明本次就是测试固件;**不要用 `--no-verify` 绕过**(绕过之后
+6
View File
@@ -24,6 +24,12 @@
- ⚠ **只读检查不受这条豁免影响**:`cppcheck`、乱码审计、存活审计、任务表、内存数字仍覆盖
`Src/`、`Inc/`(`cubemx-survival` 必须看到这些文件)。
- 豁免清单的精确条目(2026-10-09 定,两个改写型钩子共用同一份 YAML 锚点):`Src/*.{c,h}`、
`Inc/*.{c,h}`、`Drivers/CMSIS/`、`Drivers/STM32F4xx_HAL_Driver/`、`Middlewares/`、
`MDK-ARM/*.{uvprojx,uvoptx,sct,map}`、`.mxproject`、`*.ioc`。
- ⚠ **首次对全库跑**(`run --all-files`)会一次性批量改掉大量文件的行尾空白:行为正常、不改逻辑,
但要**单独一笔提交**,别混进功能改动里。
## 4. 报错怎么读
```