@@ -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 里的链接写法**(` ` 这类语法示例);外部 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 检测脚本 |