From 6822726b2c7f00ca1ee7d64c04ea816593a9b4b5 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 23 Oct 2026 11:44:21 +0800 Subject: [PATCH] =?UTF-8?q?docs(=E9=97=A8=E7=A6=81=E8=AF=B4=E6=98=8E):=20?= =?UTF-8?q?=E6=89=8B=E5=86=8C=20=C2=A72=20=E6=94=B6=E6=95=9B=E4=B8=BA?= =?UTF-8?q?=E9=80=9A=E7=94=A8=E6=9C=BA=E5=88=B6=E4=B8=8E=E6=9F=A5=E5=8F=B7?= =?UTF-8?q?=E8=A1=A8=EF=BC=8C=E9=80=90=E9=A1=B9=E8=AF=A6=E8=A7=A3=E5=BD=92?= =?UTF-8?q?=E4=B8=80=E5=88=B0=2023=20=E7=AF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户一句"现在门禁都在干什么,我一概不知"引出的收尾。原手册 §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、无行尾空白)。 --- .dsh/skills/lvgl-simulator/SKILL.md | 2 +- .pre-commit-config.yaml | 3 +- AGENTS.md | 3 +- docs/README.md | 2 +- docs/代码规范/pre-commit使用说明.md | 509 ++--------------------- docs/内存管理/RAM使用分析与优化方案.md | 2 +- docs/测试说明/测试开发标准流程.md | 2 +- docs/编号规范.md | 2 +- docs/门禁说明/README.md | 13 +- docs/门禁说明/check-added-large-files.md | 3 + docs/门禁说明/coding-standard.md | 9 + docs/门禁说明/cppcheck.md | 5 + docs/门禁说明/cubemx-survival.md | 15 + docs/门禁说明/doc-links.md | 8 + docs/门禁说明/end-of-file-fixer.md | 4 + docs/门禁说明/font-charset.md | 10 + docs/门禁说明/gitlint.md | 4 + docs/门禁说明/host-unit-tests.md | 15 + docs/门禁说明/id-check.md | 9 + docs/门禁说明/layer-deps.md | 13 + docs/门禁说明/lv-conf-sync.md | 12 + docs/门禁说明/mem-budget.md | 6 + docs/门禁说明/no-mojibake.md | 5 + docs/门禁说明/registration-check.md | 9 + docs/门禁说明/task-table.md | 5 + docs/门禁说明/test-suite-switch.md | 7 + docs/门禁说明/trailing-whitespace.md | 6 + scripts/README.md | 2 +- 28 files changed, 202 insertions(+), 483 deletions(-) diff --git a/.dsh/skills/lvgl-simulator/SKILL.md b/.dsh/skills/lvgl-simulator/SKILL.md index c169258e..3e4954f1 100644 --- a/.dsh/skills/lvgl-simulator/SKILL.md +++ b/.dsh/skills/lvgl-simulator/SKILL.md @@ -39,7 +39,7 @@ RemoDispBus 推流回归单独跑:`.venv\Scripts\python.exe simulator/tests/ch ## 两条铁律 -- **改 `simulator/lv_conf.h` 触发 `lv-conf-sync` 门禁**:它与 MCU 端 `Lib/lv_conf.h` 必须保持同一特性子集(按**生效值**比对,未定义取 `lv_conf_internal.h` 兜底);白名单与四类判定 → `docs/代码规范/pre-commit使用说明.md` §2.15 / §5.5 +- **改 `simulator/lv_conf.h` 触发 `lv-conf-sync` 门禁**:它与 MCU 端 `Lib/lv_conf.h` 必须保持同一特性子集(按**生效值**比对,未定义取 `lv_conf_internal.h` 兜底);四类判定与端专属白名单 → `docs/门禁说明/lv-conf-sync.md`;顶层 exclude 的教训 → `docs/代码规范/pre-commit使用说明.md` §5.5 - **模拟器代码与固件同标准受检**(`cppcheck` / `coding-standard` / 乱码 / 改写型钩子 / `doc-links` 全覆盖 `simulator/`);**改动触及 `sim/` 任何后端,必须同步在 `docs/测试说明/模拟器差距清单.md` 登记一行**——模拟器跑过 ≠ 真机跑过 ## 构建/运行报错速查(详解 `simulator/README.md`「四、已知坑(构建/运行)」) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ba8ef779..9e7d24e9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ # pre-commit 自动化代码审查配置 -# 详见 docs/代码规范/pre-commit使用说明.md +# 通用机制(阶段 / stash 陷阱 / 触发面 / 增量拦截三段式)→ docs/代码规范/pre-commit使用说明.md §2 +# 每个门禁一篇详解(含限制与盲区)→ docs/门禁说明/README.md default_install_hook_types: [pre-commit, commit-msg] # 顶层 exclude 只挡**第三方与生成物**。 diff --git a/AGENTS.md b/AGENTS.md index 29f89058..4926271e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -173,8 +173,9 @@ | ADC 采样节拍 / 通道映射 / V0.1 硬件缺陷 | `docs/子系统设计/ADC采样节拍设计说明.md`、`docs/硬件平台/ADC通道映射说明.md` | | 存储栈(FatFs / dhara / lftpd / 两条 FatFs 陷阱) | `docs/架构设计/STM32F4-Base存储架构说明.md` | | RAM / 栈预算 / 容量台账 | `docs/内存管理/RAM使用分析与优化方案.md`、`内存内容分配策略.md` | -| pre-commit 39 个钩子逐个详解 | `docs/代码规范/pre-commit使用说明.md` | +| **每个提交门禁一篇详解**(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径) | `docs/门禁说明/README.md`(39 个钩子 → 23 篇索引) | | 实体编号怎么写 / 前缀总表 / `id-check` 八项校验 | `docs/编号规范.md`(§4 总表、§8 门禁与实测基线) | +| 提交门禁通用机制(阶段 / stash 陷阱 / 触发面 / 增量拦截三段式) | `docs/代码规范/pre-commit使用说明.md` §2 | | 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` | | 自动化测试 / 实机测试 / 板级套件 | `docs/测试说明/自动化测试流程.md`、`实机测试实施指南.md`、`board-test-suite` skill | | 烧录与复位语义 | `docs/测试说明/ST-LINK_CLI_复位使用说明.md` | diff --git a/docs/README.md b/docs/README.md index 3c8c9316..f02f0ee2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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) | **每个提交门禁一篇文档**:查什么、拦下过什么真事、**限制与盲区**、豁免与基线口径 | diff --git a/docs/代码规范/pre-commit使用说明.md b/docs/代码规范/pre-commit使用说明.md index 14d8e05e..ca4048e0 100644 --- a/docs/代码规范/pre-commit使用说明.md +++ b/docs/代码规范/pre-commit使用说明.md @@ -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 带 `/`、任意形态带 `..` / `\` / 绝对路径或盘符;`` 这类**尖括号系统头允许带子目录**,由 `-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` 由脚本自动重写表格 | -| **受管区** | `` ~ `` 之间 | -| **典型报错** | `[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` 自动同步;散句陈旧数字需人工复核 | -| **受管区** | `` ~ `` | -| **典型报错** | `[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` 的 `` + `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__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 --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** 判违规,误伤 `` 这类系统头;已按 include 形态细化,判定见 §2.8。 +**顺带修掉的规则缺陷**:CODE-RULE-11 原实现把**任何含 `/` 的 include** 判违规,误伤 `` 这类系统头;已按 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 检测脚本 | diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 6e5ecc8b..327a1cd2 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -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) diff --git a/docs/测试说明/测试开发标准流程.md b/docs/测试说明/测试开发标准流程.md index fca9c53b..b6cab1f1 100644 --- a/docs/测试说明/测试开发标准流程.md +++ b/docs/测试说明/测试开发标准流程.md @@ -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`)。 --- diff --git a/docs/编号规范.md b/docs/编号规范.md index e5981c4b..d4608012 100644 --- a/docs/编号规范.md +++ b/docs/编号规范.md @@ -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。 --- diff --git a/docs/门禁说明/README.md b/docs/门禁说明/README.md index e0836c81..4f575f46 100644 --- a/docs/门禁说明/README.md +++ b/docs/门禁说明/README.md @@ -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"。历史变更记录里的旧编号按"冻结历史不回改"保留,靠查号表读。 diff --git a/docs/门禁说明/check-added-large-files.md b/docs/门禁说明/check-added-large-files.md index eebf886a..5b7324a0 100644 --- a/docs/门禁说明/check-added-large-files.md +++ b/docs/门禁说明/check-added-large-files.md @@ -8,6 +8,9 @@ - **新增**(不是修改)文件的体积;超阈值即 FAIL。已入库大文件被**修改**不报——它只挡"新拉进来一个东西"。 +- **阈值的正确口径(本次核对更正)**:官方钩子的参数是 `--maxkb`,**默认 500**(即 500 KB)。 + 原手册这里写的是"单个文件 > 1MB",**是错的**(已按本机安装的钩子源码更正)。 + ## 2. 为什么要它 - 本仓天然产出巨大的文件:`MDK-ARM/**/*.map`、`.htm`、`.hex`、`build_log.txt`、串口与自检日志、 diff --git a/docs/门禁说明/coding-standard.md b/docs/门禁说明/coding-standard.md index 68e23fc9..7f55e36a 100644 --- a/docs/门禁说明/coding-standard.md +++ b/docs/门禁说明/coding-standard.md @@ -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. 豁免与基线 - **没有基线**:硬拦截,新写的代码必须合规;因此**放宽任何一条都会立刻降低约束**,需谨慎。 diff --git a/docs/门禁说明/cppcheck.md b/docs/门禁说明/cppcheck.md index 2243a022..6fb7e141 100644 --- a/docs/门禁说明/cppcheck.md +++ b/docs/门禁说明/cppcheck.md @@ -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. 报错怎么读 ``` diff --git a/docs/门禁说明/cubemx-survival.md b/docs/门禁说明/cubemx-survival.md index de838ee5..b84cd416 100644 --- a/docs/门禁说明/cubemx-survival.md +++ b/docs/门禁说明/cubemx-survival.md @@ -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` 的"只减不增"**相反**: diff --git a/docs/门禁说明/doc-links.md b/docs/门禁说明/doc-links.md index b9c586fb..47b7f4da 100644 --- a/docs/门禁说明/doc-links.md +++ b/docs/门禁说明/doc-links.md @@ -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. 豁免与基线 - 没有"基线"概念:硬拦截,新写的引用必须合法。 diff --git a/docs/门禁说明/end-of-file-fixer.md b/docs/门禁说明/end-of-file-fixer.md index 6428e212..5d9b4f08 100644 --- a/docs/门禁说明/end-of-file-fixer.md +++ b/docs/门禁说明/end-of-file-fixer.md @@ -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` 绕过。 diff --git a/docs/门禁说明/font-charset.md b/docs/门禁说明/font-charset.md index dd1e1d76..24748992 100644 --- a/docs/门禁说明/font-charset.md +++ b/docs/门禁说明/font-charset.md @@ -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]`)。 diff --git a/docs/门禁说明/gitlint.md b/docs/门禁说明/gitlint.md index c7ef0b32..270fc71f 100644 --- a/docs/门禁说明/gitlint.md +++ b/docs/门禁说明/gitlint.md @@ -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` 在任何终端宽度都可读;长说明(含实测数字与踩坑)能完整留在历史里,这是本仓历史 diff --git a/docs/门禁说明/host-unit-tests.md b/docs/门禁说明/host-unit-tests.md index 2b039404..7cc16b80 100644 --- a/docs/门禁说明/host-unit-tests.md +++ b/docs/门禁说明/host-unit-tests.md @@ -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. 为什么要它 - 状态机、编解码、换算这类逻辑,在真机上验证成本极高(要么构造故障、要么等复现); diff --git a/docs/门禁说明/id-check.md b/docs/门禁说明/id-check.md index 913d7402..8660cee3 100644 --- a/docs/门禁说明/id-check.md +++ b/docs/门禁说明/id-check.md @@ -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`。 diff --git a/docs/门禁说明/layer-deps.md b/docs/门禁说明/layer-deps.md index ac548dbf..5e86d715 100644 --- a/docs/门禁说明/layer-deps.md +++ b/docs/门禁说明/layer-deps.md @@ -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` 就是清单)。 diff --git a/docs/门禁说明/lv-conf-sync.md b/docs/门禁说明/lv-conf-sync.md index 3349eed1..f0d94196 100644 --- a/docs/门禁说明/lv-conf-sync.md +++ b/docs/门禁说明/lv-conf-sync.md @@ -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 版本走,否则会把合法宏 diff --git a/docs/门禁说明/mem-budget.md b/docs/门禁说明/mem-budget.md index a66d5cfe..d99aeb56 100644 --- a/docs/门禁说明/mem-budget.md +++ b/docs/门禁说明/mem-budget.md @@ -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. 好处 - 总量红线机械化,红线以下谁都能放心加东西; diff --git a/docs/门禁说明/no-mojibake.md b/docs/门禁说明/no-mojibake.md index 88d05a9c..dffc34a3 100644 --- a/docs/门禁说明/no-mojibake.md +++ b/docs/门禁说明/no-mojibake.md @@ -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 行、不解析语义)。 diff --git a/docs/门禁说明/registration-check.md b/docs/门禁说明/registration-check.md index f7119170..bcec6d19 100644 --- a/docs/门禁说明/registration-check.md +++ b/docs/门禁说明/registration-check.md @@ -18,6 +18,10 @@ | ③ | 注册了但文件不存在(反向) | 幽灵注册(改名/删除后残留条目) | | ④ | `Drivers/BSP/UI/ui_page_*.c` 定义了 `ui_page__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. 改这个门禁本身 - 加新的注册面(例如新增一套第三方构建脚本)要同步"正向 + 反向"两向检查,只查单向会漏幽灵注册; diff --git a/docs/门禁说明/task-table.md b/docs/门禁说明/task-table.md index 07fdabe7..099f38ae 100644 --- a/docs/门禁说明/task-table.md +++ b/docs/门禁说明/task-table.md @@ -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 就知道动了哪个任务的栈; diff --git a/docs/门禁说明/test-suite-switch.md b/docs/门禁说明/test-suite-switch.md index 3fb178cc..bdabbeab 100644 --- a/docs/门禁说明/test-suite-switch.md +++ b/docs/门禁说明/test-suite-switch.md @@ -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` 绕过**(绕过之后 diff --git a/docs/门禁说明/trailing-whitespace.md b/docs/门禁说明/trailing-whitespace.md index 077146fa..0e9944e8 100644 --- a/docs/门禁说明/trailing-whitespace.md +++ b/docs/门禁说明/trailing-whitespace.md @@ -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. 报错怎么读 ``` diff --git a/scripts/README.md b/scripts/README.md index 1e192a31..723ac11a 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -13,7 +13,7 @@ | `check_mojibake.py` | `Forbid U+FFFD mojibake` | 扫描 `U+FFFD` 乱码(`EF BF BD` 字节) | | `check_doc_links.py` | `doc-links` | **文档链接存在性** + 命名规范(D-3/D-7)+ **`§N.M` 章节引用**(口径见 `docs/文档规范.md` §3.5)。钩子已带 `--sections-strict`(自引用失效也拦);`--section-todo` 列出机器判不了目标的 `§`(带改写建议)。每次跑打印「可判定比例」行,防覆盖率回落 | | `section_aliases.json` | (被 `check_doc_links.py` 读) | 文档**昵称别名表**:「内存方案」→ `docs/内存管理/RAM使用分析与优化方案.md`。只登记无歧义昵称;登记一次即把该昵称的引用纳入硬门禁 | -| `check_ids.py` | `id-check` | **编号门禁**:`docs/编号规范.md` §8 的八项校验(未登记前缀 / 形态 / 悬空 / 重复定义 / 越游标 / 未紧邻文档名 / 孤儿 / 可判定比例)。**warn-only**,`--strict` 才拦提交;比例跌破基线任何模式都失败。详表 → `docs/代码规范/pre-commit使用说明.md` §2.25 | +| `check_ids.py` | `id-check` | **编号门禁**:`docs/编号规范.md` §8 的八项校验(未登记前缀 / 形态 / 悬空 / 重复定义 / 越游标 / 未紧邻文档名 / 孤儿 / 可判定比例)。**已关闸为增量拦截**:钩子带 `--strict`,只拦 `scripts/id_known_backlog.json` 台账之外的新错误级问题(存量按指纹豁免、台账只减不增);可判定比例跌破基线任何模式都失败。详表(含限制与盲区)→ `docs/门禁说明/id-check.md` | | `id_registry.json` | (被 `check_ids.py` 读) | 前缀族**注册表**:属主登记处 / 形态正则 / 冻结 / 游标,逐项来自 `docs/编号规范.md` §4;`decidable_ratio_baseline` = 校验 8 的基线(只许涨)。与 `section_aliases.json` 同一套"别名表 + 可判定比例"范式 | | `id_aliases.json` | (被 `check_ids.py` 读) | 编号**别名/作废表**:旧字面量 → 现行字面量或 `status: void`(如 `N-32` 销项后仍被引用、`T-4` 随销项永久作废、模块码 `PHY→NET`) | | `check_test_traceability.py` | (被 `id-check` 顺带调用) | **TC 用例双向追溯**:文档 ↔ 代码两侧的 TC 编号集合比对;2026-10-20 起范围扩到 `test/host/` 与 7 篇「单元测试说明.md」(`--board-only` 回退旧板级口径)。此前该脚本**未挂任何钩子** |