From dc44c1e54e8309b3139fd5292eab3a4e15a4abb7 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Fri, 23 Oct 2026 10:54:33 +0800 Subject: [PATCH] =?UTF-8?q?docs(=E9=97=A8=E7=A6=81=E8=AF=B4=E6=98=8E):=20?= =?UTF-8?q?=E8=A1=A5=E9=BD=90=2023=20=E7=AF=87=E9=80=90=E9=A1=B9=E9=97=A8?= =?UTF-8?q?=E7=A6=81=E6=96=87=E6=A1=A3=EF=BC=8C=E6=AF=8F=E7=A7=8D=E9=97=A8?= =?UTF-8?q?=E7=A6=81=E4=B8=80=E7=AF=87=E5=90=AB=E9=99=90=E5=88=B6=E4=B8=8E?= =?UTF-8?q?=E7=9B=B2=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户诉求原话:"现在门禁都在干什么,我一概不知。" 原有文档回答"有哪些钩子、怎么配",回答不了 "这一条到底在拦什么、什么时候会骗我"。本目录按**每种门禁一篇**补齐 23 篇(39 个 hook id 全覆盖: 16 个主机单测是同机制 16 实例合一篇;cppcheck 与 test-cppcheck 是同工具两套范围合一篇), 文件名用 hook id,与提交输出里的名字一致、可 grep。 八段固定结构:查什么 / 为什么要它(拦下过什么真事)/ 触发面 / 报错怎么读 / 好处 / **限制与盲区** / 豁免与基线口径 / 改门禁本身要注意什么。最后一节是这批文档存在的理由—— 只看"拦了什么"的文档会让人以为绿就是对的;本仓三个增量拦截门禁(layer-deps 6 条、bsp-err 38 条、 id-check 台账 640 处)的绿灯含义只是"没比上次更差"。README 另立"三种拦截姿态"一节:改写型 / 硬拦截 / 增量拦截,分清才知道报错时该改代码还是该还基线。 有实证的地方全部带数字,没实证的地方直接写"预防性、未记录到实际拦截案例"(不编故事): · cppcheck:把 simulator/ 纳入受检后立刻揪出 sim_syslog.c 真实空指针;test/ 曾有 s_rx_buf 使用先于声明;**并记入两条实测负结果**——它对 14 处丢弃 HAL_SPI_* 返回值与"未初始化局部量 进 P_FAIL 判定"一条不报(这正是 consume-result 的由来)。 · lv-conf-sync:漂移导致 64,576 B 死代码入库(旧镜像的 23.5%),且 golden 跑的是模拟器那份配置。 · cubemx-survival:2026-09-29 一次重生成覆盖 4 处定制(P0×1+P1×2+P2×1),且不报编译错。 · registration-check:rs485_task.c、lftpd_log.c 两处都没注册 = 从不参与编译的死代码;第四类 "页面描述了但没接进 s_defs[] 表"编译链接全过,前三类完全照不出。 · test-suite-switch:实测已提交的 HEAD 里 TEST_SUITE_CH395F 就是开着的,.map 含 34 处 test 代码。 · task-table:2026-09 两张表都比源码落后一大截(写 2048/4096/…/16384,实际 1024…512)。 · mem-numbers:2026-09-26 复核,文档写 82,048 B / 62.6%,实际 87,040 B / 66.4%;Flash 写 249,432 / 23.8%,实际 261,932 / 25.0%。 · ui-golden:注入 .top_y +3 像素 → golden FAIL 点名"1366 px 差异"、退出码 1;并把"有基线 ≠ 有门禁"与"逼人 --no-verify 的门禁等于没有门禁"两条选型依据写进去。 · check-yaml:本仓已移除的 always 键只告警 Unexpected key(s) 然后忽略——配置"看起来生效其实没 生效"而本门全绿,所以验收方式是实跑看目标钩子是 Passed 还是 Skipped。 · no-mojibake:types: [c] 只管 C/H,**文档侧乱码无人管**——覆盖面与需求错配定性写进限制一节。 盲区写法示例(每篇都要求写真实限度,不留空):layer-deps 只查直接 include、按 basename 匹配、 不查是否真的用到;font-charset 管不到运行期拼出来的字符串与配置文件里的字;host-unit-tests 用宿主 编译器、桩是第二份真相、include_dirs 手写、没有对应钩子的模块等于零回归;mem-budget 的 92% 是管理 阈值不是安全余量、.map 缺失即"通过式跳过";registration-check 不看条件编译与注册指向是否正确; coding-standard 的 CODE-RULE-17 在无配对头时跳过、规则编号 14 作废不回收。 写文档过程自己被自家门禁拦了两轮,都按规矩改而不是绕过: ① doc-links 拦下我自己——我在解释"写 §数字 而没紧邻文档名会被判成本文自引用"这段话里,正好 就犯了这个写法(两处),另有一处示例输出未标记;改成正例写法 + [示例] 标记。 ② id-check 拦下 7 类新账:示例报错行里的号、档号引用未紧邻文档名、说明"裸形不合规"时自己写了 裸形。全部改成平实描述或补文档名、给样例加 [示例],没有一条是往台账里塞 reason 解决的。 这两次本身已写进相关篇目的"限制/变更记录",是这套文档最实在的注脚。 同步:手册 pre-commit使用说明.md 的 §2 顶部加指针(逐项详解已迁往本目录;两处收敛为"通用机制 + 总览 + 指针"排在 docs/门禁说明/README.md §4 的下一步,并注明收敛要按内容重指既有引用——只核编号 存在与否不够);README 索引 23 行状态全部置为已写、文档列改为可达链接。 验证:纯文档,不动代码与构建产物;check_doc_links --all --sections-strict **171 篇 0 问题** (新目录 24 个文件的内部链接与跨文档引用都纳入判定);id-check --strict 增量拦截通过;本次受影响 文件 pre-commit run **RC=0**;24 个文件字节卫生 0 异常(无 BOM、无 CR、无 U+FFFD、均有尾换行)。 --- docs/代码规范/pre-commit使用说明.md | 5 ++ docs/门禁说明/README.md | 57 +++++++------- docs/门禁说明/check-added-large-files.md | 52 +++++++++++++ docs/门禁说明/check-merge-conflict.md | 50 ++++++++++++ docs/门禁说明/check-yaml.md | 62 +++++++++++++++ docs/门禁说明/coding-standard.md | 91 ++++++++++++++++++++++ docs/门禁说明/cppcheck.md | 84 ++++++++++++++++++++ docs/门禁说明/cubemx-survival.md | 71 +++++++++++++++++ docs/门禁说明/doc-links.md | 85 ++++++++++++++++++++ docs/门禁说明/end-of-file-fixer.md | 51 ++++++++++++ docs/门禁说明/font-charset.md | 68 ++++++++++++++++ docs/门禁说明/gitlint.md | 67 ++++++++++++++++ docs/门禁说明/host-unit-tests.md | 91 ++++++++++++++++++++++ docs/门禁说明/id-check.md | 99 ++++++++++++++++++++++++ docs/门禁说明/layer-deps.md | 86 ++++++++++++++++++++ docs/门禁说明/lv-conf-sync.md | 73 +++++++++++++++++ docs/门禁说明/mem-budget.md | 84 ++++++++++++++++++++ docs/门禁说明/mem-numbers.md | 75 ++++++++++++++++++ docs/门禁说明/no-mojibake.md | 65 ++++++++++++++++ docs/门禁说明/registration-check.md | 77 ++++++++++++++++++ docs/门禁说明/task-table.md | 68 ++++++++++++++++ docs/门禁说明/test-suite-switch.md | 68 ++++++++++++++++ docs/门禁说明/trailing-whitespace.md | 56 ++++++++++++++ docs/门禁说明/ui-golden.md | 84 ++++++++++++++++++++ 24 files changed, 1642 insertions(+), 27 deletions(-) create mode 100644 docs/门禁说明/check-added-large-files.md create mode 100644 docs/门禁说明/check-merge-conflict.md create mode 100644 docs/门禁说明/check-yaml.md create mode 100644 docs/门禁说明/coding-standard.md create mode 100644 docs/门禁说明/cppcheck.md create mode 100644 docs/门禁说明/cubemx-survival.md create mode 100644 docs/门禁说明/doc-links.md create mode 100644 docs/门禁说明/end-of-file-fixer.md create mode 100644 docs/门禁说明/font-charset.md create mode 100644 docs/门禁说明/gitlint.md create mode 100644 docs/门禁说明/host-unit-tests.md create mode 100644 docs/门禁说明/id-check.md create mode 100644 docs/门禁说明/layer-deps.md create mode 100644 docs/门禁说明/lv-conf-sync.md create mode 100644 docs/门禁说明/mem-budget.md create mode 100644 docs/门禁说明/mem-numbers.md create mode 100644 docs/门禁说明/no-mojibake.md create mode 100644 docs/门禁说明/registration-check.md create mode 100644 docs/门禁说明/task-table.md create mode 100644 docs/门禁说明/test-suite-switch.md create mode 100644 docs/门禁说明/trailing-whitespace.md create mode 100644 docs/门禁说明/ui-golden.md diff --git a/docs/代码规范/pre-commit使用说明.md b/docs/代码规范/pre-commit使用说明.md index e859f924..14d8e05e 100644 --- a/docs/代码规范/pre-commit使用说明.md +++ b/docs/代码规范/pre-commit使用说明.md @@ -61,6 +61,11 @@ git commit ## 2. 各钩子规则详解 +> ⚠ **逐项详解已迁往 `docs/门禁说明/`(每种门禁一篇,含"限制与盲区"一节)**。本节保留作为 +> 历史详解与速查用,新增门禁请直接在 `docs/门禁说明/` 落篇;两处的收敛(同一事实只留一处) +> 排在 `docs/门禁说明/README.md` §4 的下一步里。 + + ### 2.1 trailing-whitespace — 行尾空格清理 | 项 | 说明 | diff --git a/docs/门禁说明/README.md b/docs/门禁说明/README.md index f924677c..e0836c81 100644 --- a/docs/门禁说明/README.md +++ b/docs/门禁说明/README.md @@ -22,31 +22,31 @@ ## 2. 索引(39 个 hook → 23 篇) -| 门禁文档 | 覆盖的 hook id | 一句话它在查什么 | 改了哪类文件才会跑 | 状态 | +| 门禁文档 | 覆盖的 hook id | 一句话它在查什么 | 改了哪类文件才会跑 | 文档 | |---|---|---|---|---| | [`bsp-err.md`](bsp-err.md) | `bsp-err` | BSP 的对外 API 面:错误通道类型、超时宏命名、必须写返回值、返回值不得裸丢弃 | `Drivers/BSP/**` 或门禁自身 | 已写 | -| `layer-deps.md` | `layer-deps` | 分层方向:`Drivers/BSP/**` 不得 include `App/**` | `Drivers/BSP/**` | 待写 | -| `id-check.md` | `id-check` | 实体编号的八项校验(未登记前缀、裸形、重复定义、孤儿、悬空引用…) | 几乎所有文本文件 | 待写 | -| `doc-links.md` | `doc-links` | 文档里的路径引用是否存在;`--sections-strict` 另查章节号是否还存在 | 任意 `.md` | 待写 | -| `cppcheck.md` | `cppcheck`、`test-cppcheck` | 静态分析(真机侧与 `test/` 侧两套范围与抑制项) | C/H 源文件 | 待写 | -| `coding-standard.md` | `coding-standard` | 编码规范 V1.0 的可机械检查子集(命名、头文件保护宏、模块头注释块…) | C/H 源文件 | 待写 | -| `no-mojibake.md` | `no-mojibake` | 禁 `U+FFFD`——中文被按错误编码重解码后不可逆损坏的痕迹 | 全部文本 | 待写 | -| `mem-numbers.md` | `mem-numbers` | 文档里引用的 RAM/Flash 数字与最新 `.map` 报告是否一致(体积档) | `docs/内存管理/**`、相关脚本 | 待写 | -| `mem-budget.md` | `mem-budget` | RAM 总量红线:`RW_IRAM1` / `RW_CCM` 超 92% 即拦 | 代码与配置改动 | 待写 | -| `task-table.md` | `task-table` | README/AGENTS 里的任务表是否与 `Src/freertos.c` 一致 | `README.md`、`AGENTS.md`、`Src/freertos.c` | 待写 | -| `registration-check.md` | `registration-check` | 新 `.c` 是否双端注册(Keil 工程 + 模拟器 CMake);UI 页面描述符表是否接线 | `.c`、`.uvprojx`、`CMakeLists.txt` | 待写 | -| `lv-conf-sync.md` | `lv-conf-sync` | 两份 `lv_conf.h`(MCU / 模拟器)的特性子集是否一致 | 两份 lv_conf 或脚本 | 待写 | -| `cubemx-survival.md` | `cubemx-survival` | CubeMX 重生成会不会把定制静默抹掉(L3 审计兜底) | `.ioc`、`Src/`、`Inc/` | 待写 | -| `test-suite-switch.md` | `test-suite-switch` | 板级套件开关必须互斥;全注释 = 产品构建 | `test/board/test_config.h` | 待写 | -| `font-charset.md` | `font-charset` | UI 用到的汉字是否都在子集字体里(缺字 = 白框) | `Drivers/BSP/UI/**`、字体脚本 | 待写 | -| `ui-golden.md` | `ui-golden` | UI 像素回归:菜单 8 状态 + 内容页 11 页黄金参考图比对 | UI 源码、模拟器 | 待写 | -| `host-unit-tests.md` | 16 个 `*-unit-test` | 主机侧单测(同一机制 16 个实例):逻辑层在无硬件的 PC 上编译并跑断言 | 各自被测模块与其测试目录 | 待写 | -| `trailing-whitespace.md` | `trailing-whitespace` | 行尾空白(改写型;CubeMX/Keil 生成文件豁免) | 全部 | 待写 | -| `end-of-file-fixer.md` | `end-of-file-fixer` | 文件末尾必须恰好一个换行(改写型;同豁免) | 全部 | 待写 | -| `check-added-large-files.md` | `check-added-large-files` | 别把大文件(日志/镜像/二进制)误提进仓库 | 全部 | 待写 | -| `check-merge-conflict.md` | `check-merge-conflict` | 别把没解完的冲突标记(`<<<<<<<`)提上去 | 全部 | 待写 | -| `check-yaml.md` | `check-yaml` | YAML 语法是否可解析(门禁配置自己坏了会静默不跑) | `.yml`/`.yaml` | 待写 | -| `gitlint.md` | `gitlint` | 提交信息规则:标题长度、**正文每行 ≤80**、空行要求 | 提交信息(commit-msg 阶段) | 待写 | +| [`layer-deps.md`](layer-deps.md) | `layer-deps` | 分层方向:`Drivers/BSP/**` 不得 include `App/**` | `Drivers/BSP/**` | 已写 | +| [`id-check.md`](id-check.md) | `id-check` | 实体编号的八项校验(未登记前缀、裸形、重复定义、孤儿、悬空引用…) | 几乎所有文本文件 | 已写 | +| [`doc-links.md`](doc-links.md) | `doc-links` | 文档里的路径引用是否存在;`--sections-strict` 另查章节号是否还存在 | 任意 `.md` | 已写 | +| [`cppcheck.md`](cppcheck.md) | `cppcheck`、`test-cppcheck` | 静态分析(真机侧与 `test/` 侧两套范围与抑制项) | C/H 源文件 | 已写 | +| [`coding-standard.md`](coding-standard.md) | `coding-standard` | 编码规范 V1.0 的可机械检查子集(命名、头文件保护宏、模块头注释块…) | C/H 源文件 | 已写 | +| [`no-mojibake.md`](no-mojibake.md) | `no-mojibake` | 禁 `U+FFFD`——中文被按错误编码重解码后不可逆损坏的痕迹 | 全部文本 | 已写 | +| [`mem-numbers.md`](mem-numbers.md) | `mem-numbers` | 文档里引用的 RAM/Flash 数字与最新 `.map` 报告是否一致(体积档) | `docs/内存管理/**`、相关脚本 | 已写 | +| [`mem-budget.md`](mem-budget.md) | `mem-budget` | RAM 总量红线:`RW_IRAM1` / `RW_CCM` 超 92% 即拦 | 代码与配置改动 | 已写 | +| [`task-table.md`](task-table.md) | `task-table` | README/AGENTS 里的任务表是否与 `Src/freertos.c` 一致 | `README.md`、`AGENTS.md`、`Src/freertos.c` | 已写 | +| [`registration-check.md`](registration-check.md) | `registration-check` | 新 `.c` 是否双端注册(Keil 工程 + 模拟器 CMake);UI 页面描述符表是否接线 | `.c`、`.uvprojx`、`CMakeLists.txt` | 已写 | +| [`lv-conf-sync.md`](lv-conf-sync.md) | `lv-conf-sync` | 两份 `lv_conf.h`(MCU / 模拟器)的特性子集是否一致 | 两份 lv_conf 或脚本 | 已写 | +| [`cubemx-survival.md`](cubemx-survival.md) | `cubemx-survival` | CubeMX 重生成会不会把定制静默抹掉(L3 审计兜底) | `.ioc`、`Src/`、`Inc/` | 已写 | +| [`test-suite-switch.md`](test-suite-switch.md) | `test-suite-switch` | 板级套件开关必须互斥;全注释 = 产品构建 | `test/board/test_config.h` | 已写 | +| [`font-charset.md`](font-charset.md) | `font-charset` | UI 用到的汉字是否都在子集字体里(缺字 = 白框) | `Drivers/BSP/UI/**`、字体脚本 | 已写 | +| [`ui-golden.md`](ui-golden.md) | `ui-golden` | UI 像素回归:菜单 8 状态 + 内容页 11 页黄金参考图比对 | UI 源码、模拟器 | 已写 | +| [`host-unit-tests.md`](host-unit-tests.md) | 16 个 `*-unit-test` | 主机侧单测(同一机制 16 个实例):逻辑层在无硬件的 PC 上编译并跑断言 | 各自被测模块与其测试目录 | 已写 | +| [`trailing-whitespace.md`](trailing-whitespace.md) | `trailing-whitespace` | 行尾空白(改写型;CubeMX/Keil 生成文件豁免) | 全部 | 已写 | +| [`end-of-file-fixer.md`](end-of-file-fixer.md) | `end-of-file-fixer` | 文件末尾必须恰好一个换行(改写型;同豁免) | 全部 | 已写 | +| [`check-added-large-files.md`](check-added-large-files.md) | `check-added-large-files` | 别把大文件(日志/镜像/二进制)误提进仓库 | 全部 | 已写 | +| [`check-merge-conflict.md`](check-merge-conflict.md) | `check-merge-conflict` | 别把没解完的冲突标记(`<<<<<<<`)提上去 | 全部 | 已写 | +| [`check-yaml.md`](check-yaml.md) | `check-yaml` | YAML 语法是否可解析(门禁配置自己坏了会静默不跑) | `.yml`/`.yaml` | 已写 | +| [`gitlint.md`](gitlint.md) | `gitlint` | 提交信息规则:标题长度、**正文每行 ≤80**、空行要求 | 提交信息(commit-msg 阶段) | 已写 | ## 3. 每篇文档的固定八段 @@ -59,8 +59,11 @@ 7. **豁免与基线**——白名单形状、在册数量、还债口径(只减不增); 8. **改门禁本身**——自测怎么做、改名会波及什么(例如基线键)、为什么不许拿基线洗假阳性。 -## 4. 迁移状态 +## 4. 现状与下一步 -`docs/代码规范/pre-commit使用说明.md` §2 目前仍是多数门禁的详解所在地。本目录逐篇写完后, -那节收敛为"通用机制 + 总览 + 指向本目录",同一事实只留一处(`docs/文档规范.md` 的要求)。 -上表「状态」列就是迁移进度;改完一篇就把该行改成"已写",并在 §2 对应小节留一行指针。 +- **本目录 23 篇已全部成文**(上表「文档」列 = 覆盖的 hook id,全部可达)。 +- 新增/改动一个门禁时的三步:① 在 `.pre-commit-config.yaml` 落地;② 在本目录**照八段结构 + 新增或更新一篇**("限制与盲区"一节必须写真实判据的限度,不许留空);③ 在本 README 索引加一行。 +- **下一步(尚未做)**:把 `docs/代码规范/pre-commit使用说明.md` 的 §2「各钩子规则详解」收敛为 + "通用机制 + 总览 + 指向本目录",同一事实只留一处(`docs/文档规范.md` 的要求)。收敛时要 + 按**内容**重指全库对 §2.x 的既有引用——只核编号存在与否不够(同号小节会静默放过)。 diff --git a/docs/门禁说明/check-added-large-files.md b/docs/门禁说明/check-added-large-files.md new file mode 100644 index 00000000..eebf886a --- /dev/null +++ b/docs/门禁说明/check-added-large-files.md @@ -0,0 +1,52 @@ +# `check-added-large-files` — 别把大文件提进仓库 + +> 一句话:新增文件超过阈值(官方默认 **500 KB**)即拦。它防的是"手滑提交",不是体积门禁。 + +- 配置:官方 `pre-commit-hooks` v5.0.0,未加参数 → 用官方默认阈值与默认行为。 + +## 1. 它在查什么 + +- **新增**(不是修改)文件的体积;超阈值即 FAIL。已入库大文件被**修改**不报——它只挡"新拉进来一个东西"。 + +## 2. 为什么要它 + +- 本仓天然产出巨大的文件:`MDK-ARM/**/*.map`、`.htm`、`.hex`、`build_log.txt`、串口与自检日志、 + 截图。这些要么在 `.gitignore`,要么在生成文件豁免清单里;**一旦有人手工 `git add -f` 一份 + `.map` 或一轮台架日志,仓库就永久背上这个体积**(删掉文件也不会变小,历史在对象库里)。 + +## 3. 触发面 / 看不见什么 + +- 只看新增文件;**不追历史**里已有的大文件。 +- 不看目录总大小、不看提交次数、不看"未压缩但可压"的浪费。 + +## 4. 报错怎么读 + +``` +check for added large files..............................................Failed + docs/xxx.map (12.3 MB) is too large +``` + +- 正确动作:**别提交它**(收进 `.gitignore` 或改放产物目录),而不是调高阈值。 +- 确实必须入库的二进制(子集字体生成物、golden 参考图)走体积可控的生成链,见 `font-regen` + 与 `ui-verify` 的做法。 + +## 5. 好处 + +- 挡住"最不可逆、最难补救"的那类错误(仓库膨胀只能重写历史来治)。 + +## 6. 限制与盲区 + +1. 阈值是**单个新文件**的,不看总量、不看累积; +2. 已入库的大文件永远绿(改它也不报); +3. 很多小文件堆成巨型目录(例如 `simulator/build/`)完全无感——那类靠 `.gitignore` 挡; +4. 顶层 `exclude` 与 `.gitignore` 的分工比这条门禁更根本(口径见 `docs/代码规范/pre-commit使用说明.md`)。 + +## 7. 豁免与基线 / 8. 改这个门禁本身 + +- 无基线、无逐文件豁免开关;放宽只能给 `args` 加 `--maxsize`,而那会同时放宽**所有**新文件。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(预防性门禁,未记录到本仓实际拦截案例) | diff --git a/docs/门禁说明/check-merge-conflict.md b/docs/门禁说明/check-merge-conflict.md new file mode 100644 index 00000000..5bb5fbc8 --- /dev/null +++ b/docs/门禁说明/check-merge-conflict.md @@ -0,0 +1,50 @@ +# `check-merge-conflict` — 别把没解完的冲突标记提上去 + +> 一句话:文件里还留着 `<<<<<<<` / `=======` / `>>>>>>>` 这类**行首**冲突标记就拦。 + +- 配置:官方 `pre-commit-hooks` v5.0.0,无额外参数。 + +## 1. 它在查什么 + +- 行首的冲突标记形态(7 个小于号 / 等号 / 大于号 / 竖线组合),以及 `.rej`、`.orig` 这类补丁残留文件。 + +## 2. 为什么要它 + +- 冲突标记留在代码里**常常照样编得过**(落在注释里、或被条件编译包住),只在运行时或下一个 + 人才发现——所以这条便宜且值。 + +## 3. 触发面 / 看不见什么 + +- 只看本次提交涉及的文件;**已提交的历史冲突不追**。 +- 不看语义冲突(两边各加一个函数、逻辑互相矛盾):那是人与测试的活。 + +## 4. 报错怎么读 + +``` +check for merge conflicts................................................Failed + App/x/y.c +``` + +- 解完冲突再提交。若你是在**文档里演示冲突标记**,本门禁没有局部豁免:把示例写成非行首形态 + (缩进进代码块、或用占位描述),必要时 `--no-verify` 并在提交说明里写清为什么。 + +## 5. 好处 + +- 挡住"顺手 `git add .` 把半解状态一起提交"的常见事故。 + +## 6. 限制与盲区 + +1. **判据是行首标记形态**:本仓注释里的分隔线(`// =====`、`# =====`)因为带前缀字符不会误报, + 但**真放在行首的 7 连等号**(ASCII 表格线、Markdown 里手写标题下划线)会被判成冲突标记; +2. 只认标准标记:某些合并工具/编辑器的自定义残留形态不一定被认; +3. 语义冲突、跨文件不一致(改签名没改调用点)完全不在能力范围(后者由编译器与单测负责)。 + +## 7. 豁免与基线 / 8. 改这个门禁本身 + +- 无基线。要演示冲突标记,**改写法**(不放行首)比改门禁或放宽更合适。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(含"行首 7 连等号会被误判"这个写作注意点) | diff --git a/docs/门禁说明/check-yaml.md b/docs/门禁说明/check-yaml.md new file mode 100644 index 00000000..9a297293 --- /dev/null +++ b/docs/门禁说明/check-yaml.md @@ -0,0 +1,62 @@ +# `check-yaml` — YAML 必须能被解析 + +> 一句话:只保证 YAML **语法**能解析。它不校验 schema,也就挡不住"键名写错导致配置静默失效"。 + +- 配置:官方 `pre-commit-hooks` v5.0.0,无额外参数。触发面 = YAML 文件: + +## 1. 它在查什么 + +- 语法可解析性:缩进一致性、制表符、锚点/别名引用是否存在、重复键等**解析层**错误。 + +## 2. 为什么要它 + +- `.pre-commit-config.yaml` 一旦语法坏掉,**整套门禁直接不跑**:仓库看起来一切正常,其实 39 + 个检查全部关着。CI 工作流文件同理(远端不动作、本地也不报错)。 +- 这是所有静默失效里最坏的一种,值得一条零成本门禁。 + +## 3. 触发面 / 看不见什么 + +- 只跑**改动过的** YAML;其它文件里内嵌的 YAML(Markdown 代码块、README 示例)不校验。 +- 完全不校验"字段是否符合使用方的预期"。 + +## 4. 报错怎么读 + +``` +check yaml...........................................(no files to check)Skipped +check yaml...............................................................Failed + .pre-commit-config.yaml:88:3: mapping values are not allowed here +``` + +- `Skipped` 只说明本次没改 YAML,**不代表 YAML 是好的**。 + +## 5. 好处 + +- 把"门禁整体失效"的概率压到最低;报错定位精确到行列。 + +## 6. 限制与盲区(请当正文读) + +1. **键名写错它不管**。本仓有现成教训:pre-commit 4.x 已移除 `always: true`,实测对 `local` + 钩子里的这个键**只告警 `Unexpected key(s)` 然后忽略** —— 配置"看起来生效、其实没生效", + 而 `check-yaml` 全绿。所以规则变成:**想让某个钩子总是跑,必须让 `files:` 覆盖到所有触发面** + (已写进 `AGENTS.md` 提交门禁一节,并在本目录多篇文档里复述为"触发面"一节)。 +2. 不校验语义:`args` 少写一个参数、`entry` 指向一个不存在或不可执行的命令,都不报 + (那要等到钩子真正跑起来才炸)。 +3. 不校验 CI 表达式(`${{ ... }}` 内容原样当字符串)。 +4. 多文档 YAML、锚点深合并能过解析但可能与本意相反,它不看。 + +## 7. 豁免与基线 + +- 无基线、无豁免(改到 YAML 就跑)。 + +## 8. 改这个门禁本身 / 改配置的正确验证方式 + +- 改完 `.pre-commit-config.yaml`,**验收不是"过了 `check-yaml`"**,而是实跑: + `& '.venv\Scripts\python.exe' -m pre_commit run --all-files`,然后确认目标钩子**出现在输出里** + 且状态是 `Passed` 而不是 `Skipped`(`Skipped` = `files:` 没覆盖到,等于没挂)。 +- 新增 `repo: local` 钩子时,`language: system` + 相对路径入口是最容易写错的两处。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(含 `always: true` 只告警不报错这条实测教训) | diff --git a/docs/门禁说明/coding-standard.md b/docs/门禁说明/coding-standard.md new file mode 100644 index 00000000..68e23fc9 --- /dev/null +++ b/docs/门禁说明/coding-standard.md @@ -0,0 +1,91 @@ +# `coding-standard` — 编码规范 V1.0 的可机械化子集 + +> 一句话:它只查规范里**能用形状判定的 19 条**(命名、宏、注释块、括号、禁用函数、goto、 +> 递归、可变参数……)。规范里"名字要达意、函数要短、职责要清"这些它一条都管不了。 +> 规范正文 = `docs/代码规范/嵌入式C语言代码规范_V1.0.md`。 + +- 配置:`entry: python scripts/check_coding_standard.py`、`pass_filenames: true`、 + 排除 `test/`、CubeMX 生成的 `Src/`、`Inc/`、第三方与 `Lib/`。 + +## 1. 它在查什么(逐条对应规范条款号) + +| 规则号 | 内容 | 条款 | +|---|---|---| +| CODE-RULE-01 | `.h` 必须有 `XXX_H` 形式的保护宏,禁止 `__` 前缀 | 2.3.1 | +| CODE-RULE-02 | 文件开头必须有文件头注释(含 模块名称 / 作者) | 5.2 | +| CODE-RULE-03 | `#define` 宏名全大写 + 下划线 + 数字 | 3.4.1 | +| CODE-RULE-04 | `typedef` 类型名以 `_t` 结尾 | 3.5.1 | +| CODE-RULE-05 | `switch` 必须有 `default` | 6.3.2 | +| CODE-RULE-06 | 禁用不安全函数 `strcpy/strcat/sprintf/gets/scanf/strtok` | 7.5 | +| CODE-RULE-07 | 禁止 `goto` | 6.3.3 | +| CODE-RULE-08 | 禁止 Tab 缩进(4 空格) | 4.1.1 | +| CODE-RULE-09 | 函数定义必须有函数注释(含「函数功能」) | 5.3 | +| CODE-RULE-10 | 函数名全小写(`HAL_` 厂商回调名豁免) | 3.3.1 | +| CODE-RULE-11 | 禁止相对/绝对路径 include | 2.3.5 | +| CODE-RULE-12 | 禁止可变参数函数 | 7.6 | +| CODE-RULE-13 | 禁止递归函数 | 7.7 | +| CODE-RULE-15 | 文件作用域指针变量必须初始化为 `NULL` | 6.1.4 | +| CODE-RULE-16 | 文件作用域非 static 全局变量必须 `g_` 前缀 | 3.2.2 | +| CODE-RULE-17 | 非 static 函数必须在配对 `.h` 声明(无配对头则跳过) | 2.2 | +| CODE-RULE-18 | 禁止 Yoda 条件(字面量必须在运算符右侧) | 6.3.1 | +| CODE-RULE-19 | 函数定义左大括号与右括号同行 | 4.4.2 | + +规则号里 **CODE-RULE-14 是缺的**:条款作废后编号不回收,留着是为了让历史文档里的"14"不指向别的东西。 + +## 2. 为什么要它 + +- 这 19 条如果靠人看,评审成本极高且必然漏;机械化之后评审可以把注意力留给机器管不了的部分。 +- 有硬约束背景的两条特别值:**禁止递归**与**禁止可变参数**——本仓 `DBG_*` 每个调用点内联约 + 90 B Flash + 216 B 栈,栈与 Flash 都是红线资源;`g_` 前缀和指针初始化则是最容易埋雷的静态状态。 + +## 3. 触发面 / 看不见什么 + +- 只看**改动的 C/H 文件**(`pass_filenames: true`);`test/`、`Src/`、`Inc/`(CubeMX 生成)、 + 第三方、`Lib/` 不查。 +- **后果**:同一份规范对 `test/board/`(编进固件的 6300+ 行)不生效,对 CubeMX 生成区也不生效。 + `test/` 的静态检查由 `test-cppcheck` 兜一半,形状规范没人管。 +- 逐文件判定:跨文件的事实(声明在 A、定义在 B、调用在 C)它只看到眼前这一个文件。 + +## 4. 报错怎么读 + +``` +Drivers/BSP/X/x.c:42 [CODE-RULE-09] 函数 foo_bar 缺少函数注释(应含「函数功能」) +``` + +- 单独跑:`& '.venv\Scripts\python.exe' scripts/check_coding_standard.py <文件...>`。 +- 报的是**形状**,通常一眼能改;改不动的(例如厂商回调必须叫那个名字)走脚本里的显式豁免名单, + 别在源码里塞空格绕开正则。 + +## 5. 好处 + +- 形状统一,diff 干净;新人不必记住整份规范也能过门禁。 +- 规则号可追溯:报错直接给出"规则号 + 条款号",能查到出处。 + +## 6. 限制与盲区 + +1. **"有注释"≠"注释对"**:`return-doc` 类判据(见 `bsp-err`)与本门的 CODE-RULE-09 都只查 + 关键词在不在,写了"函数功能:见调用方"照样通过。 +2. **豁免靠形状**:`HAL_` 前缀直接放过函数名规则——任何以 `HAL_` 开头的项目内函数也一并被放过。 +3. CODE-RULE-17 在**没有配对 `.h`** 时跳过:新写的 `.c` 忘了建头,就不会被这条抓到 + (双端注册由 `registration-check` 管,声明完整性没人管)。 +4. 命名"达意"、函数长度、圈复杂度、职责边界:完全没有机械检查。 +5. `test/` 与生成区整目录不查(§3),这三类目录的规范只靠人。 +6. 逐文件 + 正则形状:把违规写进字符串字面量、注释、宏展开里就看不见;反过来说,宏里真正的 + 违规代码也可能看不见。 + +## 7. 豁免与基线 + +- **没有基线**:硬拦截,新写的代码必须合规;因此**放宽任何一条都会立刻降低约束**,需谨慎。 +- 豁免方式只有两种:脚本里的显式豁免名单(按名字形状,如 `HAL_` 回调)、或改代码。 + +## 8. 改这个门禁本身 + +- 新增规则前先在**全库跑一次**统计违规数;有存量的规则要么先清存量,要么走"基线只减不增" + 的模式(参考 `bsp-err`、`layer-deps`),不要直接挂上硬拦截卡住所有人。 +- 规则作废**不回收编号**(见 CODE-RULE-14)。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(19 条规则逐条列出,含 CODE-RULE-14 缺号说明) | diff --git a/docs/门禁说明/cppcheck.md b/docs/门禁说明/cppcheck.md new file mode 100644 index 00000000..2243a022 --- /dev/null +++ b/docs/门禁说明/cppcheck.md @@ -0,0 +1,84 @@ +# `cppcheck` / `test-cppcheck` — 静态分析(同一工具,两套范围与严格度) + +> 一句话:它管**单个 C 文件内部**能被看出来的一类错(越界、空指针、未初始化、资源/性能、 +> 可移植性)。它**不知道跨文件的事实**,也不看我们的项目契约(那是 `bsp-err` 的活)。 + +- 两个钩子:`cppcheck`(项目自有代码,`--enable=warning,performance,portability,style`)、 + `test-cppcheck`(只扫 `test/`,**只开 warning**,排除 `test/host/*/stub`)。 +- `entry: cppcheck`、`language: system`、`types: [c]`、`pass_filenames: true`(**逐文件**分析)、 + `--error-exitcode=1`、`--std=c99`、`--quiet`、`--inline-suppr`。 + +## 1. 它在查什么 + +- 检查集 = cppcheck 的 `warning` / `performance` / `portability` / `style` 四类(`test-cppcheck` 只有 `warning`)。 +- `--inline-suppr`:允许源码里用 `// cppcheck-suppress ` 单行抑制——**这是唯一的一事一议豁免通道**。 +- **抑制项清单**(`args` 里逐条,别当噪音看,每条都是为了让它跑得动): + `missingIncludeSystem` / `missingInclude`(我们没有完整的 include 环境,否则整文件解析不下去)、 + `invalidPrintfArgType_sint` / `_uint`(`DBG_*` 宏展开后参数类型判定全是噪音)、 + `unusedFunction`(逐文件分析看不到调用方,误报连天)、`unusedStructMember`、 + `noExplicitConstructor` / `useStlAlgorithm`(C 项目无关)、`constParameterPointer` / + `constParameterCallback`(建议加 const,属排版建议)、`redundantAssignment`、 + `normalCheckLevelMaxBranches`(**分支数上限提示**——见 §6 第 2 条)。 +- include 路径:`Inc`、`App`、`Drivers/BSP`、HAL Inc、CMSIS Include、FreeRTOS include。 + +## 2. 为什么要它 + +- **真实案例一(把 `simulator/` 纳入受检的直接收获)**:本仓原先在顶层 `exclude` 里整目录挡掉 + `simulator/`,实测那是自研代码;纳入 cppcheck 后**立刻揪出 `sim_syslog.c` 一处真实的空指针解引用**。 +- **真实案例二(`test/` 为什么单独立钩)**:`test/board/` 下 6300+ 行 C 代码**是要编进固件的**, + 此前完全豁免、只靠自律,出过 `s_rx_buf` 使用先于声明。基线:2026-10-03 全量实测 0 条 warning, + 所以敢直接挂钩子(先清存量再关闸,顺序不能反)。 + +## 3. 触发面与看不见的地方 + +- 排除:`Drivers/CMSIS/`、HAL、`Drivers/BSP/NET/lftpd/`、`Drivers/BSP/dhara/`、`Lib/`、 + `Middlewares/`、`MDK-ARM/`、`test/`(`test/` 由第二个钩子负责)。 +- UI 生成文件(`ui_font_cn_12`、`ui_menu_tab`)**纳入受检**——生成器按工程约定产出,不是第三方。 +- 只在**本机装了 cppcheck** 时有效;CI 不跑构建类门禁(见 `docs/CI/README.md`)。 + +## 4. 报错怎么读 + +``` +Drivers\BSP\XXX\x.c:123:45: warning: ... [someCheckId] +``` + +- 单独跑:`cppcheck --enable=warning,style,performance,portability --inline-suppr --std=c99 --quiet <文件>`; + 想复现钩子行为,**照抄 `args` 里的抑制清单**,否则噪音会让你误判成"新引入的问题"。 +- 确属误报:在**那一行**加 `// cppcheck-suppress ` 并写一句为什么;不要用 `--suppress=` 全仓关掉一类。 + +## 5. 好处 + +- 便宜、快、无副作用;对越界/空指针/未初始化这类"编译器不管但真会炸"的问题有效。 +- `test/` 有独立档位,测试代码不再是法外之地。 + +## 6. 限制与盲区(今天最有料的一节) + +1. **它照不出"返回值被丢弃"**。实跑证据:按本钩子自己的 `--enable` 跑 `gd5f2gq5ue.c`,对 + **14 处丢弃 `HAL_SPI_*` 返回值**、以及"未初始化局部量直接进 `P_FAIL` 判定"**一条不报**—— + 它假定被调函数会写出参,而我们的驱动在失败路径根本不写出参。**这条负结果是 `bsp-err` 加 + `consume-result` 检查的直接依据**(另一半依据是 ARMCC v5 不认 `warn_unused_result`)。 +2. `normalCheckLevelMaxBranches` 被抑制,但**问题本身还在**:默认检查级别会**限制分支分析** + (它会打印 "Limiting analysis of branches")。分支多的函数,它其实没分析完。想要全量得 + 显式 `--check-level=exhaustive`(很贵,没挂进提交门禁)。 +3. **逐文件**分析(`pass_filenames: true`):没有跨 TU 知识,`unusedFunction` 因此被整条抑制; + 跨文件的契约错误(A.c 改了返回值语义、B.c 的判据就失效)看不见。 +4. include 缺失被整条抑制 → **解析可能只到一半**,越报"干净"越要留个心眼:某类检查是否根本 + 没跑到,本工具不告诉你。 +5. 它不懂项目约定:错误码、超时单位、分层方向、日志成本一概不管。 + +## 7. 豁免与基线 + +- 没有存量基线:**新报即拦**。所以存量必须先清(`test/` 就是这么做的)。 +- 两类合法豁免:单行 `// cppcheck-suppress`(要写理由)、`args` 里的整类抑制(要写清为什么整类都是噪音)。 + +## 8. 改这个门禁本身 + +- 加抑制项 = 永久放弃一类检查,必须在提交说明里写清噪音样本;能单行抑制就别整类抑制。 +- 加/减 `--enable` 档位前,先跑一次全库看新增多少条——直接挂钩子会把所有人卡在提交外。 +- 两个钩子的抑制清单**有意不同**(`test-cppcheck` 少几条),改的时候别顺手"对齐"成一套。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档;补记"照不出返回值丢弃"这条实测负结果(它是 `consume-result` 的由来) | diff --git a/docs/门禁说明/cubemx-survival.md b/docs/门禁说明/cubemx-survival.md new file mode 100644 index 00000000..de838ee5 --- /dev/null +++ b/docs/门禁说明/cubemx-survival.md @@ -0,0 +1,71 @@ +# `cubemx-survival` — CubeMX 重生成的定制存活审计 + 漂移检测 + +> 一句话:CubeMX 重生成会**静默覆盖** `USER CODE` 标记区之外的一切定制,而且多数**不报编译错**, +> 只在真出故障时才暴露。这一门把"易失清单 + 关键位置哈希"钉成基线,重生成后一眼看出被改了什么。 + +- 配置:`entry: python scripts/check_cubemx_survival.py --check`、`pass_filenames: false`、 + `files:` 覆盖 `.ioc`、`Src/**`、`Inc/**`。方法论:`docs/子系统设计/看门狗设计说明.md` §10.2 与 §10.3。 + +## 1. 它在查什么 + +1. **存活审计**:逐项检查"重生成易失清单"里的定制是否仍在预期位置(清单以 `must_contain` 等 + 断言表达),标记区外的存量给出显式警告; +2. **漂移检测**:把关键位置哈希写进 `.cubemx_survival_baseline.json`,`--check` 逐项比对, + 任一项变化即 FAIL。 + +## 2. 为什么要它 + +- 实测记录:`.ioc` 里 `ProjectManager.KeepUserCode=true` **只**保护 `USER CODE BEGIN/END` 之间; + 2026-09-29 一次重生成覆盖了 **4 处定制**(P0×1 + P1×2 + P2×1)。典型形态:崩溃采集函数被打回 + 空 `while(1)`、加固宏被整块删除——**不报错,只在真崩的时候才发现没日志**。 +- AGENTS 把防丢失排成三层(L1 写进 `.ioc` → L2 搬进 `USER CODE` 区 → **L3 本门当审计兜底**)。 + +## 3. 触发面 / 看不见什么 + +- `MDK-ARM/*.uvprojx` **不参与漂移检测**(`no_baseline=True`):该文件每次 `@build` 后都被 UV4 + 自动改写(XML 归一化 + `PackID` 降到本机 DFP 版本),参与检测等于每次构建后提交都被拦; + 对本项只保留 `must_contain` 断言("源文件仍挂载")。 +- 只查清单里**列了的东西**:没写进清单的新定制 = 不受保护(这是清单式门禁的固有代价)。 + +## 4. 报错怎么读 + +``` +CubeMX 重生成存活审计.......Failed + [FAIL] Src/main.c 缺少定制片段断言: fault_handler_enable +``` + +- 单独跑:`--report` 只看审计报告(不比对基线);确认改动无误后 `--write` 重建基线。 +- 正确动作:**先把定制搬回正确位置(优先 L1/L2),再 `--write`**;直接 `--write` 等于宣布 + "被覆盖掉的样子是新的正确答案"。 +- 每次重生成后**立刻**跑(AGENTS 铁律),别等到提交时才发现。 + +## 5. 好处 + +- 把"重生成到底动了什么"从人肉 diff 变成一条命令的 FAIL 清单; +- 基线是**位置哈希**而不是内容全文,重排注释不炸、删掉定制必炸。 + +## 6. 限制与盲区 + +1. **清单驱动**:只保护登记过的定制。新写的标记区外定制不在清单里 → 静默丢失且不会报。 +2. 标记区**内**的内容不受本门保护(CubeMX 不动它,但如果我们自己写错,它也不判对错)。 +3. 不检测 `.ioc` 配置项与生成结果是否自洽(比如某个外设被关掉,它不会告诉你)。 +4. `uvprojx` 无漂移检测(见 §3)→ 工程文件里被 UV4 抹掉的东西照不出;那条只能靠"改动不入库"的约定。 +5. 基线文件是 `.cubemx_survival_baseline.json`(仓库根):它若被顺手 `--write`,门禁当场失效且看不出来。 + **`--write` 必须与"确认过的改动"写在同一个提交里并说明。** +6. 它不判断定制**是否仍然正确**,只判断"还在不在原来的位置"。 + +## 7. 豁免与基线 + +- 基线 = `.cubemx_survival_baseline.json`,语义与 `bsp-err`/`layer-deps` 的"只减不增"**相反**: + 它是"确认后的快照",允许被 `--write` 前滚。因此它防的是**意外**,不防"有意改掉"。 + +## 8. 改这个门禁本身 + +- 新增受保护项:先想能不能落到 `USER CODE` 区(L2)或 `.ioc`(L1)——清单越长,人工维护越重; +- 自造标记区(自定义 `BEGIN/END` 名字)会被官方模板换代抹掉,别拿它当保护手段。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(含 2026-09-29 一次覆盖 4 处的实测记录) | diff --git a/docs/门禁说明/doc-links.md b/docs/门禁说明/doc-links.md new file mode 100644 index 00000000..b9c586fb --- /dev/null +++ b/docs/门禁说明/doc-links.md @@ -0,0 +1,85 @@ +# `doc-links` — 文档引用一致性(路径存在性 + 章节号存在性 + 文件命名) + +> 一句话:它管**"你引用的东西还在不在"**——文件路径、`§小节号`、文件命名合法。 +> 它**不管**你引用的那节内容还是不是你要讲的事(见 §6)。规范 = `docs/文档规范.md`。 + +- 配置:`entry: python scripts/check_doc_links.py --sections-strict`,`pass_filenames: true`, + `files: \.md$`(只跑改动的 `.md`,但脚本内部对全库做一次 `--all` 复核更常见)。 + +## 1. 它在查什么 + +| 类 | 写法 | 解析基准 | +|---|---|---| +| A | Markdown 链接 `[文本](相对路径)` | 相对**当前文档所在目录** | +| B | 反引号路径引用 `` `docs/xxx/yyy.md` `` | 相对**仓库根**(本仓约定:文档互引一律用仓库根相对路径) | +| C | 目录名 / 文件名规范 | 不得含空格与路径敏感字符 | +| D | 章节引用 `` `路径.md` §N.M `` | **目标文档里必须真有这个编号的标题** | + +D 类刻意只校验**紧邻写法**:文档名与 `§` 之间只允许空白、反引号、括号。实测这样能把误报压到 0。 +钩子带 `--sections-strict`,所以"紧邻/裸名/前缀/别名"几种不合规写法**都会拦提交**。 + +## 2. 为什么要它 + +- 本仓文档互引极密(实跑:**2594 处章节引用**),一次改名/删小节历史上要扫 10~30 个文件, + 手工扫既慢又漏。 +- **真实案例(本次写文档时发生,且这段解释本身又触发了一次同样的报错)**:我在正文里只写了 + **本文档自引用**——恰好本文档没有那个编号的小节,于是提交前被拦。这类"看着是人话、机器判成悬空" + 的写法,靠这份门禁一条命令就能挑出来。 +- 另一类真实价值:把某小节内容搬去别的文档后,旧引用若还指向原文档的同号小节,只要那个 + 号还在,**它是合法的但错的**——本门禁不报(见 §6 第 3 条),但**号被删掉的情形会报**, + 而"搬家时顺手删旧号"是本仓的常规做法,所以多数迁移错误能被抓到。 + +## 3. 触发面 + +- 只跑 `*.md`;第三方目录由 `exclude` 控制,脚本内再兜一层;`docs/芯片手册/` 属外部资料, + 链接形态与文件名都不受控 → 整体豁免。 +- 每次全库复核会打印统计。实跑:149 篇文档、章节引用 2594 处、**机器可判定 1504 处(58%)**; + 跳过分类:昵称式 25 / 非文档锚点 13 / **变更记录 655** / 外部手册 341 / 省略文档名的跨文档引用 2 / + 目标无编号体系 54 / 无法判定 0。 + +## 4. 报错怎么读 + +``` + [示例] docs\代码规范\pre-commit使用说明.md:528 [章节引用(自引用)] §4.3(… 无此编号标题) + 修法:确认它到底指哪篇 → 写成「`路径.md` §N.M」,或改指本文档现有编号。 +``` + +- 单独跑:`& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all --sections-strict`; + 想看"哪些引用判不了目标、建议怎么改写":`--section-todo`。 +- 合法的"故意引用不存在文件"的写法:行首/行内加 `[计划]` 前缀(约定产物尚未创建)、 + 加 `[示例]` 标记(引用报错样例、日志片段),或在该行写"未创建 / 尚未创建 / 已删除 / 待排期"等状态词。 + +## 5. 好处 + +- 改名与挪目录有安全网(配合 `scripts/rename_ref.py`:`git mv` + 自动改写全库引用 + 落盘后 + 自动跑本脚本复核),所以"改名"不再是靠人肉 grep 的危险动作。 +- 把"引用要写全路径 + 紧邻编号"这条写作约定变成机器判定,读者永远能点开你指的地方。 + +## 6. 限制与盲区 + +1. **只判存在性,不判语义**。它只知道目标文档里那个节号还在,不知道那一节还在讲你要讲的事。 + 内容搬家而号还在 → 全绿但是错的。**这是本门禁最大的盲区**。 +2. **58% 的章节引用机器判不了**(见 §3 的跳过分类)。其中"变更记录 655"是设计选择:历史行 + 不回改,所以不校验——代价是变更记录里的引用可以烂掉。 +3. 只校验**紧邻写法**:昵称式(文档简称 + 空格 + 节号)不校验。写法不紧邻 = 放过,所以规则是 + "宁可啰嗦也要写全"。 +4. 表格里裸写的小节号、或"设计文档 + 编号"这种不写文件名的写法,会被当成**本文档自引用** + 静默放过(本文恰好有同号小节就通过)。搬移内容时这是真踩过的坑。 +5. 豁免清单本身可被滥用:加个 `[示例]` 就安静了。**只有引用报错样例/日志片段才该用它。** +6. 只管 `.md`:代码注释里的文档引用不在范围内(那边靠人工与 `bsp-err` 之类的文档检查间接覆盖)。 + +## 7. 豁免与基线 + +- 没有"基线"概念:硬拦截,新写的引用必须合法。 +- 有**别名表** `scripts/section_aliases.json`:昵称 → 文档的映射,歧义就登记进去或把话写清楚。 + +## 8. 改这个门禁本身 + +- 放宽匹配(例如允许裸名引用)前先想清楚:**误报压到 0 是靠"只认紧邻写法"换来的**。 +- 新增一类豁免,必须同时新增一类误报;把豁免写在 `docs/文档规范.md` 与本节里,别只写进代码。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(数据为当日实跑:149 篇 / 2594 处章节引用 / 可判定 58%) | diff --git a/docs/门禁说明/end-of-file-fixer.md b/docs/门禁说明/end-of-file-fixer.md new file mode 100644 index 00000000..6428e212 --- /dev/null +++ b/docs/门禁说明/end-of-file-fixer.md @@ -0,0 +1,51 @@ +# `end-of-file-fixer` — 文件末尾必须恰好一个换行(**改写型**) + +> 一句话:文件最后一个字节必须是换行、且末尾不能留多余空行;改的是工作区,需要再 `git add` 一次。 + +- 配置:官方 `pre-commit-hooks` v5.0.0,`exclude: *cubemx-generated`(与行尾空白同一段豁免)。 + +## 1. 它在查什么 + +- 末尾无换行 → 补一个;末尾多余空行 → 删到只剩一个换行;空文件(0 字节)不动。 + +## 2. 为什么要它 + +- 无末尾换行的文件在 `git diff`、`cat`、`grep` 拼接时会与下一条输出黏在一起; +- 避免 `\ No newline at end of file` 这种噪声长期留在历史 diff 里。性质上属**预防性**。 + +## 3. 触发面与豁免 + +- 与 `trailing-whitespace` 共用生成文件豁免(`.ioc`、`Src/`、`Inc/`、HAL/CMSIS/中间件、 + Keil 工程与 `.map` 等)——理由相同:那是工具产物。 + +## 4. 报错怎么读 + +``` +Fix End of Files.........................................Failed +Files were modified by this hook. +``` + +- `git add` 被改文件后重新提交。 +- ⚠ **stash 陷阱(本仓踩过)**:pre-commit 跑钩子前会 stash 未暂存改动,钩子看到的是"没有你 + 中间状态"的树;改代码同时更新基线/受管区时,务必**先 `git add` 再跑钩子**,否则会假报警。 + +## 5. 好处 + +- 全库文件末尾形态统一,脚本类工具(按行读文件的门禁脚本自己也算)行为更可预期。 + +## 6. 限制与盲区 + +1. 只管文件末尾:行间的多余空行、缩进风格都不管; +2. **不查编码**:BOM 有无与丢失由构建期 `#870-D` 暴露,`U+FFFD` 由 `no-mojibake` 管,且后者 + 只覆盖 C/H——文档侧的字节卫生目前靠人工(本目录作者每次交付前逐文件核 BOM/CR/`EF BF BD`); +3. 只处理本次涉及的文件,历史文件不会被批量整理(避免一次性巨大 diff)。 + +## 7. 豁免与基线 / 8. 改这个门禁本身 + +- 无基线。若真有"某类文件必须无末尾换行"的需求,加豁免并写明原因,别用 `--no-verify` 绕过。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档 | diff --git a/docs/门禁说明/font-charset.md b/docs/门禁说明/font-charset.md new file mode 100644 index 00000000..dd1e1d76 --- /dev/null +++ b/docs/门禁说明/font-charset.md @@ -0,0 +1,68 @@ +# `font-charset` — UI 字符集差集(防"缺字显示白框") + +> 一句话:本仓唯一中文字体是**子集**字体;UI 字符串用了没进字体的汉字,上机就是**空白方框**, +> 没有日志、没有报错。本门把"改了 UI 文案要重跑字体生成链"这条约定机械化。 + +- 配置:`entry: python scripts/check_font_charset.py`、`pass_filenames: false`、 + `files:` 覆盖 `Drivers/BSP/UI/`、`App/task/ui_task.(c|h)`、`ui_cn_chars.txt`、`tools/font_gen/`。 +- 生成链(四步,见 `font-regen` skill):UI 源码 →`extract_cn.py`→ `ui_cn_chars.txt` →`make_font.py`→ `ui_font_cn_12.c`。 + +## 1. 它在查什么(两段链条,缺一环即白框) + +| 段 | 比对 | 不满足时 | +|---|---|---| +| A | 必需字符集(源码在用 ∪ `KEEP_EXTRA` 保留字) vs `ui_cn_chars.txt` | 必需而 txt 无 → **GAP FAIL**(字体必然缺字形);txt 有但非必需 → 告警不拦(只费 Flash);`--strict` 升级为 FAIL | +| B | `ui_cn_chars.txt` vs `ui_font_cn_12.c` 的 cmap | txt 有而字体无 → **GAP FAIL**(转换器对缺字形/低码位会**静默丢字**) | + +A 通过 + B 通过 ⟹ 字体覆盖"源码在用"与"刻意保留"的每一个字。 +`KEEP_EXTRA` 的存在理由:防止将来 `extract_cn.py` 按"源码已不用"静默删掉**已落盘记录里还在用**的字。 + +## 2. 为什么要它 + +- 生成期的 `verify_coverage()` 只校验"txt ⊆ 字体 cmap",**管不到"源码用到的字有没有进 txt"**; + 改了 UI 字符串却忘跑生成链时,此前提交期没有任何门禁能拦——只能靠肉眼截图发现白框。 + +## 3. 触发面 + +- 只跑改动的 `.c`/`.h`(`types: [c]`)。 + +## 4. 报错怎么读 / 单独跑 + +- `& '.venv\Scripts\python.exe' scripts/check_font_charset.py --strict`(严格档:冗余字也拦); + `--chars` / `--font` 换输入用于测脚本自己。 +- 修法是**重跑生成链**(`font-regen` skill 四步),不是把字从 UI 里删掉凑绿。 + +## 5. 好处 + +- 把一种"上机才知道、且完全没有日志"的显示缺陷压到提交前; +- `KEEP_EXTRA` 用"逐条带理由"的形式沉淀,避免将来被无声删掉。 + +## 6. 限制与盲区 + +1. **只看 C/H 源文件**:`types: [c]`。写进 `ui_menu_tree.txt`(生成菜单表的文本源)里的汉字 + 不在直接扫描面内——它靠"重新生成 UI 表 → 落到 `.c` → 被扫"这条间接路径生效;**手工改生成物 + 或跳过生成器时,覆盖关系可能出现空洞**。 +2. 必需集来自"源码里出现的汉字",**运行期拼出来的字符串**(格式化进来的字、外部配置文件里的字、 + 记录内容回显)完全在视野外——那类白框本门照不到。 +3. 只校验**码位覆盖**,不校验字形是否美观/是否为所需点阵尺寸。 +4. 拉丁字母、数字、标点不在本门范围(另有字体与布局约定)。 +5. `--strict` 不在钩子默认档位里(默认只拦 GAP):冗余字形长期靠人自觉清理。 +6. 字体文件本身若被手工编辑(不跑生成器),A/B 两段仍可能同时"通过"而与实际 cmap 不符—— + 因为它读的就是生成物。 + +## 7. 豁免与基线 + +- `KEEP_EXTRA` 是**刻意保留清单**(存量 + 逐条理由),性质同 `registration-check` 的已知缺口表; + 新增要写理由,删掉等于放弃保护。 +- 没有"只减不增"的债务基线。 + +## 8. 改这个门禁本身 + +- 放宽"必需集"来源(比如不再扫某个目录)前先确认该目录的字最终确实会进生成链; +- 与 `ui-golden` 是互补关系:本门管"有没有字形",golden 管"像素对不对"。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(两段链条判定口径 + 生成期校验覆盖不到的那一环) | diff --git a/docs/门禁说明/gitlint.md b/docs/门禁说明/gitlint.md new file mode 100644 index 00000000..c7ef0b32 --- /dev/null +++ b/docs/门禁说明/gitlint.md @@ -0,0 +1,67 @@ +# `gitlint` — 提交信息格式(`commit-msg` 阶段) + +> 一句话:管**提交信息长什么样**:标题长度、正文行长度。内容真假、是否与 diff 相符,它一个字都不评。 + +- 配置:官方 `gitlint` v0.19.1(`repo: https://github.com/jorisroovers/gitlint`),规则文件 + `.gitlint`:`ignore=body-is-missing`、`[title-max-length] line-length=100`,其余用默认值。 +- 阶段:只在 `commit-msg` 跑(`.pre-commit-config.yaml` 顶部 `default_install_hook_types:` + 显式装了 `commit-msg`)。 + +## 1. 它在查什么(本仓实际生效值) + +| 项 | 生效值 | 来源 | +|---|---|---| +| 标题最大长度 | **100** 字符 | `.gitlint` 覆盖默认 | +| 正文每行最大长度 | **79/80** 字符级(gitlint 默认 `max-line-length`) | gitlint 默认 | +| 正文缺失 | **不检查** | `.gitlint` 显式 ignore | +| 标题结尾标点等 `T` 系列 | 默认 | gitlint 默认 | + +## 2. 为什么要它 + +- 本仓的提交说明是**主要载体**:实测数据、基线只减不增的口径、负结果、为什么这么改,全写在正文 + 里。格式一炸(超长行在 `git log` / 网页里被硬切),这些内容就没人读得下去。 +- 两条**本仓真踩过**的坑,已固化进 `AGENTS.md`: + ① 中文长标题极易超 100 → 标题保持短,细节写正文; + ② 正文每行必须控制在 80 内,而且**用 `git commit -m` 传长中文会被终端代码页转码破坏** + → 一律"短标题 + `git commit -F `"。 + +## 3. 触发面 / 看不见什么 + +- `git commit`、`--amend`、`-F`、合并生成的提交都会跑;**`--no-verify` 连它和其它 38 个钩子一起跳过**。 +- 不校验:提交说明与 diff 是否相符、是否真的构建/测试过、是否夹带无关改动、是否该拆成多笔。 + +## 4. 报错怎么读 + +``` +gitlint..............................................................Failed + 12: BBody line length limit exceeded, lengthy body line (max-line-length: 79) + 1: T2 Title has trailing whitespace +``` + +- 前缀数字是行号(第 1 行是标题)。本仓最常犯的是 `B`(正文行超长)与标题超 100。 +- 稳妥写法:正文落成 UTF-8 无 BOM、每行 ≤78 字符的临时文件,再 `git commit -F`;提交前自查 + 有没有超 80 的行(本目录作者在实践中就是先做这一步再提交)。 + +## 5. 好处 + +- `git log` 在任何终端宽度都可读;长说明(含实测数字与踩坑)能完整留在历史里,这是本仓历史 + 可读性的基础设施。 + +## 6. 限制与盲区 + +1. **`body-is-missing` 被显式忽略** → 空说明也能过。信息质量完全靠自觉(本仓选择宽松,代价写在这里)。 +2. 只管格式:正文写满 40 行**错误结论**也照样过。 +3. 不追溯历史:`--no-verify` 用多了,历史里就永远留着不合规提交,事后无法自动补齐。 +4. 长度按**字符数**算,不是显示宽度:中英文混排 + 反引号时,"看着没到 80"仍可能超限。 +5. 它不看提交是否该拆(一次提交里混了代码 + 文档 + 配图,它不管)。 + +## 7. 豁免与基线 / 8. 改这个门禁本身 + +- 无基线;放宽只能改 `.gitlint`。抬长度前先确认你要的不是"把长句拆短"。 +- 想加严(例如要求正文非空、要求带模块前缀):先在最近 20 条真实提交上试跑,别直接关闸。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(生效值:标题 100 / 正文行 ≤80 / `body-is-missing` 已忽略) | diff --git a/docs/门禁说明/host-unit-tests.md b/docs/门禁说明/host-unit-tests.md new file mode 100644 index 00000000..4e5b3c3f --- /dev/null +++ b/docs/门禁说明/host-unit-tests.md @@ -0,0 +1,91 @@ +# 主机侧单测(16 个 `*-unit-test` 钩子)— 同一机制的 16 个实例 + +> 一句话:把**不依赖硬件的逻辑层**拿到 PC 上编译并跑断言(每个模块一个 `test/host/<模块>/run_unit_test.py`)。 +> 它是"回归最快的两层之一"(静态层 + 逻辑层),但**不等于真机正确**。 + +- 16 个钩子各有一个 `files:` 白名单(改动被测模块或测试目录才跑)、`pass_filenames: false`; + 远端 CI 只跑 L0 静态门禁 + L0.5 主机侧单测(见 `docs/CI/README.md`)。 + +## 1. 它在查什么(16 个实例) + +| 钩子 | 被测对象 | +|---|---| +| `watchdog-unit-test` | 看门狗判定状态机 `App/util/task_watchdog.c` | +| `sys-clock-unit-test` | 系统时钟换算 `App/util/sys_clock.c` | +| `cfg-file-unit-test` | 配置引擎 `App/cfg/cfg_file.c` | +| `phasor-unit-test` | 相量层 `App/algo/phasor.c` + `sample_ring.c` | +| `adc-input-unit-test` | ADC 输入层 `App/algo/adc_input.c` | +| `vol-lock-unit-test` | 卷锁 `App/fs/vol_lock.c` | +| `time-sync-unit-test` | 时间同步 `App/sync/time_sync.c` | +| `adc-health-unit-test` | ADC 有效性校验 `App/algo/adc_health.c` | +| `prot-set-unit-test` | 保护定值模型 `App/algo/prot_set.c` | +| `prot-elm-unit-test` | 保护元件框架 `App/algo/prot_elm.c` | +| `freq-unit-test` | 测频 `App/algo/freq.c` | +| `sha256-unit-test` | SHA-256 `App/util/sha256.c` | +| `ftp-auth-unit-test` | FTP 鉴权状态机 `Drivers/BSP/NET/lftpd/lftpd_auth.c` | +| `iec104-unit-test` | IEC104 协议栈(ASDU / 链路 / 端口表) | +| `soe-unit-test` | SOE 动作记录编解码 `App/log/soe_rec.c`、`soe_store.c` | +| `nand-port-unit-test` | dhara NAND 移植层 `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` | + +共同机制:`run_unit_test.py` 在宿主编译器上编译被测 `.c` + 该目录的 `stub/` 替身,跑断言并汇总 +用例数(各模块的 `TC-HOST-<模块码>` 号段见 `docs/编号规范.md`)。 + +## 2. 为什么要它 + +- 状态机、编解码、换算这类逻辑,在真机上验证成本极高(要么构造故障、要么等复现); + 在 PC 上毫秒级就能重跑,是"改完敢不敢提交"的第一层信心。 +- 它们**已经在真实收口上起过作用**:`nand-port-unit-test` 与 `sys-clock-unit-test` 的桩里就 + 注入过 RTC 读失败等错误路径,用来证明失败分支的行为(本次 S-5 收编时同步改过这两个 runner + 的 include 目录,见 §6 第 4 条)。 + +## 3. 触发面 + +- 只有**改动该被测模块或该测试目录**时对应钩子才跑(16 个钩子各管一块,改日志不会触发 16 份编译)。 +- 单独跑:`& '.venv\Scripts\python.exe' test/host/<模块>/run_unit_test.py`。 + +## 4. 报错怎么读 + +常见两类,处置完全不同: + +1. **断言失败** → 逻辑真的变了/错了:先判断是被测代码错还是期望值该更新; +2. **编译失败**(找不到头文件)→ 多为**测试环境滞后**:`run_unit_test.py` 的 `include_dirs` + 是手写清单。今天真实发生:新加 `Drivers/BSP/bsp_err.h` 后,两个 runner 报"无法打开包括文件 + `bsp_err.h`",要在两个 runner 里补一行 `os.path.join(ROOT, "Drivers", "BSP")`。 + +## 5. 好处 + +- 快(单套件数百毫秒级)、可在无硬件机器上跑、可在远端 CI 上跑(CI 不跑 Keil 构建,单测是唯一 + 能跑的代码级验证); +- 错误路径可注入:真机上很难制造的"RTC 读失败""扇区坏"在这里是一行赋值。 + +## 6. 限制与盲区 + +1. **宿主编译器 ≠ ARMCC**:ARMCC 的警告(`#870-D` 多字节字符、`#1-D` 之类)、结构体对齐、 + `enum` 取型、MicroLIB 行为差异,本层一概照不到 → 必须 `@build`。 +2. **桩就是第二份真相**:`stub/` 与被测代码依赖的真实头文件不同步时,会出现"单测全绿、真机 + 行为不同"。桩签名改了而测试没跟着改,编译器未必报错(尤其桩返回宽类型)。 +3. `include_dirs` 与源文件清单是**手写**的:新依赖一个目录就会假失败(见 §4 第 2 类),反过来, + 漏了某个真实依赖可能让测试**编过却少编了一个翻译单元**(少测而不自知)。 +4. **没有对应钩子的模块 = 零回归**:本层只覆盖上表 16 个模块;其余逻辑层(含 UI 逻辑、网络 + 状态机主体)只能靠 `ui-golden`、板级套件与真机 `@test`。 +5. 时序与并发不在范围:中断抢占、DMA 完成回调、临界区、真机 tick 抖动都照不到(历史上最贵的 + 几个坑正是这类,见 `docs/问题记录/*_Trap_Records.md`)。 +6. 断言写的是"作者当时想到的分支":分支覆盖是否充分,没有任何度量(无覆盖率统计)。 + +## 7. 豁免与基线 + +- 没有基线:新增或改动的模块必须自身通过。 +- 用例号段:`TC-HOST-<模块码>` 以 `test/host/<目录>/*` 为属主登记处(`docs/编号规范.md`); + 新增用例要带号,否则 `id-check` 会按形态/悬空规则处理。 + +## 8. 改这个机制本身 + +- **新增模块的宿主单测 = 复制一个 runner 模板 + 在 `.pre-commit-config.yaml` 加一个钩子 + + 登记 TC 号段**,三件事缺一件就会出现"测试存在但没人跑"或"跑了但无号可引"。 +- 改 `include_dirs`/源文件清单时,务必确认失败是环境问题而不是逻辑回归(先看是编译错还是断言错)。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(16 个实例清单 + 今天两个 runner 需补 include 目录的实测) | diff --git a/docs/门禁说明/id-check.md b/docs/门禁说明/id-check.md new file mode 100644 index 00000000..913d7402 --- /dev/null +++ b/docs/门禁说明/id-check.md @@ -0,0 +1,99 @@ +# `id-check` — 编号门禁(实体编号的八项校验) + +> 一句话:它管**"号"这个东西不许野生**——每个编号必须属于一个已登记的族、形态合法、 +> 有定义处、有人引用、引用它的时候说清是哪篇文档的号。 +> 规范正文 = `docs/编号规范.md`(§4 前缀总表、§8 门禁),本门禁是那八项的机械执行。 + +- 配置:`entry: python scripts/check_ids.py --strict`,`pass_filenames: false`, + `files:` 覆盖 `.md/.c/.h/.py/.yaml/.json/.txt` 与配置文件本身。 +- 两份数据源(不是脚本里写死的):`scripts/id_registry.json`(族 → 形态正则 / 属主登记处 / + 是否冻结 / 游标 / 作用域 / 级别)、`scripts/id_aliases.json`(旧字面量 → 现行字面量)。 + +## 1. 它在查什么 + +| 校验 | 内容 | 级别 | +|---|---|---| +| 1 | 出现**未登记前缀**(自造编号) | 错误 | +| 2 | **形态不合规**:缺连字符的裸形(模块字母直接跟数字)、族名与数字之间空一格的空格形、中文形、补零位数不对、档位词带后缀、缺模块码 | 错误 | +| 3 | **悬空引用**:引用了查无定义且不在别名表的号 | 错误 | +| 4 | **重复定义**:同一字面量被两个属主各定义一次 | 错误 | +| 5 | **冻结族越游标**:已冻结的族里出现游标之外的新号 | 错误 | +| 6 | **引用未紧邻文档名**:文档内编号(各文档自己的风险表等)裸引 | 错误 | +| 7 | **孤儿条目**:定义了但全库无人引用 | 告警(不拦) | +| 8 | 打印**编号引用可判定比例**,只许涨不许跌(跌破 `id_registry.json` 里的基线即失败) | 恒生效 | + +"定义"的机械判据是:**该字面量出现在它所属族的"属主登记处"文档里**。不依赖表格行首之类 +容易漂移的形态。副作用要知道:把号写进属主文档的**任何位置**都算定义。 + +## 2. 为什么要它 + +- 本仓同时存在 `N-xx`/`N-x`/`TC-*`/`E-xx`/`TRAP-*`/`D10`/`U12` 等几十族编号,历史上出过 + **真事故**:登记簿销项删行后号被复用(撞号)、引用指向已被删的号(悬空)、同一字面量两个 + 属主(重复定义)。规范 §1 把成因写得很直白:连字符与补零偷偷承载语义。 +- **本次实测拦下过我自己两次**:一次是"字母加数字"自造号(校验 1),一次是引用某个跨文档 + 编号时没紧邻文档名(校验 6)——两次都是提交前门直接报,说明它在真起作用。 +- 校验 8 是唯一的"趋势"闸门:它不要求号变少,只要求**解释能力不许退化**(新增不可判定的字面量会让比例下跌 → 失败)。 + +## 3. 触发面 + +- 扫描面以 git 清单为准。当前一次实跑:**551 个文件、编号字面量 8868 处**。 +- **排除**:`Lib/`、`Middlewares/`、HAL/CMSIS、`docs/芯片手册/`、`docs/硬件平台/`。 + 最后一个是关键:硬件文档里的**元器件位号 `U1`/`C12`/`R3`/`L5`/`D7`** 与我们的编号字面 + 同形,不整体排除就会大量误报(脚本里另有一套位号识别,但仍**没有解除这两个目录的语料排除**)。 +- 已声明的"同号不同义"族对(`docs/编号规范.md` §4.3):实跑 583 个字面量走 `collides_with` + 白名单,**不计入校验 4**。 + +## 4. 报错怎么读 + +``` +[FAIL] 编号门禁·增量拦截:N 类台账之外的错误级问题(新引入的债) + +2 [校验1] <文件> <字面量> + → 改引用/补登记处;确属该欠的债要在台账写 reason(默认禁止顺手加) +``` + +- 单独跑:`& '.venv\Scripts\python.exe' scripts/check_ids.py --strict --verbose`(`--verbose` 带修法,`--top 30` 多打,`--only 1,3,5` 只跑指定校验)。 +- 常用旗标:`--strict`(钩子用的档位)、`--verbose`、`--write-backlog` / `--prune-backlog` + (重建 / 收紧台账)、`--update-baseline`(把当前可判定比例写回 `id_registry.json`,只许涨)、`--no-trace`。 +- 存量在 `scripts/id_known_backlog.json`(按指纹豁免,实跑 **640 处**在册)。 + +## 5. 好处 + +- 把"编号卫生"从人脑记忆变成机器判定:销项只做一半、引用写错文档、自造号,都会在提交时被点名。 +- 台账机制让存量不阻塞日常工作,同时"无台账外新账"这句话是可验证的承诺。 + +## 6. 限制与盲区(这一节请当正文读) + +1. **绿灯不等于编号都合法**:门禁是【增量拦截】——存量 640 处按台账豁免。它保证的是 + "没变差",不是"干净"。 +2. **`collides_with` 白名单挡事故、不挡可读性**。已声明同号不同义的族对不再报——于是 + "新造一个和已有裸形撞车的代号"可以一路绿灯。**这正是本门禁今天照不出来的东西**: + 我自己造的四个字母数字代号在被改名前,没有任何一条校验会响(同形多义已被声明为已知事实)。 + 结论:**编号可读性要人管**,`docs/门禁说明/bsp-err.md` §6 那条就是这个教训的落点。 +3. 实跑有 **867 处"裸形盲区"**:形态太像普通词/数字组合的字面量识别不出来,等于不在管辖内。 +4. 校验 7(孤儿)只告警不拦提交:定义完一直没人引用的号会长期存在而门禁不响。 +5. "定义"的判据是"出现在属主登记处文档里",**不校验语义**:写进错的属主文档、或写成一句 + 否定("该号从未存在")也算定义——批量改名工具最阴的失效模式就是"字面改对、意思改错"。 +6. **登记表类文件不得当引用语料**(`id_registry.json`、`id_aliases.json`、`id_known_backlog.json`、 + `scripts/rename_maps/` 里必然写着旧的非法形态),否则第一次提交就被自家审计表当"新账"拦下。 +7. 校验 8 只许涨不许跌:好处是防退化,代价是**顺手写一个未登记族的号就会红**,哪怕与本次改动无关。 +8. 它不懂"这个号该不该存在"。新增一族编号是否合理,仍是 `docs/编号规范.md` 的人工判断。 + +## 7. 豁免与台账口径 + +- 存量:`scripts/id_known_backlog.json`(指纹级);**只减不增**,收紧用 `--prune-backlog`。 +- 别名:`scripts/id_aliases.json`(旧字面量 → 现行字面量)——号改名走 `scripts/rename_ids.py`, + 它会显式写别名条目,**绝不全局查找替换**。 +- 比例基线:`id_registry.json` 的 `decidable_ratio_baseline`(实跑当前 82.85%,基线 82.6%、容差 0.1pp)。 + +## 8. 改这个门禁本身 + +- 改族定义 = 改 `scripts/id_registry.json`,改完必须看**编号字面量总数有没有变小**: + 历史上两次批量改名后,识别正则的上界没跟着放宽,**整批号不再被匹配**(等于静默放行)。 +- 不要为了绿而往台账加 reason;确实该欠的债要写清楚原因(默认禁止顺手加)。 +- 台账文件自身不是语料(见 §6 第 6 条),改动它之后要单跑一次确认没有自循环新账。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(数据为当日实跑:551 文件 / 8868 字面量 / 台账 640 处 / 可判定 82.85%) | diff --git a/docs/门禁说明/layer-deps.md b/docs/门禁说明/layer-deps.md new file mode 100644 index 00000000..ac548dbf --- /dev/null +++ b/docs/门禁说明/layer-deps.md @@ -0,0 +1,86 @@ +# `layer-deps` — 分层方向(`Drivers/BSP/**` 不得 include `App/**` 的头) + +> 一句话:它只查**一条边**——下层(BSP)有没有反向依赖上层(App)。它不管同层乱依赖、 +> 不管传递依赖,也不管"这行 include 是不是真的需要"。 + +- 配置:`entry: python scripts/check_layer_deps.py`、`pass_filenames: false`、 + `files: ^Drivers/BSP/|^scripts/check_layer_deps\.py$|^\.pre-commit-config\.yaml$`。 +- 规范出处:`docs/架构设计/项目架构总览.md`(六层单向:业务 → UI/基础服务 → 中间件 → + BSP 驱动 → HAL/RTOS → 硬件)、`AGENTS.md` 分层边界;整改背景见报告 §0.4 的 S-1 行。 + +## 1. 它在查什么 + +1. 取 `git ls-files` 的全部文件(不扫构建产物); +2. 用 `App/**` 下的 `.h` 建**按文件名(basename)**的索引; +3. 扫 `Drivers/BSP/**` 的 `.c`/`.h`,抓 `#include "..."` 形式的引号路径; +4. 命中 App 头 → 记一条边 `(引用方文件, App 头)`;先按完整路径精确匹配,精确命中为空才退回同名候选; +5. 头在 `ALLOW` 集合里 → 永久豁免(横切基础设施);否则与 `BASELINE` 比对:**新增即拦,消失即提示收紧**。 + +**`ALLOW` 三条(新增条目必须先回答"它为什么不是上层服务",否则就是给自己开后门)**: +`App/log/console_log.h`(日志后端,BSP 的 `dbg_log.h` 本来就建在它上面)、 +`App/util/sys_clock.h`(时间源,日志时间戳的唯一提供者)、`App/util/sha256.h`(纯算法、无状态)。 + +**`BASELINE` 六条(2026-10-22 清点,只减不增)**:`NETCFG/net_cfg.c`、`NETCFG/ftp_cfg.c` +→ `App/cfg/cfg_file.h`(配置引擎属主是 defaultTask);`NET/lftpd/lftpd_io.c` → `App/fs/vol_lock.h`; +`UI/ui_page_runlog.c`、`UI/ui_page_selftest.c`、`UI/ui_w_records.h` → `App/log/sys_log.h`。 +修法是**注入 / 传参**(上层把数据或回调递进 BSP),不是把上层服务搬进 BSP。 + +## 2. 为什么要它 + +- 这句话在两份文档里写了很久,**但此前没有任何机械检查**:BSP 里可以随手 `#include "cfg_file.h"` + 而无人报警。 +- **口径漂移是可测量的证据**:2026-09 审查报告记"NETCFG 一处",2026-10-22 实测已长到 + **8 个文件 9 处**。边界的静默腐蚀只能靠门禁止住——先钉基线,再只拦新增。 + +## 3. 触发面 + +- `pass_filenames: false`:**每次都全库扫**(反向依赖是全局事实,只看暂存文件会漏)。 +- 只看 `Drivers/BSP/**` 里的 `.c`/`.h`;`App/**` 内部谁依赖谁、`UI` 是否越层,一概不看。 + +## 4. 报错怎么读 + +``` +[FAIL] 新增反向依赖(BSP -> App): + - Drivers/BSP/XXX/x.c -> App/cfg/cfg_file.h +``` + +- 单独跑:`--verbose` 列出全部边并标注 豁免/基线/新增;`--prune` 只报已消失、可删的基线条目。 +- 退出码 0 通过 / 1 有新增 / 2 用法或环境错误。 +- ⚠ pre-commit 会 stash 未暂存改动:改完要**先 `git add` 再跑钩子**,否则看到的是旧树。 + +## 5. 好处 + +- 把"分层"从 PPT 变成一条命令的事实;债务总量随时可查(`--verbose` 就是清单)。 +- 存量与新增分离,不阻塞正常工作,但保证它只会变小。 + +## 6. 限制与盲区 + +1. **只查直接 include,查不到传递依赖**。BSP 的 `a.h` 里 include 了 App 头,BSP 的 `b.c` + 只 include `a.h`——`b.c` 这条边不出现,**但 `a.h` 那条会报**。基线里 `ui_w_records.h` 的 + 注释专门写着"(组件头,会外溢到 include 链)",就是提醒这种外溢要人来看。 +2. 只认 `#include "引号"` 形式;`#include ` 这类写法不在管辖内(也违反 include 规范, + 由 `coding-standard` 的 CODE-RULE-11 兜)。 +3. **按 basename 匹配**:如果 `App/` 与别处存在同名头,会按同名候选放宽(先精确后同名), + 可能把边记到错误的属主上;反向地,如果 App 头改名而 BSP 用别的路径引到它,判定也会变。 +4. 只管 `BSP → App` 一个方向。`App → UI`、`UI → 驱动内部` 这类越层不在本门范围。 +5. 它不看**是否真的用到**那个头里的东西:删掉最后一处使用却忘了删 include,边仍然存在, + `--prune` 也就不会提示你收紧基线。 +6. `git` 不可用时退回 `os.walk`(跳过 `.git/.venv/build`)——两条路径的清单可能不同, + 在无 git 环境下跑出的"通过"不能当证据。 +7. 绿灯的含义只是"没有新增反向边":6 条存量债务仍然在那里。 + +## 7. 豁免与基线 + +- `BASELINE` 与 `ALLOW` 都写在脚本里(不是外部台账)。**只减不增,脚本没有 `--write`**。 +- 两类豁免语义不同,别混:`ALLOW` = 永久横切能力(判"它是不是叶子");`BASELINE` = 待还债(判"这条边什么时候拆")。 + +## 8. 改这个门禁本身 + +- 往 `ALLOW` 加条目要写理由,并想清楚会不会变成"给上层开后门";能进基线就别进 `ALLOW`。 +- 还掉一条债就跑 `--prune` 并删掉那一行;基线只减不增,加行等于放弃约束。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(在册 6 条基线逐条列出,含每条的属主背景) | diff --git a/docs/门禁说明/lv-conf-sync.md b/docs/门禁说明/lv-conf-sync.md new file mode 100644 index 00000000..3349eed1 --- /dev/null +++ b/docs/门禁说明/lv-conf-sync.md @@ -0,0 +1,73 @@ +# `lv-conf-sync` — 两份 `lv_conf.h` 的**生效值**必须一致 + +> 一句话:MCU 那份与模拟器那份 LVGL 配置,比对的是**最终生效值**(含默认值回填),不是 +> "文件里写了什么"。不一致时最直接的伤害是:**像素回归跑的不是固件真正编译的那套特性**。 + +- 配置:`entry: python scripts/check_lv_conf_sync.py`、`pass_filenames: false`;2026-10-11 加, + 与 Flash 死代码裁剪那一簇同批落地(背景见 `docs/问题记录/项目审查与优化建议报告.md`)。 + +## 1. 它在查什么 + +比对 `Lib/lv_conf.h`(MCU)与 `simulator/lv_conf.h`(PC),对每个宏**取生效值**:文件里没定义的, +去 `Lib/lvgl/src/lv_conf_internal.h` 取默认值。四类判定: + +| 类别 | 含义 | 级别 | +|---|---|---| +| MISMATCH | 生效值不同 | FAIL | +| INVALID | 写了 LVGL 根本不认的宏名(写了不生效) | FAIL | +| DUPLICATE | 同一文件里定义两次(后者生效,极易误读) | FAIL | +| IMPLICIT | 一边靠默认值生效 | 告警(若默认值与另一边不同,已升级为 MISMATCH) | + +## 2. 为什么要它(本目录里"省了多少字节"最可量化的一篇) + +- 此前长期漂移:模拟器早已 I1-only,MCU 端却让 11 个 `LV_DRAW_SW_SUPPORT_*`、5 个未用控件类、 + `LV_USE_THEME_SIMPLE`、`LV_USE_FLEX/GRID` 全靠默认值隐式生效,把用不到的代码整份链进固件 = + **实测 64,576 B 死代码(旧镜像的 23.5%)**。 +- 更糟的一面:`ui-golden` 跑的是模拟器那份配置 → 特性子集不一致时,**回归验证不到固件真实行为**。 + +## 3. 触发面 + +- `files:` 覆盖两份 `lv_conf.h` 与本脚本;`--mcu/--sim/--internal/--lvgl-src` 可换输入(测脚本自己用)。 +- `--list` 打印全量对照表,是回答"到底哪些宏在两边不一样"的唯一可靠入口。 + +## 4. 报错怎么读 + +``` +两份 lv_conf.h 特性子集一致性.......Failed + MISMATCH LV_USE_GRID: MCU=1(默认) SIM=0 +``` + +- 单独跑:`& '.venv\Scripts\python.exe' scripts/check_lv_conf_sync.py --list`。 +- 修法一律是**两边都显式写出来**(把生效值写成定义),别靠"两边都不写、默认值刚好一样"过门。 +- 退出码 0 一致 / 1 有不一致或无效或重复 / 2 环境错误(文件缺失)。 + +## 5. 好处 + +- 直接防 Flash 回归(23.5% 那个量级),也让"模拟器看到的 = 固件编译的"这句前提成立。 +- INVALID/DUPLICATE 两类抓到的是"写了却不生效"这种最难查的坑。 + +## 6. 限制与盲区 + +1. **只比对这两份文件里出现的 + LVGL 认识的宏**:某个宏两边都没写、默认值也相同 → IMPLICIT 都不报, + 但一旦 LVGL 升级改了默认值,两边可能同时变成你不想要的值,而它是一致的、绿的。 +2. 依赖 `lv_conf_internal.h` 的路径与形态(`--internal` 可覆盖):LVGL 换版本、内部头改名, + 默认值回填会退化成"看不见"。 +3. 不校验**条件编译**:`#if defined(...)` 包住的 `#define` 是否真的生效,脚本不判断。 +4. 不校验特性之间的语义冲突(比如开了 A 必须开 B),只管两边一致。 +5. IMPLICIT 只是告警:长期靠"顺手补一行"来收敛,没有基线压力。 + +## 7. 豁免与基线 + +- 无基线(硬拦截)。两边确实要不同时,**唯一正确做法是先问"为什么模拟器要偏离固件"**—— + 答案通常是"模拟器有桩/无硬件",那属于平台后端差异,不该藏在 `lv_conf.h` 里。 + +## 8. 改这个门禁本身 + +- 扩充受检宏清单要同步 `--list` 输出;新增 INVALID 词表时要跟着 LVGL 版本走,否则会把合法宏 + 误判成不认(假阳性会逼人 `--no-verify`,比没有门禁更糟)。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(含 64,576 B / 23.5% 死代码实测出处) | diff --git a/docs/门禁说明/mem-budget.md b/docs/门禁说明/mem-budget.md new file mode 100644 index 00000000..a66d5cfe --- /dev/null +++ b/docs/门禁说明/mem-budget.md @@ -0,0 +1,84 @@ +# `mem-budget` — RAM 总量红线(`RW_IRAM1` / `RW_CCM` 超 92% 即拦) + +> 一句话:它是**总量刹车**,不是预算规划器。它只管两件事:任一静态区域别超 92%,以及 +> 台账里的水位数字别和 `.map` 漂移。 + +- 配置:`entry: python scripts/ram_baseline.py --gate`、`pass_filenames: false`、 + `files:` 覆盖 `App/`、`Drivers/BSP/`、`Src/`、`Inc/`、`Lib/lv_conf.h` 等会影响占用面。 +- 背景:登记簿 N-27 前半;三件套里的机械部分(阈值门禁 + 趋势台账 + 台账受管区)。 + +## 1. 它在查什么 + +1. 解析 `.map`,算 `RW_IRAM1`、`RW_CCM` 的占用率;**任一 > 92.0% → FAIL**,并附 Top-10 + `RW+ZI` 对象表(直接告诉你是谁吃掉的); +2. 顺带核对 `docs/内存管理/RAM预算台账.md` 的受管区(`MEM_BUDGET:BEGIN/END`)是否与 `.map` + 一致——防台账漂移,与 `mem-numbers` 同一思路; +3. `.map` 不存在 → 自适应跳过(这台机器没做 Keil 构建)。 + +配套的两个入口(**不在提交门禁里**,属于日常): + +- `--update`:每次 `@build` 后往 `scripts/ram_baseline.tsv` 追一行趋势; +- `--trend`:打印趋势并**高亮 `RW+ZI` 较上一行增长 > 1 KB 的对象**——把"ZI 增量超 1 KB 必须解释" 这条纪律机械化; +- `--write`:同步台账受管区(区域水位 + 子系统配额对照)。 + +子系统配额(规划值,改配额要先在台账变更记录里登记,再改脚本 `QUOTAS`): +网络 12 KB / 存储 12 KB / UI 16 KB / RTOS 堆 56 KB / 应用 8 KB。 + +## 2. 为什么要它 + +- 本仓 RAM 是真红线:`RW_CCM` 常年 **87.9%**(FreeRTOS 堆迁 CCM 之后),离 92% 只差几个百分点; + 链接期才发现塞不下,代价是回退一大片改动。 +- 实跑当前值:`RW_IRAM1 57.4%` / `RW_CCM 87.9%`(阈值 92%)。 +- 趋势台账让"谁什么时候吃掉了这 3 KB"可查,不必靠记忆。 + +## 3. 触发面 / 看不见什么 + +- 只看 `RW_IRAM1` 与 `RW_CCM` 两个**静态区域**;不看:栈运行时水位、FreeRTOS 堆运行时余量、 + 堆碎片、MicroLIB 的 512 B C 堆(那条纪律是"自研代码禁用 `malloc/free`",靠 `AGENTS.md` 铁律与评审)。 +- `.map` 缺失即跳过 → 无构建的机器(含远端 CI)这一门不产生信息。 + +## 4. 报错怎么读 + +``` +RAM 预算门禁 FAIL:RW_CCM 占用 92.3% > 92%(阈值 92%) + Top-10 RW+ZI 对象 … +``` + +- 单独跑:`& '.venv\Scripts\python.exe' scripts/ram_baseline.py --gate`(只读); + `--trend -n 12` 看趋势。**`--gate` 不修改任何东西**,要同步台账用 `--write`。 +- 报 FAIL 的正确动作是**回去减**(缩缓冲/挪区段/减日志调用点),不是抬阈值。 + +## 5. 好处 + +- 总量红线机械化,红线以下谁都能放心加东西; +- Top-10 归属表把"超了"变成"某某对象吃了多少",省掉一次手工 .map 考古; +- 趋势 + >1 KB 高亮让"增量必须有解释"变成可查记录,而不是口头承诺。 + +## 6. 限制与盲区 + +1. **92% 是拍的管理阈值,不是安全余量计算**(脚本里 `THRESHOLD_PCT = 92.0`)。它不回答 + "还剩多少才够下一个任务/缓冲"——那是 `docs/内存管理/RAM预算台账.md` 的配额表与人的事。 +2. **静态视角**:栈峰值、堆低水位、碎片一概看不见(运行时水位要靠串口 `[STACK]` 与 `stack_report.py`)。 +3. 只看两个区:Flash(`ER_IROM1`)不在本门阈值内,它由 `mem-numbers` 报数、无人卡阈值。 +4. `.map` 缺失即"通过"式跳过 → **在没有 Keil 构建的环境里绿,不代表有余量**。 +5. 配额对照是"目标态 vs 现状",现状超配(例如 UI)会被如实展示但**不拦提交**——它是台账,不是红线。 +6. 阈值/配额改动没有双人复核机制,改 `QUOTAS` 或 `THRESHOLD_PCT` 只靠提交说明约束。 +7. 并行分支约定(Lead 裁决 2026-10-12):**分支不跑 `--write`**,台账由主干统一回填;分支上 + 本门与 `mem-numbers` 可能因受管快照与 `.map` 漂移而红,那不代表预算超标(处置见 §7 的 `mem-numbers` 篇)。 + +## 7. 豁免与基线 + +- 没有基线、没有 `--no-verify` 之外的合法豁免:**超了就减**。 +- `docs/内存管理/RAM预算台账.md` 的受管区是事实展示区,不是豁免登记表;别在那里"解释一下"就算过关。 + +## 8. 改这个门禁本身 + +- 抬阈值前必须在台账里写清"为什么 92% 变松了",否则等于把刹车线往后画; +- 新增区域(例如再开一个自定义段)要同时改解析器与台账口径,否则新区域完全不在监控内; +- `--update` 的 tsv 是趋势事实源,改列格式会让 `--trend` 的历史对比失效。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(实跑:`RW_IRAM1 57.4%` / `RW_CCM 87.9%`,阈值 92%) | diff --git a/docs/门禁说明/mem-numbers.md b/docs/门禁说明/mem-numbers.md new file mode 100644 index 00000000..9c800080 --- /dev/null +++ b/docs/门禁说明/mem-numbers.md @@ -0,0 +1,75 @@ +# `mem-numbers` — 文档里的内存 / Flash 数字是否与 `.map` 一致 + +> 一句话:它让**"照文档推算内存余量"这件事仍然成立**。事实源是 Keil 的 `.map`,不是文档作者的记忆。 +> 属于审查报告"文档数字一致性"三档里的**体积档**(档号见 `docs/问题记录/项目审查与优化建议报告.md`)。 + +- 配置:`entry: python scripts/check_mem_numbers.py --scan`、`pass_filenames: false`、 + `files:` 覆盖 `docs/内存管理/*.md` 与相关脚本/文档改动面。 + +## 1. 它在查什么 + +1. 解析 `.map`,取 `RW_IRAM1`、`RW_CCM`、`ER_IROM1`、`Code`/`RO`/`RW`/`ZI` 等用量与占比; +2. 重新渲染文档里的**受管生成区**并比对是否一致: + `` … ``; +3. `--scan` 额外扫描**行文中的散句数字**(不在受管区里的手抄数字)并报不一致项; +4. 受管区外的内容**一律不改写**,只提示。 + +## 2. 为什么要它(这是本目录里"真事故"最硬的一篇) + +- 实测记录:2026-09-26 复核时,文档写 `RW_IRAM1 82,048 B / 62.6%`,而 `.map` 实际已是 + `87,040 B / 66.4%`;Flash 文档写 `249,432 B / 23.8%`,实际 `261,932 B / 25.0%`。 + 读者照文档推算余量会得出**错误结论**——这类错误不会报错,只会让人做出错误的容量决策。 +- 做法与 `task-table` 同构:能生成的就别手抄,手抄的必须有门禁核对。 + +## 3. 触发面与"看不见"的地方 + +- 事实源必须是**最新一次 `@build` 产出的 `.map`**;`.map` 不存在(这台机器没做 Keil 构建) + → **自适应跳过**(与 `ram_baseline.py --gate` 同口径)。CI 明确不跑构建,所以这一门在远端 + CI 是盲区,登记在 `docs/任务规划/` 的专项里。 +- 只覆盖脚本配置里登记的文档与字段;**没进受管区、也没被 `--scan` 认出的数字 = 无人核对**。 + +## 4. 报错怎么读 + +``` +内存/Flash 数字一致性(体积档).......Failed + docs/内存管理/xxx.md:46 受管区与 .map 不一致:RW_IRAM1 期望 75,264 B (57.4%),文档写 82,048 B (62.6%) +``` + +- 同步受管区:`& '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --write`(只改受管区)。 +- 只想看期望值:`--diff`;改 `.map` 路径:`--map <路径>`。 +- 行文散句数字**要手改**(`--scan` 只报不改),改完再跑一次确认。 + +## 5. 好处 + +- 文档里的体积数字从"某次构建的快照"变成"每次核对的活数"; +- 数字与 `.map` 一起进 diff,评审时一眼看出这次改动吃了多少 RAM/Flash。 + +## 6. 限制与盲区 + +1. **只核对数字对不对,不核对结论对不对**。受管区旁边那句"余量充足,可以再开一个任务" 与数字是否矛盾,它不知道。 +2. **散句只报不改**,且 `--scan` 靠正则认数字——写成"约 75 KB""七万五千字节"或换单位的句子 + 不在核对范围内(假阴性)。 +3. `.map` 缺失即跳过 → **在没有 Keil 构建的机器上它是绿的**(自适应跳过 = 无信息,不是通过)。 +4. 依赖最新构建:`@build` 之后没跑 `--write` 与文档同步,就会红;反过来,**先 `--write` 再改代码** + 会写出与将要提交的代码不一致的漂亮数字。 +5. 只覆盖已登记的文档:新开一篇写满体积数字的文档,不在名单里就没人管。 +6. 数字口径 = `.map` 的静态视图,**不含运行时水位**(栈峰值、堆低水位、碎片)——那些要另跑 + `scripts/stack_report.py` 与串口 `[STACK]` 读数。 + +## 7. 豁免与基线 + +- 无基线概念:要么与 `.map` 一致,要么失败。 +- 并行开发时的口径(Lead 裁决 2026-10-12):**分支不跑 `--write`**(多分支同改受管表会冲突), + 台账由主干在合并后统一回填;分支若只因本门与 `mem-budget` 失败,须先跑全量门禁确认只有这两条 + 失败、把摘要写进提交说明,再考虑绕过(见 `docs/环境与部署/并行任务工作树方案.md` 的合并流程)。 + +## 8. 改这个门禁本身 + +- 新增要核对的文档/字段:改脚本里的文档清单与解析器,**先确保 `.map` 解析不出错**再挂进提交; +- 别把 `--write` 接进钩子——"自动改数字"等于允许文档自己证明自己是对的。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(含 2026-09-26 的数字漂移实测记录) | diff --git a/docs/门禁说明/no-mojibake.md b/docs/门禁说明/no-mojibake.md new file mode 100644 index 00000000..88d05a9c --- /dev/null +++ b/docs/门禁说明/no-mojibake.md @@ -0,0 +1,65 @@ +# `no-mojibake` — 禁止 `U+FFFD`(编码损坏的不可逆痕迹) + +> 一句话:文件里只要出现 `U+FFFD`(替换字符),说明某段中文已经被按错误的编码解码过一轮, +> **损坏不可逆**——它必须被拦在提交之外。 + +- 配置:`entry: python scripts/check_mojibake.py`、`language: system`、`types: [c]` + (**只跑 C/H**)、`pass_filenames: true`(只查本次改动的文件)。 +- 脚本只有 17 行:读字节 → 数 `EF BF BD` → 有则退出码 1。 + +## 1. 它在查什么 + +- 唯一判据:字节序列 `EF BF BD`(UTF-8 编码的 `U+FFFD`)。 +- 它**不试图判断**"这是不是故意的"——按 AGENTS 铁律,字符串字面量必须纯 ASCII,中文只写注释, + 所以正常代码里出现替换符一定是损坏。 + +## 2. 为什么要它 + +- 本仓最常见的不可逆事故:用 `Get-Content`/`Set-Content`/`cmd` 重写含中文的文件,整文件按 + 系统默认代码页重解码 → 中文变 ``,**而且原内容再也拿不回来**(AGENTS 列为 Never)。 +- 它是那条铁律唯一机械化的部分:其它铁律(只用 Edit 工具、怀疑乱码先数 `EF BF BD`)都靠人。 + +## 3. 触发面 / 看不见什么 + +- 只看**本次改动的 C/H 文件**:`types: [c]` + `pass_filenames: true` 两重限制叠加。 +- 因此 `.md`、`.py`、`.txt`、`.yml` 里的 `U+FFFD` **不报**——而本仓文档量远大于代码量, + 乱码最容易发生在文档上(**这是本门最主要的盲区**)。文档侧目前靠人工字节卫生检查。 +- 只查 `EF BF BD` 这一种形态:别的损坏(GBK 字节直接落进 UTF-8 文件、无 BOM 文件里奇数个 + 汉字吞掉结束引号)它认不出。 + +## 4. 报错怎么读 + +``` +Forbid U+FFFD mojibake.......Failed + Drivers/BSP/X/x.c: 3 处 U+FFFD +``` + +- 处置:**立刻用 Edit 工具只修坏行**;不要 `git checkout`(会丢掉本次全部未提交工作)。 +- 若整文件已损坏且未提交,唯一救援是编辑器/系统的本地历史(未跟踪副本),否则只能重写该文件。 + +## 5. 好处 + +- 一条命令、零依赖、零误报地挡住不可逆损失;成本几乎为零(17 行、不解析语义)。 + +## 6. 限制与盲区 + +1. **不覆盖文档与脚本**(`types: [c]`)。写 `.md` 时的乱码不会被拦——这是本目录里"覆盖面与 + 需求错配"最明显的一例。 +2. **只查改动文件**:历史里已存在的 `` 不会被追溯(存量不报)。 +3. 不判断"是否故意的":真要在注释里写替换符做样例,会被拦(本仓没有这种需求)。 +4. 不检测编码本身(BOM 有无、CP936 内容)——BOM 丢失由构建期 `#870-D` 暴露,不在本门。 + +## 7. 豁免与基线 + +- 没有豁免:出现即失败。这是有意的——不可逆损失不值得讨论。 + +## 8. 改这个门禁本身 + +- 若把覆盖面扩到 `.md`/`.py`:先全库扫一遍存量数(历史上文档里可能已有),**有存量就别直接挂 + 硬拦截**(会把所有人卡在提交外),要么先清要么走基线式增量拦截。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(含"只覆盖 C/H、文档侧仍是人工"这条覆盖面错配的定性) | diff --git a/docs/门禁说明/registration-check.md b/docs/门禁说明/registration-check.md new file mode 100644 index 00000000..f7119170 --- /dev/null +++ b/docs/门禁说明/registration-check.md @@ -0,0 +1,77 @@ +# `registration-check` — 双端注册反向校验(源文件有没有真的参与编译) + +> 一句话:本仓两处注册表都是**显式文件列表**,忘了登记的文件能编过、能提交,但**从不参与编译** +> ——这一门专抓"写了却从不生效"的死代码,包括第四种更隐蔽的:页面描述符没接线。 + +- 配置:`entry: python scripts/check_registration.py --strict`、`pass_filenames: false`。 +- 背景:登记簿 N-23(出处审查报告 N-06 残余);UI 页面那类是 U4 门禁补强(2026-10-15)。 + +## 1. 它在查什么(四类缺口) + +扫描 `App/`、`Drivers/BSP/`、`Src/` 下全部 `.c`(排除第三方),对照 `MDK-ARM/STM32F407-Demo.uvprojx +` 的 `` 与 `simulator/CMakeLists.txt` 的源列表: + +| 类 | 情形 | 后果 | +|---|---|---| +| ① | 文件存在但未进 uvprojx | 固件端不编译(死代码或漏注册) | +| ② | 文件存在但未进 CMakeLists | 模拟器端不编译——**MCU-only 文件不进模拟器是正确的**,脚本按豁免判定 | +| ③ | 注册了但文件不存在(反向) | 幽灵注册(改名/删除后残留条目) | +| ④ | `Drivers/BSP/UI/ui_page_*.c` 定义了 `ui_page__desc`,但该符号在 UI 目录内除自身 `.c/.h` 外无人引用 | 页面没接进 `ui_content_page.c` 的 `s_defs[]` 描述符表:两端**编译链接全过**(无人引用的 desc 被链接器整段剥离),菜单永远打不开这一页 | + +## 2. 为什么要它 + +- 真事故:`App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c` 历史上**两处都没注册**, + 等于代码从不参与任何一端编译,而读者以为功能已生效。 +- 第 ④ 类尤其值:2026-10-15 实测,前三类对"页面写了但没接线"**完全照不出**(编译链接都过), + 所以才有"新增 UI 页面必须过本门"的机械口径。 + +## 3. 触发面 + +`files:` 覆盖 `\.c$|\.uvprojx$|CMakeLists\.txt$|^scripts/check_registration\.py$`; +`pass_filenames: false` → 每次全量对照两份注册表。 + +## 4. 报错怎么读 + +``` +双端注册反向校验 (uvprojx + CMakeLists).......Failed + [示例] [GAP-1] App/x/y.c 未出现在 MDK-ARM/STM32F407-Demo.uvprojx +``` + +- 单独跑:`& '.venv\Scripts\python.exe' scripts/check_registration.py --strict`。 +- 修法是**补注册**(新 `.c` 走 `new-source-file` skill 的两处登记),不是把文件删掉凑绿。 +- 新目录还要把目录加进 `uvprojx` 的 ``,否则报错会落在**消费者**编译单元上 + (`cannot open source input file`),容易被误判成别人的问题。 + +## 5. 好处 + +- 把"显式列表 = 必须登记两次"这条极易忘的约定机械化; +- 反向校验(③)让改名/删文件不留幽灵条目,配合 `scripts/rename_ref.py` 才敢说"改名安全"。 + +## 6. 限制与盲区 + +1. **只管"在不在列表里",不管"注册得对不对"**:把 `a.c` 的 `` 指向 `b.c`,只要路径 + 存在就过。 +2. MCU-only / 模拟器专用的判定靠**豁免清单**(脚本内 `KNOWN_GAPS` 一类,存量 + 逐条理由)。 + 新增一个正确的 MCU-only 文件需要同时改豁免清单,否则②会误报——**反过来也成立**:进了豁免 + 清单的文件就永久不再被检查。 +3. 第 ④ 类只覆盖 `ui_page_*.c` + `ui_page__desc` 这一种接线形态;其它"注册了但运行期 + 无人调用"的入口(命令表、事件回调)不在内。 +4. 头文件不校验:只漏 `.h` 不会报(漏的是 include 路径,不是注册表)。 +5. 它不看条件编译:文件在列表里但整个文件被 `#ifdef` 关掉,仍然算"已注册"。 + +## 7. 豁免与基线 + +- 无"只减不增"的基线,而是**逐条带理由的已知缺口清单**(`KNOWN_GAPS` 形态):新增条目必须写 + 理由,性质上比基线更容易被滥用,改动时要看得到理由文本。 + +## 8. 改这个门禁本身 + +- 加新的注册面(例如新增一套第三方构建脚本)要同步"正向 + 反向"两向检查,只查单向会漏幽灵注册; +- 放宽第 ② 类(更多豁免)前,先确认该文件真的不需要在 PC 上跑——模拟器不编译意味着该文件的 + 逻辑永远没有主机侧单测(见 `host-unit-tests`)。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(四类缺口与两条真实死代码案例) | diff --git a/docs/门禁说明/task-table.md b/docs/门禁说明/task-table.md new file mode 100644 index 00000000..07fdabe7 --- /dev/null +++ b/docs/门禁说明/task-table.md @@ -0,0 +1,68 @@ +# `task-table` — README / AGENTS 里的任务表是否与 `Src/freertos.c` 一致 + +> 一句话:任务名 / 栈字数 / 优先级这三列**不许手抄**,事实源是 `Src/freertos.c`。 + +- 归属:审查报告"文档与源码一致性"里的任务表档(`docs/问题记录/项目审查与优化建议报告.md`)。 +- 配置:`entry: python scripts/gen_task_table.py --check`、`pass_filenames: false`、 + `files: ^(README\.md|AGENTS\.md|Src/freertos\.c)$`。 + +## 1. 它在查什么 + +1. 解析 `Src/freertos.c` 里 `const osThreadAttr_t _attributes = { .name = …, + .stack_size = N * 4, .priority = (osPriority_t) osPriorityX }` 形式的块; +2. 把 `stack_size` 按 `N * 4` 换算回 **words**(CMSIS-RTOS2 的单位是字节,本仓约定写 + `words * 4`,所以注释里的数字就是字数); +3. 重新渲染被 `` / `:END` 包住的表格区,与文档现状比对,不一致即 FAIL。 + +## 2. 为什么要它 + +- 真事故:历史上两张表全靠手写,`Src/freertos.c` 改栈后表忘了同步——2026-09 实测两张表都比 + 源码落后一大截(表里写 2048/4096/4096/4096/16384/2048,实际是 1024…512)。 +- 为什么这件事在本仓特别危险:栈大小是 **IWDG 任务健康判定与栈水位核算的输入**,读者拿错的 + 表去推余量,会得出"还有余量"的错误结论(同一思路见 `mem-numbers`)。 + +## 3. 触发面 / 看不见什么 + +- 只校验**受管生成区**内的表;受管区外手写的任务叙述(散文里的"某某任务栈 640")不校验。 +- 只解析 `osThreadAttr_t` 常量块形式。用变量、宏或运行时拼出来的属性 → 解析不到,表里那一行会**消失**而不是报错(少一行比错一行更隐蔽)。 +- `Src/freertos.c` 是 CubeMX 生成文件(只允许写 `USER CODE` 区)→ 任务表本身由 CubeMX 生成, + 重生成后本门正好负责发现"栈被模板改回去了"。 + +## 4. 报错怎么读 + +``` +FreeRTOS 任务表一致性.......Failed + AGENTS.md: 任务 protectTask 栈 512 与源码 640 words 不一致 +``` + +- 同步:`& '.venv\Scripts\python.exe' scripts/gen_task_table.py --write`(只改受管区,两处文档一起改); + 只看期望值:`--diff`。 + +## 5. 好处 + +- 抬栈 / 加任务时,表格跟着源码走,评审看 diff 就知道动了哪个任务的栈; +- 与 `mem-budget`、`stack_report.py` 形成闭环:表里的字数是核算的输入,不再靠记忆。 + +## 6. 限制与盲区 + +1. 只保证"表 == 源码属性块",**不判断这个栈够不够**(够不够要靠 `scripts/stack_report.py` + 静态最坏链 + 串口 `[STACK]` 实测)。 +2. 只覆盖受管区;README/AGENTS 散文里的任务与栈叙述无人核对。 +3. 解析依赖固定形状(`N * 4`);换成宏或表达式就静默漏掉该行。 +4. 任务是否真的被创建、是否纳入看门狗 `required_mask`,不在本门范围(那是 `Src/freertos.c` + 与看门狗配置的语义问题)。 + +## 7. 豁免与基线 + +- 无基线:不一致即拦。合法动作只有 `--write`(把文档改成与源码一致)。 + +## 8. 改这个门禁本身 + +- 加新的属性字段(例如 `mpu_stack_size`)要同步解析器与表头,否则新字段静默丢失; +- 改列顺序会同时改两处文档,属于"生成物改动",提交里要写明。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(含 2026-09 两张表落后的实测数字) | diff --git a/docs/门禁说明/test-suite-switch.md b/docs/门禁说明/test-suite-switch.md new file mode 100644 index 00000000..3fb178cc --- /dev/null +++ b/docs/门禁说明/test-suite-switch.md @@ -0,0 +1,68 @@ +# `test-suite-switch` — 别让测试固件被静默提交 + +> 一句话:`test/board/test_config.h` 的 `TEST_SUITE_*` **全部注释 = 产品构建**;只要有开关是 +> 打开状态,提交的就是测试固件,本门直接拦。 + +- 配置:`entry: python scripts/check_test_suite.py`、`pass_filenames: false`、 + `files: ^test/board/test_config\.h$`。 + +## 1. 它在查什么 + +| 状态 | 判定 | +|---|---| +| 全部注释 | PASS(产品构建) | +| 启用一个或多个 | FAIL,列出套件名与修复方法 | +| 同时启用多个 | FAIL(编译期另有 `#error` 兜底,两道闸) | + +## 2. 为什么要它 + +- 真事故(2026-10-01 测试架构评审):该开关此前**没有任何门禁检查**,实测**已提交的 HEAD 里 + `TEST_SUITE_CH395F` 就是开启状态**,`.map` 含 34 处 test 代码——躺在分支上的是测试固件, + 而它看起来完全正常。纯靠人记得改回去,是迟早会漏的事。 + +## 3. 触发面 + +- 只在 `test/board/test_config.h` 变化时跑(`files` 精确到这一个文件)。 +- 测试构建本身是合法的(跑套件回归就是要开),所以放行通道是**显式声明**: + `--allow-test-build` 或环境变量 `ALLOW_TEST_BUILD=1`;显式放行后仍打印醒目 `[WARN]` 横幅。 + +## 4. 报错怎么读 + +``` +测试套件开关=产品构建.......Failed + [FAIL] 启用了 TEST_SUITE_GD5F —— 提交前请全部注释(产品构建) +``` + +- 单独跑:`& '.venv\Scripts\python.exe' scripts/check_test_suite.py`(`--file` 换目标、`--quiet` 只报失败)。 +- `run_all_suites.py`(`board-test-suite` skill)会自动改宏、无条件还原,跑完正常情况不会留下开关。 + +## 5. 好处 + +- 把"跑完回归记得还原"从纪律变成判定; +- 与破坏性开关的持久代价配合:`ENABLE_DESTRUCTIVE_TESTS` 一旦开过又忘了,本门是最后一道提醒。 + +## 6. 限制与盲区 + +1. **只看 `test_config.h` 一个文件**:如果有人把 `#define TEST_SUITE_GD5F` 写进别处(编译器 + `-D` 参数、`uvprojx` 的预处理定义、某个头文件),本门完全不知道。 +2. 放行通道 `ALLOW_TEST_BUILD=1` 不记录在提交里——环境变量放行**不留痕**,所以它适合本地临时 + 跑,不适合作为常规手段。 +3. 它不判断"产品构建是否真的没有测试代码"(`test/board/*.c` 可能仍在工程里,只是被宏关掉)。 +4. `ENABLE_DESTRUCTIVE_TESTS` 的开/关**不由本门判定**(那是破坏性代价,属 Ask-first 事项, + 见 `docs/测试说明/存储维护与容量恢复.md`)。 + +## 7. 豁免与基线 + +- 无基线。唯一的"放行"是显式声明本次就是测试固件;**不要用 `--no-verify` 绕过**(绕过之后 + 没有任何痕迹说明你提交了测试构建)。 + +## 8. 改这个门禁本身 + +- 新增套件宏要同时更新判定与互斥名单,否则新宏会被当成"没开"而放过; +- 若改成"允许提交测试构建但要求写进提交说明",需要先想清楚:那种提交怎么和真实产品构建区分。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(含"HEAD 里躺着测试固件"的实测背景) | diff --git a/docs/门禁说明/trailing-whitespace.md b/docs/门禁说明/trailing-whitespace.md new file mode 100644 index 00000000..077146fa --- /dev/null +++ b/docs/门禁说明/trailing-whitespace.md @@ -0,0 +1,56 @@ +# `trailing-whitespace` — 行尾空白(**改写型**) + +> 一句话:它不改内容语义,只删每行末尾的空格/制表符;**改的是工作区**,所以会"先失败一次、你再提交一次"。 + +- 配置:官方 `pre-commit-hooks` v5.0.0;`exclude: *cubemx-generated`(YAML 锚点,与 + `end-of-file-fixer` 共用同一段豁免清单)。 + +## 1. 它在查什么 + +- 每行行尾的空格与制表符;发现即就地删除,改动留在工作区(不自动进索引)。 + +## 2. 为什么要它 + +- 行尾空白在 diff 里表现为"整行被改",是评审噪声主要来源之一; +- 与本仓另一条铁律同源:`*.uvprojx` 每次构建被 UV4 改写、不入库——工具抖动不该进历史。 + 性质上属**预防性**(本仓没有因它出过事故,它的价值是让 diff 保持可信)。 + +## 3. 触发面与豁免 + +- 豁免(锚点 `cubemx-generated`):`Src/`、`Inc/` 的 `.c/.h`、`Drivers/CMSIS/`、HAL、 + `Middlewares/`、`MDK-ARM/` 下的工程 / 散列 / 映射文件、`.mxproject`、`.ioc`。 +- **理由**:这些文件的事实源是 `.ioc` 与工具链,每次重生成都按工具自己的风格写回(实测每次 + 重生成后 `Src/`、`Inc/` 下 10+ 个文件各被补行尾空白),整理它们只制造提交抖动。 +- ⚠ **只读检查不受这条豁免影响**:`cppcheck`、乱码审计、存活审计、任务表、内存数字仍覆盖 + `Src/`、`Inc/`(`cubemx-survival` 必须看到这些文件)。 + +## 4. 报错怎么读 + +``` +Trim Trailing Whitespace.................................Failed +Files were modified by this hook.(下面列出被改的文件) +``` + +- **这不是错误,是它的正常工作**:`git add` 被改的文件,再提交一次即可。 +- 想避免二次提交:提交前跑一次 `& '.venv\Scripts\python.exe' -m pre_commit run --all-files`。 + +## 5. 好处 + +- diff 干净;编辑器和工具链不会为"补不补空格"互相拉锯。 + +## 6. 限制与盲区 + +1. **豁免是整目录级**:`Src/`、`Inc/` 里**我们手写的 `USER CODE` 区**同样不被整理; +2. 只处理本次提交涉及的文件(`--all-files` 除外),历史里的行尾空白不动(这是优点也是盲区); +3. 需要保留行尾空白的语法会被误伤(Markdown 用两个空格换行的写法;本仓不用这种写法); +4. 缩进混用、行间空行数量它都不管(Tab 缩进由 `coding-standard` 的 CODE-RULE-08 管)。 + +## 7. 豁免与基线 / 8. 改这个门禁本身 + +- 无基线;豁免只有生成文件那一段锚点。**新增豁免目录前先自问:是不是只是今天不想整理**。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档 | diff --git a/docs/门禁说明/ui-golden.md b/docs/门禁说明/ui-golden.md new file mode 100644 index 00000000..0a16ec7c --- /dev/null +++ b/docs/门禁说明/ui-golden.md @@ -0,0 +1,84 @@ +# `ui-golden` — UI 像素回归(菜单 8 状态 + 内容页 11 页黄金参考) + +> 一句话:改 UI 源码时,把模拟器增量重编 + 两条 golden 回归串成一次提交门禁,**像素对不上就拦**。 +> 它是包装器:真正的判据在被包装的两条脚本与参考图里。 + +- 配置:`entry: python scripts/check_ui_golden.py`、`pass_filenames: false`、 + `files:` 覆盖 `Drivers/BSP/UI/`、`App/task/ui_task.(c|h)`、模拟器等**能改变像素的面**。 +- 包装器只做三件事:跑 `simulator/build_sim.bat` → 跑 `simulator/tests/check_menu_rows.py` → + 跑 `simulator/tests/check_content_pages.py`,透传输出与退出码。 + +## 1. 它在查什么 + +- 菜单侧:整棵菜单树的行渲染结果(8 个状态)与参考图逐像素比对; +- 内容页侧:11 个内容页的渲染结果与参考图比对; +- 参考图 `simulator/tests/ref/*.bin` **就是判据本身**。 + +## 2. 为什么要它(有牙的实测) + +- 两条 golden 脚本早就存在且一直人工跑,但**从未进自动门禁**——审查报告里自认的这个缺口, + 正是"**有基线 ≠ 有门禁**"的教科书案例。 +- **注入实验(证明它真的会咬人)**:给 `ui_page_dc.c` 的 `.top_y` 注入 +3 像素位移 → + 增量重编 → golden **FAIL 并点名** `[dc] FAIL 与参考页不一致(1366 px 差异)`、退出码 1; + 还原后 diff 干净。 +- 形态选型是**实测出来的**:改一个 UI 源文件后,增量重编 + 两条 golden 合计 2~4 秒 → 才决定 + 进自动门禁。项目原则写得很清楚:**若实测要几分钟,就应改为文档化的人工门禁——一个逼人 + `--no-verify` 的门禁等于没有门禁**。 + +## 3. 触发面 + +- ⚠ pre-commit 4.x 没有 `always: true`(实测对 `local` 钩子只告警并跳过)→ 想"总能跑到", + `files:` 必须**穷举所有能改变像素的入口**:UI 源码、推流任务、字体生成物、模拟器源码/配置。 +- 包装器**有意忽略** pre-commit 传进来的文件名参数:两条 golden 按"整棵树 + 全量参考图"工作, + 不接受文件参数(所以它必须 `pass_filenames: false`)。 +- 依赖本机可构建模拟器(需要 Visual Studio / CMake,见 `lvgl-simulator` skill)→ 换机第一次跑 + 可能因为构建环境缺失而 FAIL,那不是 UI 有问题。 + +## 4. 报错怎么读 + +``` +[ui-golden] 增量构建模拟器 ... +[ui-golden] 跑 simulator\tests\check_content_pages.py ... +[dc] FAIL 与参考页不一致(1366 px 差异) +``` + +- 先分清两类失败:**构建失败**("模拟器构建失败,无法做像素回归"→ 环境问题,先修构建)与 + **像素不一致**(真改了显示效果)。 +- 确认显示改动是**有意的**,才允许按 `ui-verify` skill 的流程更新参考图; + **任何时候都不得用刷新基线的方式让钩子变绿。** + +## 5. 好处 + +- 抓的是"看不出来的错位":白底白字、palette 头越界、位移 3 像素这类**编译与单测都不报**、 + 只有肉眼能发现的问题(两条实证坑见 `docs/UI设计/UI_设计架构.md` §1.4)。 +- 让"改 UI 不用手点一遍"成为可能,配合 `ui-verify` skill 是 UI 改动的标准回归路径。 + +## 6. 限制与盲区 + +1. **参考图即判据**:刷新参考图 = 修改判据。评审时必须盯 `simulator/tests/ref/` 的 diff; + 一旦"顺手更新",此后所有回归都只在和错的基线比。**门禁无法阻止这件事**,只能靠评审。 +2. 只覆盖 8 个菜单状态 + 11 个内容页:未列入的状态(密码错误态、超长文本、缺字渲染、 + 无 SD 卡 / 挂载失败的降级显示)**不会有任何回归**。 +3. 跑的是**模拟器那份配置**,不是固件那份 —— 两份 `lv_conf.h` 漂移时,回归结论不能外推到固件 + (所以 `lv-conf-sync` 是本门有效性的前提)。 +4. 不覆盖真机显示:面板时序、灰度、对比度、SPI 刷屏速率一概不看;像素一致 ≠ 上机正确。 +5. 动态内容(时间戳、日志页、SOE 记录)若被纳入参考图,会因数据变化误报;因此这类页面通常 + 在稳定数据下生成参考图 —— **数据源一变(例如月份进位)就可能假阳性**。 +6. 与 `font-charset` 互补:那门管"有没有字形",本门管"像素对不对";两边都绿仍可能有布局问题 + 不在黄金参考集内。 + +## 7. 豁免与基线 + +- 基线 = 参考图二进制;更新流程属 `ui-verify` skill 的一步,且**必须与引起变化的 UI 改动同批提交**, + 提交说明里写清"为什么像素变了"。 + +## 8. 改这个门禁本身 + +- 加新页面 / 新状态时,要同时扩参考图集合,否则新页面**完全没有回归**(不会报错,只是没覆盖); +- 触发面 `files:` 每加一类能改像素的入口就补一行,漏一类 = 那一类改动静默不跑回归。 + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-10-23 | 首篇逐项文档(含 +3 像素注入实验与"有基线 ≠ 有门禁"的缺口来源) |