# pre-commit 自动化代码审查配置 # 通用机制(阶段 / stash 陷阱 / 触发面 / 增量拦截三段式)→ docs/代码规范/pre-commit使用说明.md §2 # 每个门禁一篇详解(含限制与盲区)→ docs/门禁说明/README.md # 加/删一条钩子:同一笔提交里改 AGENTS.md 的「共 N 个钩子」与本文件注释;口径 = YAML 解析后的 hook 条目数, # 自查命令与「声明 / 文档 / 实跑」三数同源核对 → docs/门禁说明/README.md §2(直接数文本行会把下面注释掉的候选条目算进去) default_install_hook_types: [pre-commit, commit-msg] # 顶层 exclude 只挡**第三方与生成物**。 # ⚠ 2026-10-11 修正:原先在这里整目录挡 `simulator/` 是错的——`simulator/` 下 21 个 # tracked 文件**全是本项目自研代码**(模拟器入口 main.c、平台后端 sim/*.c、回归脚本 # tests/*.py、两份配置与 README)。整目录豁免 = 自研代码落在所有钩子之外(连 # lv-conf-sync 都拦不到);实测把 simulator/ 纳入 cppcheck 后,立刻揪出 # sim_syslog.c 一处真实的空指针解引用。真正的生成物/第三方(`simulator/build/`、 # `simulator/SDL2/`、`simulator/shot*.{bin,png}`、`__pycache__/`)**已在 .gitignore 里**, # 不会进 git 索引 → 无需在此重复豁免,故直接删掉该条。 # ⚠ 例外(2026-10-22):`simulator/tests/ref/*.bin`(ui-golden 的像素黄金参考)已从 # 「整目录 gitignore」改为**入库**——基线不入库时任何新克隆 / 新工作树都必然报 # 「缺黄金参考」,这条门禁就只在有老副本的机器上真实存在。入库前提是实测可复现: # 两份各自独立构建的 lvgl_sim(不同构建目录、相隔 11 天的代码)对同一批 11 个 .bin # SHA-256 全部一致。改写型钩子(trailing-whitespace / end-of-file-fixer)声明 # `types: [text]`,位图含 NUL → 被判二进制自动 Skipped(实测「索引 blob == 磁盘 == # 参照」三方一致),无需为它另加 exclude。 exclude: | (?x)^( \.opencode/node_modules/| Lib/lvgl/| MDK-ARM/RTE/ ) repos: # 1) 通用钩子(官方) - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace # 豁免 **CubeMX / Keil 生成的文件**(锚名 cubemx-generated,end-of-file-fixer 复用): # 这类文件的内容事实源是 `.ioc` 与工具链,每次重生成都按工具自己的风格写回 # (行尾空白、文件末尾不带换行)。2026-10-09 实测:每次重生成后 `Src/`/`Inc/` # 下 10+ 个文件各被补 1 行末尾空行,纯属提交抖动。 # ⚠ 只豁免**改写型**钩子(trailing-whitespace / end-of-file-fixer); # cppcheck / 乱码 / 存活审计 / 任务表 / 内存数字等**只读检查**仍覆盖 # `Src/`、`Inc/`——`cubemx-survival` 必须能看到这些文件。 exclude: &cubemx-generated | (?x)^( Src/.*\.(c|h)$| Inc/.*\.(c|h)$| Drivers/CMSIS/| Drivers/STM32F4xx_HAL_Driver/| Middlewares/| MDK-ARM/.*\.(uvprojx|uvoptx|sct|map)$| \.mxproject$| .*\.ioc$ ) - id: end-of-file-fixer exclude: *cubemx-generated - id: check-added-large-files - id: check-merge-conflict - id: check-yaml # 2) C 代码格式:只检查,绝不改写(编码铁律) # Route A:暂不启用,待全库格式化基线后再放开 # - repo: https://github.com/pre-commit/mirrors-clang-format # rev: v19.1.4 # hooks: # - id: clang-format # args: [--dry-run, --Werror] # 3) commit message 格式(宽松规则,兼容中文标题) - repo: https://github.com/jorisroovers/gitlint rev: v0.19.1 hooks: - id: gitlint # 4) 本地自定义钩子(仓库级) - repo: local hooks: - id: cppcheck name: cppcheck # ⚠ stages: [manual](2026-10-28 用户裁定):全库扫描实测 ~20 s,不压在提交期; # 由**远端 CI** 强制(.gitea/workflows/ci.yml 的「手动阶段门禁(覆盖率总闸 + 全量 cppcheck)」步), # 本地的等价入口 = scripts/ci_local.py(manual 段)或 # python -m pre_commit run --all-files --hook-stage manual cppcheck stages: [manual] entry: cppcheck # 注意:YAML 流序列中逗号是元素分隔符,含逗号/空格的参数必须加引号 args: ['--enable=warning,performance,portability,style', '--error-exitcode=1', '--inline-suppr', '--std=c99', '--quiet', '--suppress=missingIncludeSystem', '--suppress=missingInclude', '--suppress=invalidPrintfArgType_sint', '--suppress=invalidPrintfArgType_uint', '--suppress=unusedFunction', '--suppress=unusedStructMember', '--suppress=noExplicitConstructor', '--suppress=useStlAlgorithm', '--suppress=constParameterPointer', '--suppress=constParameterCallback', '--suppress=redundantAssignment', '--suppress=normalCheckLevelMaxBranches', '-I', 'Inc', '-I', 'App', '-I', 'Drivers/BSP', '-I', 'Drivers/STM32F4xx_HAL_Driver/Inc', '-I', 'Drivers/CMSIS/Include', '-I', 'Middlewares/Third_Party/FreeRTOS/Source/include'] language: system types: [c] # 仅扫描项目自有代码;排除第三方库与 LVGL 依赖文件 # (UI 生成文件 ui_font_cn_12/ui_menu_tab 由生成器按工程约定产出,纳入受检) exclude: | (?x)^( Drivers/CMSIS/| Drivers/STM32F4xx_HAL_Driver/| Drivers/BSP/NET/lftpd/| Drivers/BSP/dhara/| Lib/| Middlewares/| MDK-ARM/| test/ ) # 4b) test/ 代码宽松静态检查(S5,2026-10-03 加) # 背景:`test/` 此前完全豁免 pre-commit(靠"自律"),而 `test/board/` 下 # 6300+ 行 C 代码**且编入固件**(曾出现 `s_rx_buf` 使用先于声明)。 # 本钩子**只开 warning**(不引入存量 style 噪音),排除模拟器桩 # (`test/host/*/stub/`,第三方/桩头形态不受控)。 # 基线:2026-10-03 全量实测 **0 条 warning**(存量可控,故直接挂钩子)。 # 依据:docs/审查记录/测试与架构改进建议.md §3.4;实机测试实施指南.md §8.1 - id: test-cppcheck name: cppcheck (test/, warning only) # ⚠ stages: [manual](2026-10-28,同 cppcheck):实测 ~38 s;由远端 CI 与 # scripts/ci_local.py 的 manual 段跑,提交期不跑。 stages: [manual] entry: cppcheck args: ['--enable=warning', '--error-exitcode=1', '--inline-suppr', '--std=c99', '--quiet', '--suppress=missingIncludeSystem', '--suppress=missingInclude', '--suppress=invalidPrintfArgType_sint', '--suppress=invalidPrintfArgType_uint', '--suppress=unusedFunction', '--suppress=unusedStructMember', '--suppress=constParameterPointer', '--suppress=constParameterCallback', '--suppress=redundantAssignment', '--suppress=normalCheckLevelMaxBranches', '-i', 'test/host/watchdog/stub', '-i', 'test/host/cfg_file/stub', '-i', 'test/host/sys_clock/stub', '-I', 'Inc', '-I', 'App', '-I', 'Drivers/BSP', '-I', 'Drivers/STM32F4xx_HAL_Driver/Inc', '-I', 'Drivers/CMSIS/Include', '-I', 'Middlewares/Third_Party/FreeRTOS/Source/include', 'test/'] language: system # 触发条件:test/ 下 C/头文件变动;扫描范围恒为整个 test/(pass_filenames=false) files: ^test/.*\.(c|h)$ pass_filenames: false - id: no-mojibake name: Forbid U+FFFD mojibake entry: python scripts/check_mojibake.py language: system types: [c] - id: coding-standard name: C coding standard (规范V1.0) entry: python scripts/check_coding_standard.py language: system types: [c] # 范围与 cppcheck 钩子一致;另排除 CubeMX 生成文件(Src/、Inc/,重新生成即覆盖)、 # dhara/FatFS 接口适配层(nand_ftl,函数名由外部 API 固定) exclude: | (?x)^( Drivers/CMSIS/| Drivers/STM32F4xx_HAL_Driver/| Drivers/BSP/NET/lftpd/| Drivers/BSP/GD5F2GQ5UE/nand_ftl\.| Drivers/BSP/dhara/| Lib/| Middlewares/| MDK-ARM/| test/| Src/| Inc/ ) # 5) 文档一致性门禁 # 存在性:md 互链 / 反引号路径是否指向真实文件(防迁移/删除后残留死链) # 章节:`§N.M` 必须能在目标文档的编号标题里找到(--sections-strict:自引用失效也拦) - id: doc-links name: 文档链接存在性 entry: python scripts/check_doc_links.py --sections-strict language: system files: \.md$ # 外部资料(芯片手册转录)链接形态不受控;第三方目录整体排除 # (`simulator/` 已不再排除:其下 3 个 README 是本项目文档,2026-10-11) exclude: | (?x)^( \.opencode/node_modules/| Lib/lvgl/| MDK-ARM/RTE/| docs/芯片手册/ ) # mermaid 图:围栏内确定性语法预检(第 52 条,2026-10-30 加,口径 51 → 52,提交期实跑 48 → 49)。 # 为什么要有:屏板链路设计说明.md §1 的 flowchart 里 `-.|"文字"|`(`-.` 直接跟管道,不是任何合法 # 箭头 token)让 mermaid 渲染 `Syntax error in text`——该图 2026-10-29 落库、期间 doc-links 等 # 全绿:文档门禁查链接与章节,**从不查围栏内图表语法**,这是它们的共同盲区。 # v1 只查「错了必渲染失败」三条(不重造解析器):R1 缺/错图类型头;R2 `-.` 后直接跟 `|` # (带文字点线合法形态只有 `-.->|t|` 与 `-. 文字 .->`,selftest 证明两种合法形态不误伤); # R3 flowchart 的 subgraph 与独占行 `end` 计数配对、sequence 的块起点与 `end` 计数配对。 # 存量基线:接线当日 --all 实测 29 个自有文件 60 块围栏、0 命中 ⇒ **空基线零语料**(还清类 # 先例:id-check-new);docs/芯片手册/ 是导入转录、围栏不受控,与 doc-links 同排除面不进语料。 # 自证:--selftest 五条(正例含三种合法点线 / 本坑形态必响 / 缺 end 必响 / 假头必响且 # sequence alt-else-end 不误伤 / 基线吞得下且清空后同命中算新发)。 - id: mermaid-syntax name: "mermaid 围栏语法预检(图类型头 / 非法点线管道 / 块配对)" entry: python scripts/check_mermaid.py language: system files: \.md$ exclude: | (?x)^( \.opencode/node_modules/| Lib/lvgl/| MDK-ARM/RTE/| docs/芯片手册/ ) # 数值:README/AGENTS 任务表须与 Src/freertos.c 一致 - id: task-table name: FreeRTOS 任务表一致性 entry: python scripts/gen_task_table.py --check language: system files: ^(README\.md|AGENTS\.md|Src/freertos\.c)$ pass_filenames: false # 体积:内存/Flash 数字须与最新 .map 一致(体积档) # 只在内存文档或代码变动时触发;.map 未生成时脚本自身给出明确提示 - id: mem-numbers name: 内存/Flash 数字一致性(体积档) entry: python scripts/check_mem_numbers.py --scan language: system files: ^(docs/内存管理/.*\.md|scripts/check_mem_numbers\.py|App/.*\.c|App/.*\.h|Src/.*\.c|Inc/.*\.h)$ pass_filenames: false # 5d) 两份 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 # 默认值隐式生效 → 固件白链 **64,576 B 死代码**,且模拟器 golden 回归跑的是 # 另一套特性子集(等于验证不到固件实际编译的配置)。 # 口径:比对**生效值**(文件未定义则取 lv_conf_internal.h 的兜底默认), # 并抓"写了 LVGL 不认的宏名"(`LV_USE_THEME_BASIC` 同类,实测两端共 26 处) # 与同文件重复定义。规则正文见 docs/UI设计/UI_设计架构.md §1.4 第 10 条。 # ⚠ 触发范围(2026-10-11 两次修正):本钩子由**两端任一配置**或脚本自身触发。 # 原先顶层 exclude 整目录挡掉 `simulator/`,导致"只改 simulator/lv_conf.h" # 的提交拦不到;已删该条(见文件顶部 exclude 注释),故这里可直接匹配 # `simulator/lv_conf.h`。(pre-commit 4.x 已移除 `always: true`, # 实测对 local 钩子只告警并跳过——不能靠它兜底。) - id: lv-conf-sync name: 两份 lv_conf.h 特性子集一致性 entry: python scripts/check_lv_conf_sync.py language: system files: ^(Lib/lv_conf\.h|simulator/lv_conf\.h|scripts/check_lv_conf_sync\.py)$ pass_filenames: false # 6) CubeMX 重生成存活审计(2026-09-29 加) # 背景:.ioc 的 KeepUserCode 只保护 USER CODE 标记区;标记之外的手工改动 # 重生成即被覆盖,且多数不报编译错(崩溃采集被打回空 while(1)、加固宏被删)。 # 本钩子验证"重生成易失清单"各项是否仍在预期位置,并对关键区做哈希漂移检测。 # 范围:仅在这些易失文件变动时触发(改动 .ioc 或生成文件本身就值得复核)。 - id: cubemx-survival name: CubeMX 重生成存活审计 entry: python scripts/check_cubemx_survival.py --check language: system files: ^(STM32F407-Demo\.ioc|Src/.*\.(c|h)|Inc/.*\.h|App/util/task_watchdog\.(c|h)|MDK-ARM/.*\.uvprojx|scripts/check_cubemx_survival\.py)$ pass_filenames: false # 7) 看门狗判定状态机主机侧单元测试(2026-10-01 加) # 背景:task_watchdog.c 的判定逻辑(位图取走清零 / 掩码 / miss_streak # 累计 / 100ms 节流 / tick 回绕)是纯逻辑,板端套件无法穷举边界;本钩子 # 用 MSVC 离线编译真实源文件(#include 纳入)并用可观测桩驱动,秒级、零 # 硬件、不占 MCU RAM。仅在被测文件或其平台层 / 测试变动时触发。 # 详见 docs/测试说明/单元测试/看门狗单元测试说明.md - id: watchdog-unit-test name: 看门狗判定状态机单测(主机侧) entry: python test/host/watchdog/run_unit_test.py language: system files: ^(App/util/task_watchdog\.(c|h)|test/host/watchdog/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7b) 系统时钟时间换算主机侧单元测试(2026-10-02 加) # 背景:sys_clock.c 的 Unix↔日历换算(闰年 / 月末 / 世纪 / 2038 上限)、 # 星期(Zeller)、"30 天惰性再基准化"(防 49.7 天 tick 回绕)、RTC 故障 # 降级路径均为纯逻辑;本钩子用 MSVC 离线编译真实源文件(#include 纳入) # 并以可注入桩驱动(可控时钟 + 阴影 RTC)。首次落地即抓出 Zeller 公式 # 缺陷(1970~2099 全部日期星期皆错,详见测试说明文档)。 # files 含 sys_clock.c 依赖的 sd2506.h(时间结构/返回值定义)。 - id: sys-clock-unit-test name: 系统时钟换算单测(主机侧) entry: python test/host/sys_clock/run_unit_test.py language: system files: ^(App/util/sys_clock\.(c|h)|Drivers/BSP/SD2506/sd2506\.h|test/host/sys_clock/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7c) 配置引擎主机侧单元测试(2026-10-02 加,S1-b) # 背景:cfg_file.c 的核心是"文本 <-> 结构"纯变换(INI 段/键解析、四种值 # 类型编解码、外科手术式保存、超长守卫、load/save/poll 契约);板端套件只 # 保存到缺失/新文件(走 s_gen_document 路径),**从不触发对已存在文件的外科 # 手术改写**,故掩盖了该路径的缺陷。本钩子用 MSVC 离线编译真实源文件 # (#include 纳入)+ 内存虚拟文件系统后端驱动,首次落地即抓出 P0: # `s_encode(...) == 0` 恒假(其成功返回文本长度而非 0)使**外科手术保存的 # 受管值改写路径沦为死码**——对已存在 /config.ini 保存时受管键从不改写(静默 # 不落盘,且 net_cfg_save 仍报 OK)。详见测试说明文档。 - id: cfg-file-unit-test name: 配置引擎单测(主机侧) entry: python test/host/cfg_file/run_unit_test.py language: system files: ^(App/cfg/cfg_file\.(c|h)|App/cfg/cfg_file_store\.h|App/log/sys_log\.h|test/host/cfg_file/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7d) 相量层(全周傅氏相量)主机侧单元测试(2026-10-10 加) # 背景:App/algo/ 为**纯算法**层(无寄存器、无 RTOS、无外设依赖),是本项目 # 首个可完全离线验证的算法层。板端**无信号源**,无法注入已知幅值/相位/频率的 # 标准波形,故"读数 = 设定值"这一核心判据在板上不可验;本钩子用 MSVC 离线编译 # 真实源文件(#include 纳入,无需桩)+ 合成任意波形,把系数表、幅值/相位、 # 直流与谐波抑制、频偏敏感度、滑动窗、双区交换一次性钉死。 # files 含被测的两个源与其头,以及 harness 本身。 - id: phasor-unit-test name: 相量层单测(主机侧) entry: python test/host/phasor/run_unit_test.py language: system files: ^(App/algo/(phasor|sample_ring)\.(c|h)|test/host/phasor/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7e) ADC 输入层(语义取数与有效性)主机侧单元测试(2026-10-10 加) # 背景:adc_input.c 的核心是"通道号 <-> Rank 顺序 <-> 语义"三者映射的正确性; # 板端无信号源,无法注入已知电压验证"读到的是哪只脚"(映射说明 §8)。本钩子用 # MSVC 离线编译真实源文件(#include 纳入)+ HAL/TPAFE 可注入桩:给每个 Rank # 注入互不相同的码值以**逐通道钉死映射**,并一次钉死 HAL 启动失败 / 转换超时 / # 参数错等真机难复现的分支。 # files 含被测源与其头、两个桩头所在目录,以及 harness 本身。 - id: adc-input-unit-test name: ADC 输入层单测(主机侧) entry: python test/host/adc_input/run_unit_test.py language: system files: ^(App/algo/adc_input\.(c|h)|test/host/adc_input/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7f) 卷锁(vol_lock)主机侧单元测试(2026-10-10 加) # 背景:vol_lock 的**核心不变量是 fail-open**——互斥量创建失败时 # `vol_lock_wait()` 必须**返回 1 且绝不阻塞**,因为它的调用者 defaultTask 是 # 看门狗喂狗点,一旦在此阻塞即整机复位。板端该路径**只在内存耗尽时才可能走到**, # 无法人为复现;本钩子用 MSVC 离线编译真实源文件(#include 纳入)+ CMSIS 互斥桩, # 把"创建失败 -> 直接放行"、"创建成功 -> 正常加解锁"、"未持锁释放"、 # 超时参数透传等分支一次性钉死。 # files 含被测源与其头,以及 harness 本身。 - id: vol-lock-unit-test name: 卷锁单测(主机侧) entry: python test/host/vol_lock/run_unit_test.py language: system files: ^(App/fs/vol_lock\.(c|h)|test/host/vol_lock/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7f2) 卷空闲同步(写回缓存空闲 flush)主机侧单元测试(2026-10-25 加,道F 批 2) # 背景:该模块是写回缓存掉电窗口的兜底网——掉电与"脏页驻留"本身台架 # 不可按需构造;卷忙跳过 / ioctl 失败边沿 latch / 32 位 tick 回绕 / # 失败路径成对放锁(不得空放)全是纯状态逻辑。本钩子用 MSVC 离线编译 # 真实源文件(#include 纳入;桩:可控时钟 + 事件位图 + 卷忙注入 + # 可注入 disk_ioctl),把四级门与 latch 分支一次钉死。 # files 含被测源与其头、就绪位属主 sys_events 的消费面 default_task.c、 # 以及 harness 本身。 - id: vol-idle-sync-unit-test name: 卷空闲同步单测(主机侧) entry: python test/host/vol_idle_sync/run_unit_test.py language: system files: ^(App/fs/vol_idle_sync\.(c|h)|App/task/default_task\.c|test/host/vol_idle_sync/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7g) 时间同步(time_sync)主机侧单元测试(2026-10-10 加) # 背景:time_sync.c 是 TCP 时间同步协议状态机,也是探针窗口心跳缺口那项看门狗风险的**全部 # 逻辑贡献者**——"listen 重建 -> 800ms~1s 心跳间隔"由本模块的分片延时/退避/ # 结算等待组成,板端只能看到"是否复位"这一结果,看不到各段延时是否如设计。本钩子 # 用 MSVC 离线编译真实源文件(#include 纳入)+ 可脚本化的网络/事件桩(含 longjmp # 跳出 for(;;) 任务循环),把分片喂狗节奏、监听退避、报文重组(半包/粘连/EOF/真错/ # 预算耗尽/tick 回绕)、重听门控逐条钉死。 # files 含被测源与其头、被测的真实看门狗头,以及 harness 本身。 - id: time-sync-unit-test name: 时间同步单测(主机侧) entry: python test/host/time_sync/run_unit_test.py language: system files: ^(App/sync/time_sync\.(c|h)|App/util/task_watchdog\.h|test/host/time_sync/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7h) ADC 采样有效性校验主机侧单元测试(2026-10-11 加) # 背景:adc_health.c 是"静默/丢帧/序号倒退/错误计数增量"的判定层——这些 # 故障在健康台架上**不可复现**(复现 = 拔芯片/改驱动),板端闭环只能证明 # "健康时不误报",证不了"故障时必报";100ms 门限、tick 回绕、事件边沿 # 语义等边界只有合成计数序列能精确构造。本钩子用 MSVC 离线编译真实源文件 # (#include 纳入,纯逻辑零桩)把 11 组判据一次性钉死。 # files 含被测源与其头、adc_task.c(告警编排的消费方),以及 harness 本身。 - id: adc-health-unit-test name: ADC 有效性校验单测(主机侧) entry: python test/host/adc_health/run_unit_test.py language: system files: ^(App/algo/adc_health\.(c|h)|App/task/adc_task\.c|test/host/adc_health/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7h2) 通道标定层(标定折算,「数据可信」标定折算与落盘整定)主机侧单元测试(2026-10-25 加,道A) # 背景:标定折算是"码值→工程量"的唯一插入点,Q16 系数/零点两段落盘 + # 合法带哨兵回退 + 两点标定公式——板端无标准源、坏文件注入路径在真机 # 上难以复现,只有离线端能逐系数钉死。钩子用 MSVC 编译真实源文件 # (#include 纳入;桩:dbg_log / cfg_file_register / adc_input_desc 强符号)。 # files 含被测源与头、系数口径联动的 adc_health/cfg_file/adc_task 消费方, # 以及 harness 本身。 - id: adc-cal-unit-test name: 通道标定层单测(主机侧) entry: python test/host/adc_cal/run_unit_test.py language: system files: ^(App/algo/adc_cal\.(c|h)|App/algo/adc_health\.(c|h)|App/cfg/cfg_file\.(c|h)|App/task/adc_task\.c|test/host/adc_cal/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7h3) 量纲回归(「数据可信」"不该动作时不动作"用例,道A)主机侧单元测试(2026-10-25 加) # 背景:台架曾长期缺"不该动作时不动作"的判据,码值/工程量错配曾以 # 恒假动作的形态混过验收。本钩子用真实链(adc_cal→ring→phasor→ # prot_elm)+ 显式换算的合成波把七类元件的量纲钉死:折算点被旁路 # 或量纲再错位,断言立刻翻转。files 覆盖链上全部生产源与两处编排。 - id: prot-dim-unit-test name: 量纲回归单测(主机侧) entry: python test/host/prot_dim/run_unit_test.py language: system files: ^(App/algo/(adc_cal|prot_elm|prot_set|phasor|sample_ring|freq)\.(c|h)|App/task/(adc_task|protect_task)\.c|test/host/prot_dim/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7h4) 遥测快照层(全周傅氏相量发布侧,「数据可信」遥测快照发布,道D)主机侧单元测试(2026-10-25 加) # 背景:遥测快照发布项的 DoD 一半是并发与结构类判据——双区切换无撕裂(撕裂窗 # 200 ms 量级,台架人工比对观测不到)、seq 只增、§4.5 布局与字段偏移 # (104 与 UI 直接按整体拷贝消费,破布局 = 破协议)、CLIPPED 品质汇总 # (削顶位图要等两点标定才真实出现)。主机侧以 #include 原样纳入 # meas/phasor/sample_ring 生产源,合成 9 通道帧序列一次性钉死; # 读数与注入源比对类判据仍归台架(实测采样类)。files 覆盖被测源、 # 同链核(phasor/sample_ring)、编排方 adc_task 与 harness 本身。 - id: meas-unit-test name: 遥测快照层单测(主机侧) entry: python test/host/meas/run_unit_test.py language: system files: ^(App/algo/meas\.(c|h)|App/algo/(phasor|sample_ring)\.(c|h)|App/task/adc_task\.c|test/host/meas/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7h5) 运行参数段(「数据可信」道D 交付项)主机侧单元测试(2026-10-25 加) # 死区把守突发上报的量、104 整定段是现场唯一远程写入口:注册契约、 # 分档量程守卫、两段式、系数直通的合法带与生效值回显必须离线钉死。 - id: run-par-unit-test name: 运行参数段单测(主机侧) entry: python test/host/run_par/run_unit_test.py language: system files: ^(App/algo/run_par\.(c|h)|App/algo/adc_cal\.(c|h)|App/cfg/cfg_file(_store)?\.(c|h)|test/host/run_par/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7i) 保护定值模型(定值校验和载体)主机侧单元测试(2026-10-11 加) # 定值是保护动作的"输入宪法":范围校验、整表装载 all-or-nothing、 # 校验和封存-自检-闭锁(参考 tagPRS dwTagSum/dwTagSumN 模式)必须在 # 任何元件接线之前钉死——板端无法穷举越界定值注入,主机侧毫秒级全跑。 - id: prot-set-unit-test name: 保护定值模型单测(主机侧) entry: python test/host/prot_set/run_unit_test.py language: system files: ^(App/algo/prot_set\.(c|h)|test/host/prot_set/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7j) 保护元件框架(保护判据 P1 子集)主机侧单元测试(2026-10-11 加) # 判据核(滞回/定时限/欠量方向/闭锁联动/时基回绕)是纯逻辑, # 板端复现需要标准源 + 故障注入,主机侧用合成测量序列即可穷举 # 1.2×/0.9× 验收口径(规划 §8)。 - id: prot-elm-unit-test name: 保护元件框架单测(主机侧) entry: python test/host/prot_elm/run_unit_test.py language: system files: ^(App/algo/prot_elm\.(c|h)|App/algo/prot_set\.(c|h)|App/algo/phasor\.(c|h)|App/algo/freq\.(c|h)|test/host/prot_elm/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7j2) 频率测量层(测频与遥测快照,B8)主机侧单元测试(2026-10-12 加) # 自适应窗测频(K_eff 对齐负序泄漏纹波周期)的 J1~J6 判据 # (扫频/噪声/收敛/阶跃/浮空 valid=0/钳制恢复)是纯合成信号 # 可穷举的精度契约;板端无标准源照不出来(规划 §3.3 过频 / 低频)。 - id: freq-unit-test name: 频率测量单测(主机侧) entry: python test/host/freq/run_unit_test.py language: system files: ^(App/algo/freq\.(c|h)|App/algo/phasor\.(c|h)|App/algo/sample_ring\.(c|h)|test/host/freq/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7k) SHA-256 主机侧单元测试(2026-10-11 加,FTP 鉴权) # sha256 是 FTP pwd_hash 校验的地基——哈希算错是"正确口令被拒 / # 错误口令放行"的静默语义错误,板端只见 530/230 看不出根因; # FIPS 180-4 标准向量(空串/abc/56B 补位跨界/1MB)+ hex_eq 非法输入 # 在主机侧毫秒级钉死。零平台依赖,无桩。 - id: sha256-unit-test name: SHA-256 单测(主机侧) entry: python test/host/sha256/run_unit_test.py language: system files: ^(App/util/sha256\.(c|h)|test/host/sha256/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7l) FTP 鉴权状态机主机侧单元测试(2026-10-11 加,FTP 鉴权) # 锁定 60s/解锁/成功清零/tick 32 位回绕是"防口令爆破"契约核心, # 板端每验一条要真等 60s、回绕要跑满 49.7 天;lftpd_auth 纯函数 # 设计(tick 参数注入)就是为了让这些分支主机侧毫秒级穷举。 - id: ftp-auth-unit-test name: FTP 鉴权状态机单测(主机侧) entry: python test/host/ftp_auth/run_unit_test.py language: system files: ^(Drivers/BSP/NET/lftpd/lftpd_auth\.(c|h)|App/util/sha256\.(c|h)|test/host/ftp_auth/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7m) IEC 60870-5-104 被控站协议栈主机侧单元测试(2026-10-12 加,B4) # APCI 重组/STARTDT/总召分片/COV/k-w-t1~t3/遥控定值两段式/C_CS/错误 # 应答——link 层 tick 注入 + pts 数据源 vtable 抽象就是为主机侧纯逻辑单测全离线 # 穷举设计(分层约束);板端复现这些分支要真主站+真等待。 # 触发面含数据源依赖(prot_set/prot_elm/phasor/sample_ring)——测试 # 直接 #include 这些生产源,签名变了要立刻在这里暴露。 - id: iec104-unit-test name: IEC104 协议栈单测(主机侧) entry: python test/host/iec104/run_unit_test.py language: system # 运行参数段落地起本 TU 链入真实 cfg_file/adc_cal/run_par(板端真源链),触发面同步扩 files: ^(App/protocol/iec104/iec104_(asdu|pts|link)\.(c|h)|App/algo/(prot_set|prot_elm|phasor|sample_ring|adc_input|freq|meas|adc_cal|run_par)\.(c|h)|App/cfg/cfg_file(_store)?\.(c|h)|test/host/iec104/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7m2) SOE 动作记录编解码主机侧单元测试(2026-10-12 加,B9) # 64 B 定长记录(类型/seq/CP56Time2a/epoch/校验和)是磁盘上的 # 字节级契约——编解码错了要 FTP 下载+解码才看得见;纯 codec # 零平台依赖(soe_store 胶水 SIM_EXEMPT),主机侧穷举往返/ # 截断/坏校验/IV 位/seq 连续性(规划 §3.4 出口与记录)。 - id: soe-unit-test name: SOE 动作记录编解码单测(主机侧) entry: python test/host/soe/run_unit_test.py language: system files: ^(App/log/soe_rec\.(c|h)|App/log/soe_store\.(c|h)|test/host/soe/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7n) dhara NAND 移植层(nand_ftl)主机侧单元测试(2026-10-15 加) # 背景:移植层六个 HAL 入口是**纯地址算术**,此前对越界页/块号(含 # DHARA_PAGE_NONE 哨兵)**不做任何上界校验**——加偏移送进 SPI 命令后被地址位 # 截断成某个真实页 → 静默读写/搬走无辜数据;而 is_free 旧实现只看页首 64 B, # 会把"页首 0xFF、后段有数据"的已编程页误判空闲 → dhara 往已编程页再编程 # = 数据丢失。两类缺陷真机**极难复现**(要恰好撞上特定页形态 / 被污染的 meta), # 板端只能表现为"某次复位后丢数据",无从定位。 # 本钩子用 MSVC 离线编译真实 nand_ftl.c(#include 纳入)+ **RAM 假芯片** # (32 块 x 64 页 x 2048 B):把"双编程"做成可数事件、记录全部 # 访问次数,于是"越界不产生任何芯片访问""越界标坏块次数为 0"可机械断言; # 并内联一份"只看页首 64 B"的旧算法做对照,证明这组断言真能咬人。 # 触发面含 dhara 本体(测试一并 #include map.c/journal.c 跑真链路场景)。 - id: nand-port-unit-test name: dhara NAND 移植层单测(主机侧) entry: python test/host/nand_port/run_unit_test.py language: system files: ^(Drivers/BSP/GD5F2GQ5UE/nand_ftl\.(c|h)|Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue\.h|Lib/dhara/(map|journal|nand|error|bytes)\.(c|h)|test/host/nand_port/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7z) UI 数据快照层(道E task-14)主机侧单元测试(2026-10-25 加) # 背景:ui_data 是"页面看到的整个世界",但 ui-golden 只覆盖已接线的页面—— # 遥信 27 点、定值 40 项、遥测未接线的通道错了不会让任何参考图变红。本钩子 # 用 MSVC 离线编译真实 ui_data.c 与 ui_w_soe.c(#include 纳入,零桩),把两件事 # 钉死:① 快照存储语义(首帧前读 -1 / 序号由本层盖章 / 三通道互不串台 / # NULL 防御);② 跨层不透明下标与显示名映射的**现场一致性**——测试单元在同一 # TU 里同时 include BSP 侧与 App/algo、App/log 真头,逐项比对 adc_semantic_t / # prot_set_id_t / soe_type_t / prot_elm_id_t(解耦换来的"魔法下标"风险,口径 # docs/UI设计/UI_数据供给设计说明.md §9.4;SOE 类型含未标定闭锁=7/复归=8)。 # files 覆盖被测两件 + 编号真源四头 + 页面消费面(ui_content_page/ui_menu*)+ harness。 - id: ui-data-unit-test name: UI 数据快照层单测(主机侧) entry: python test/host/ui_data/run_unit_test.py language: system files: ^(Drivers/BSP/UI/(ui_data|ui_w_soe|ui_w_records|ui_content_page|ui_menu)\.(c|h)|App/app/ui_data_feed\.(c|h)|App/algo/(adc_input|prot_set|prot_elm)\.(c|h)|App/log/soe_rec\.(c|h)|test/host/ui_data/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 8) 测试套件开关门禁(2026-10-01 加) # 背景:test_config.h 的 TEST_SUITE_* 决定固件里编入哪个测试套件, # 全部注释 = 产品构建。但此前**无任何门禁检查**——实测已提交的 HEAD 里 # TEST_SUITE_CH395F 就是开启状态(.map 含 34 处 test 代码),即仓库里 # 躺着的是测试固件。本钩子抓这个静默漏网:默认要求产品构建,跑套件回归 # 确需提交测试构建时用 `--allow-test-build` / ALLOW_TEST_BUILD=1 显式放行。 # 详见 docs/任务规划/登记簿.md - id: test-suite-switch name: 测试套件开关=产品构建 entry: python scripts/check_test_suite.py language: system files: ^test/board/test_config\.h$ pass_filenames: false # 9) 双端注册反向校验(2026-10-12 加,登记簿条目「rs485_task 白名单缺口」) # 背景:新 .c 必须手动注册两处(`MDK-ARM/*.uvprojx` + `simulator/CMakeLists.txt`) # 才会被编译;历史上 `App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c` # **两处都没注册** = 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效。 # 本钩子反向核对两份注册表:①存在但未进 uvprojx ②存在但未进 CMakeLists # (MCU-only 豁免见脚本 SIM_EXEMPT,显式清单+每行理由,不整目录一刀切) # ③注册了但文件不存在(幽灵注册)。 # 存量缺口(rs485_task/lftpd_log)已进脚本 KNOWN_GAPS 白名单 → WARN 不拦, # 只对新缺口 FAIL,故钩子可立即上线不拦现有提交(`--strict` 供审计关白名单)。 # ⚠ pre-commit 4.x 已移除 `always: true` → files 必须覆盖所有触发面: # 任意 .c + uvprojx + CMakeLists.txt + 脚本自身(脚本自身改动也值得复跑)。 - id: registration-check name: 双端注册反向校验 (uvprojx + CMakeLists) entry: python scripts/check_registration.py language: system files: (\.c$|\.uvprojx$|CMakeLists\.txt$|^scripts/check_registration\.py$) pass_filenames: false # 10) RAM 预算 92% 阈值门禁(2026-10-12 加,登记簿条目「缩堆迁 CCM 后半(暂缓)」的台账/门禁那半 / 审查报告 §5.5 三件套) # 背景:报告 §5.5 第 1 条要求"超阈值即失败 + Top-10 附表"——此前 mem-numbers 只防 # "文档数字漂移",**没有任何机械检查拦"占用超阈值"**;台账三件套(ram_baseline.py + # ram_baseline.tsv + docs/内存管理/RAM预算台账.md)落地后,本钩子把 92% 红线机械化。 # 逻辑:RW_IRAM1/RW_CCM 任一 >92% → FAIL + Top-10 RW+ZI 对象表;台账受管区漂移 → FAIL # (提示 --write);无 .map → 自适应跳过(同 mem-numbers 在 CI 的行为,盲区已登记)。 # ⚠ pre-commit 4.x 无 `always: true` → files 覆盖所有能改变 RAM 的触发面: # 全部自研代码目录 + lv_conf.h + 测试板配置 + 脚本/tsv/台账自身 + 本配置文件。 - id: mem-budget name: RAM 预算 92% 阈值门禁 entry: python scripts/ram_baseline.py --gate language: system files: ^(App/|Drivers/BSP/|Src/|Inc/|Lib/lv_conf\.h|test/board/|scripts/ram_baseline\.|docs/内存管理/|\.pre-commit-config\.yaml$) pass_filenames: false # 11) UI 字符集差集检查(2026-10-15 加,报告 §6.5 的 UI 字符集与像素回归门禁残余 / §7 交付物清单"字符集差集检查"行) # 背景:`ui_font_cn_12.c` 是**子集**字体,只含 `ui_cn_chars.txt` 列出的码位。 # 生成链 UI 源码 --extract_cn--> ui_cn_chars.txt --make_font--> 字体 .c,其中 # verify_coverage 只在**生成期**校验"txt ⊆ cmap";改完 UI 字符串忘了重跑 # font-regen 就提交 → 缺字上机渲染为空白(白框),无日志无报错只能肉眼截图发现 # (font-regen skill 的"缺字渲染为空白"约定,此前纯靠自律)。本钩子把它机械化: # A. 源码在用 vs txt 缺 → FAIL(白框方向);多 → WARN(只费 Flash,--strict 升级) # B. txt vs 字体 cmap 缺 → FAIL(lv_font_conv 对缺字形/低码位**静默丢字**) # A 过 + B 过 ⟹ 字体 ⊇ 源码每个在用字。提取与 cmap 解析**复用** tools/font_gen # 两个生成器的同一套规则(importlib 直载 extract_cn.SCAN/collect + make_font. # verify_coverage),杜绝第二套解析规则漂移(§5.5 同类教训)。 # ⚠ pre-commit 4.x 已移除 `always: true` → files 必须覆盖全部触发面: # UI 源码(含字体 .c 自身)+ ui_task.{c,h} + 字符集 txt + 两个生成器 + 本脚本。 - id: font-charset name: UI 字符集差集检查(防缺字白框) entry: python scripts/check_font_charset.py language: system files: ^(Drivers/BSP/UI/|App/task/ui_task\.(c|h)$|ui_cn_chars\.txt$|tools/font_gen/|scripts/check_font_charset\.py$) pass_filenames: false # 12a) BOM 漂移门禁(2026-10-25 道E task-20) # 背景:AGENTS 铁律第 2 条——编辑工具会**静默剥掉**文件原有 BOM,ARMCC 于是按 CP936 # 解码带中文的源文件,报 #870-D / #8 / #274 一类**假语法错**(报错位置与内容都不指向 # 真因),而 MSVC(模拟器侧)与既有钩子全照不出来。2026-10-25 一天内道E 踩 4 次、 # 道B 批 1 踩 1 次,此前全靠人工逐文件比 HEAD 兜底——人工审计只防得住记得做的人。 # 判定:HEAD 有 EF BB BF 而工作区没有 → FAIL。两条刻意不报(新增文件无 BOM、HEAD 无 # BOM 而工作区加了 BOM)与一条刻意不做(不自动补:整文件重写正是本仓更大的坑)都 # 写在脚本头部。files 与 AGENTS 名单、脚本内 BOM_LIST **三处互指**,脚本每次跑核一次, # 单独改任何一处都会在别人的提交里被拦下——名单散在三处而无人同步等于没有名单。 - id: bom-guard name: BOM 漂移检查(带 BOM 文件被剥掉即拦) entry: python scripts/check_bom_drift.py language: system files: ^(Drivers/BSP/UI/|App/task/ui_task\.c$|App/cfg/cfg_file\.c$|Lib/lv_conf\.h$) # 12b) 定值查看行表一致性(2026-10-25 道E task-20 追加批;脚本随 task-15 批 3 落) # 核 Drivers/BSP/UI/ui_w_setview.c 的四张视图表与 App/algo/prot_set 的枚举序、 # g_prot_set_meta 上下限是否仍是同一份口径:id 恰好覆盖 0..39 各一次、数值行 # min/max 等于 meta 换算、kind 与 meta.is_ctl 一致、单位纯 ASCII、小数位查表与行表 # 一致。为什么必须机械化:BSP 侧看不到 App 头,行表里的 id 是裸数字 + 行尾注名, # 这条对齐在编译期**物理上无法自证**;而页面上只有当前可见的 8 行会被像素断言看到, # 错位要翻到第 2、3 页才可能被人发现(发现了也已被基线钉死)。 # ⚠ 跨分支先后:脚本文件随 task-15 分支合入主干;在此之前 files 永不命中 → 钩子自然 # skip(skip 不是失败),合并后自动生效,不需要任何人再动这条配置。 - id: setview-check name: 定值查看行表与 prot_set 枚举/上下限一致性 entry: python tools/setview_check/check_setview_limits.py language: system files: ^(tools/setview_check/|Drivers/BSP/UI/ui_w_setview\.(c|h)$) pass_filenames: false # 12) UI 像素回归门禁(2026-10-20 加,W6 的 UI 字符集与像素回归门禁收尾第三件 / 报告 §6.5 同项、§6.1 golden 行) # 背景:golden 两脚本(check_menu_rows.py / check_content_pages.py)早已存在且一直在 # 人工跑,但**从未进自动门禁**——"有基线"不等于"有门禁"(报告 §6.1 原文即自认此缺口)。 # 形态选型依据实测:改一个 UI 源文件后 build_sim 增量重编 + 两条 golden 合计 **2~4 秒**, # 成本可接受 → 走自动门禁(若实测要几分钟,则应改为文档化人工门禁,一个逼人 # --no-verify 的门禁等于没有门禁)。 # 有牙实测:给 ui_page_dc.c 的 .top_y 注入 +3 位移 → build_sim 重编 → # golden FAIL 且点名 `[dc] FAIL 与参考页不一致(1366 px 差异)`、退出码 1;还原后 diff 干净。 # ⚠ 两条 golden 按"整棵菜单树 + 全量参考图"工作、不接受文件参数,故包装器有意忽略 # argv 并 pass_filenames: false(pre-commit 传的变更名对它无意义)。 # ⚠ 参考图 simulator/tests/ref/*.bin 是判据本身,改动它等于改判据——它已入库, # 评审时看 `git diff -- simulator/tests/ref` 必须逐页过目并在提交正文说明; # 任何时候都不得用 --update 刷基线来"让钩子变绿"。 # ⚠ pre-commit 4.x 无 `always: true` → files 必须覆盖所有能改变像素的触发面。 # ⚠ 已知瞬时现象:首次实测时在"连续注入/还原的紧邻重建"之间偶发失败一次, # 疑似 MSBuild 节点争用;此后 3 次干净树连跑均绿。判据:单次失败先复跑一次再看, # 连续两次失败才当真(不许直接 --no-verify 绕过)。 - id: ui-golden name: UI 像素回归(菜单 8 状态 + 内容页 74 张 黄金参考) entry: python scripts/check_ui_golden.py language: system files: ^(Drivers/BSP/UI/|App/task/ui_task\.(c|h)$|simulator/(tests|sim|src)/|simulator/main\.c$|simulator/CMakeLists\.txt$|simulator/lv_conf\.h$|scripts/check_ui_golden\.py$|\.pre-commit-config\.yaml$) pass_filenames: false # 12a) 板端 UI 导航器认屏自检(2026-10-25 道E task-25) # 背景:大窗实测——靠预置键序盲走到深页时,9 帧里只有 3 帧真是它声称的态; # KEY 的 ACK 是空应答、不携带状态,所以**画面是唯一事实源**。判据脚本 # scripts/board_ui_nav.py 据此改成闭环:每按一键重新认屏再决定下一个键。 # 本钩子跑它的**快检档** --selftest-quick:只核"认屏认得对"(接线当时 18 张、现 74 张黄金参考 + # 真机留档帧,目录不在版本控制里、缺就整段跳过),秒级、无构建产物依赖。 # ⚠ 不挂完整 --selftest:第三段闭环需要 lvgl_sim.exe(构建产物,新工作树里没有) # —— 正是同一天 mem-numbers 缺 .map 那课的形状;脚本对缺 exe 报 FAIL 并给出 # 可执行提示,不"跳过当通过"。合入前 / 上板前手工跑一次完整档: # python scripts/board_ui_nav.py --selftest(约 30 s,含全绑定页闭环)。 # ⚠ 触发面刻意只两块(脚本自身、黄金参考帧):字体或菜单树变动**不**触发它, # 那两类的守卫分别是 font-charset / ui-golden / registration-check 与 id 侧; # 把每个 UI 提交都拉去跑导航器,只会让人习惯等一个与本改动无关的钩子。 - id: uinav-selftest name: 板端 UI 导航器认屏自检(快检:黄金参考帧分类) entry: python scripts/board_ui_nav.py --selftest-quick language: system files: ^(scripts/board_ui_nav\.py$|simulator/tests/ref/.*\.bin$) pass_filenames: false # 13) 编号门禁的历史:2026-10-20 起曾有一版「全库语料审计」(规范 §8 的 8 项校验 + # 99 族 registry + 别名表)。它 2026-10-28 退役——维护成本高过收益(近 80 笔提交 # 61 笔被编号体系牵连),脚本与语料整体移入 `scripts/archive/id_gate/` # (保留可复现性,**不再进提交门禁**)。退役路线与判据替换见规范 §8.5。 # 14) 编号门禁(2026-10-28 上线只查新增行那一道;2026-10-29 用户裁定**整体退役**,钩子已摘) # 退役后编号形态没有机器面:判据回到 docs/编号规范.md §1 三句自问 + §4.1 族总表 + 评审。 # 脚本移入 scripts/archive/id_gate/check_ids_new.py(手工可跑,--self-test 保留),留档说明 # → docs/门禁说明/id-check-new.md;手工普查与恢复机器面 → docs/编号规范.md §8.5。 # ⚠ 计数口径:本条摘除使 YAML 解析条目 52 → **51**,须与 AGENTS.md 的「共 N 个钩子」、 # docs/门禁说明/README.md §2 标题三数同源同批改(自查命令见该条;别在注释里写数钩子的字面量)。 # 14) 分层方向门禁(2026-10-22 加,结构债的"防扩大"那一半) # 背景:AGENTS.md 与 项目架构总览.md 早就写着"六层单向依赖",但**从来没有机械检查** # ——Drivers/BSP/ 里可以随手 include "cfg_file.h" 而无人报警。后果可测量:2026-09 审查 # 报告记"NETCFG 一处",2026-10-22 实测已长到 **8 个文件 9 处**。边界的静默腐蚀只能 # 靠门禁止住,所以本钩子做**增量拦截**(与 id-check 同构):现状 6 条钉成 BASELINE # (只减不增,没有 --write 往里加),新出现的 (BSP 文件, App 头) 组合直接 FAIL; # 3 条横切基础设施头走 ALLOW 永久豁免(日志后端 / 时间源 / 纯算法工具——它们是 # 任何层都要用的叶子能力,不是上层服务)。修完存量后跑 --prune 会点名让你收紧基线, # 于是"还欠几条"变成可见进度,而不是文档里的一句口号。 # 注:本钩子只拦"静态 include 方向";**运行时的属主纪律本来就没错**(UI 只投递 # net_cfg_save_request(),写盘在 defaultTask),别把它当缺陷读。 # 扫描范围恒为全库(要跨文件建 App 头索引),故 pass_filenames: false。 - id: layer-deps name: 分层方向(BSP 不得反向 include App) entry: python scripts/check_layer_deps.py language: system files: ^Drivers/BSP/|^scripts/check_layer_deps\.py$|^\.pre-commit-config\.yaml$ pass_filenames: false # 15) BSP 错误码与超时命名约定(机械检查那一半)。 # 四项检查:公开 API 不得用 HAL_StatusTypeDef/bool 当错误通道(谓词 is_/has_/can_/valid # 不在此列);*_TIMEOUT 宏不得取负值(名字叫 TIMEOUT 其实是返回码);非 void 的公开 API # 必须有「返回值:」注释;**返回错误码的函数不得裸丢弃返回值**(接住或显式 (void)——实测 # ARMCC v5 与 cppcheck 都不报这类,只有这条会响)。存量钉 BASELINE、只减不增(2026-10-23 # 首清点 19 条 → 已还 18;新增一项又暴露 37 条 → **现 38 条**:37 丢弃 + 1 个 bool 待改签名), # 修完跑 --prune 点名收紧基线。约定正文 = Drivers/BSP/bsp_err.h,规则说明 = # scripts/check_bsp_err.py 文件头,逐项详解(含限制与盲区)= docs/门禁说明/bsp-err.md。 # 恒扫全库(要按 git 清单建公开头索引),故 pass_filenames: false。 - id: bsp-err name: BSP 错误码与超时命名约定 entry: python scripts/check_bsp_err.py language: system files: ^Drivers/BSP/|^scripts/check_bsp_err\.py$|^\.pre-commit-config\.yaml$ pass_filenames: false # 8) 主机单测覆盖率总闸(2026-10-24 接线,用户裁定选 FAIL 语义) # 为什么现在才挂:接线前本文件只有 16 个 *-unit-test 钩子,而仓库已有 28 套主机单测—— # P1 新增的 12 套不在任何提交钩子里。原先"再加一份全量插桩等于同一件事做两遍" # 这条理由到现在已经**反了**(现状是有一遍没做),所以必须挂;缺口此前如实登记在 # docs/测试说明/单元测试/主机单测覆盖率口径.md §8。 # 为什么挂这一根而不是补 12 个 per-module 钩子:本钩子当时跑**全部 28 套**(现值 47 套,台账见 # docs/门禁说明/host-unit-tests.md §6 第 4 条),并且守 §5 的 # 回退台账(某套被删、或某文件的已覆盖行变少 → [REGRESS] 直接红)。这是 per-module # 钩子做不到的——删掉一套测试,它那根钩子只会因为"没有文件要检查"而静默跳过。 # ⚠ FAIL 语义(用户裁定 2026-10-24):找不到 mscoverage 时脚本退出码 2 → 本钩子**失败**, # 不做 skip。理由:skip 等于造一根会静默失效的门禁(与 pre-commit 4.x 移除 always: true # 是同一类坑);宁可在没装组件的机器上被挡一次、看清 [ENV] 那行提示,也不要一根假绿的闸。 # 本机与 CI runner 同一台;实测两份 VS 安装都自带该 exe(口径 §7 第 8 条已按实测更正)。 # 口径 / 分母 / 基线 / 盲区详解都在 docs/测试说明/单元测试/主机单测覆盖率口径.md,本处只留接线事实。 # 触发面**含门禁自身**(.pre-commit-config.yaml):不带自身就等于谁改坏这根闸的 files: # 模式,它就再也不跑且无人察觉(同 lv-conf-sync / bsp-err 的既有做法)。 # ⚠ stages: [manual](2026-10-28 用户裁定):本钩子跑光全部 47 套单测(41 套时实测 ~105 s,套件数 # 涨了耗时就涨,47 套这轮未重测耗时; # 占一次带代码提交耗时的三分之二)→ 不压在提交期,由**远端 CI** 强制 # (.gitea/workflows/ci.yml 的「手动阶段门禁(覆盖率总闸 + 全量 cppcheck)」步),本地等价入口 = # scripts/ci_local.py(manual 段);手工复跑: # python -m pre_commit run --all-files --hook-stage manual test-coverage # 另:此前它没写 stages: → 默认属于所有阶段,同一次提交输出里出现两次(commit-msg # 阶段那遍几乎必然 Skipped,只看结尾会把实跑读成"没跑")。固定 manual 阶段后不再有这一坑。 - id: test-coverage name: 主机单测覆盖率(增量拦截:已覆盖行只增不减) stages: [manual] entry: python scripts/check_test_coverage.py language: system files: ^(App/|Drivers/BSP/|test/host/|scripts/check_test_coverage\.py|scripts/test_coverage_baseline\.json|\.pre-commit-config\.yaml$) pass_filenames: false # 16) 生成器幂等性(2026-10-26 加,task-18;判据正文 docs/文档规范.md §4.4,索引行见 docs/门禁说明/README.md) # 背景(实证,不是预防性):0f7b5a87 改了 tools/menu_gen/gen_menu.py 的**模板文字**却没重跑 # 生成物 → HEAD 里 Drivers/BSP/UI/ui_menu_tab.h 与模板不一致,后来是别人重跑生成器才被顺带 # 纠正(属主确认以生成器输出为准)。接线前实测又扫出 **5 张 docs/images/*.svg** 与各自生成器 # 不一致(文档挪目录后 rename_ref.py 只改了 tools/ 里的引用路径、产物没跟着重跑)——生成物 # 比生成器旧是**系统性**缺口。隐蔽之处:改生成器的人看不到任何 diff(脚本改动编译、单测全过), # 下一个**碰巧**重跑的人却看到一段「不是我改的」改动 → 要么连带提交、要么 revert 掉真修复。 # 判定:把声明的输入与生成器脚本一起拷进**系统临时目录**重跑(三个生成器都按 __file__ 的三级 # 父目录定位仓库根 → 只会写 temp),再逐字节比对 temp 与工作区:DIFF / NEW / 生成器报错 = FAIL。 # **仓库一个字节都不写**,因此不需要还原、也不会 stash 别人在飞的改动(多人同树可安全跑)。 # 覆盖:gen_menu(含它回写的 uvprojx + CMakeLists 两份注册表)、中文字库链(extract_cn → # ui_cn_chars.txt、make_font → ui_font_cn_12.c)、tools/arch_diagram 全部配图生成器。 # 一处刻意不按裸字节比对:ui_cn_chars.txt 按 font-regen 约定 = 源码在用 ∪ KEEP_EXTRA,改判 # 「txt 恰等于 重跑 ∪ KEEP_EXTRA」(两向都不许差;清单 importlib 直载 scripts/check_font_charset.py, # 不留第二份清单)。存量基线 = 接线当天已漂的那 5 张 SVG,钉进脚本内 BASELINE 走 WARN 不拦提交 # (只减不增,还清即删、默认运行会点名);业务生成物(菜单表 / 页面注册表 / 字库 / 双端注册表) # **零豁免面**,漂了就是 FAIL。 # ⚠ entry 是裸 python,而 pre-commit 在提交环境里可能把它解到**机器级**解释器:2026-10-26 真实提 # 交实测解到 C:/Python314(无 fontTools)→ 字体链报 No module named fontTools = 假红。所以脚本 # 自己按「显式 DTU_GEN_PYTHON > 当前解释器 > 仓库 .venv > PATH」解析并**逐个探针校验**(要真能 # import fontTools 才算数),整轮统一用同一个解释器;挑不出来打 [ENV]+候选表+修法、退出码 2 不 # SKIP。不把 entry 写死成 .venv 路径:那是 ci_local 2026-10-11「候选收窄 → runner 上静态门禁全灭」 # 的同一条坑(详解 docs/门禁说明/generated-fresh.md §4)。 # ⚠ 字体链外部工具(node + 全局 lv_font_conv + 系统 simsun.ttc / seguisym.ttf)缺失时该项 SKIP 并 # 打印 [SKIP],--strict-env 才升 FAIL——与 test-coverage「缺 mscoverage 必 FAIL」刻意不同: # 缺工具的机器上没有可执行的修法,SKIP 至少把「这台没验过」留在输出里(盲区同登记在索引行)。 # ⚠ pre-commit 4.x 已移除 always: true → files 必须覆盖全部触发面:任何生成器(tools/)、 # 生成器的输入(Drivers/BSP/UI 源码、App/task/ui_task、ui_menu_tree.txt、ui_cn_chars.txt)、 # 生成物(docs/images/*.svg + 两份注册表)、脚本自身与本配置文件(同 registration-check 口径)。 - id: generated-fresh name: 生成器幂等性(重跑到临时目录 → 与工作区逐字节一致) entry: python scripts/check_generated_fresh.py language: system files: ^(tools/.+|Drivers/BSP/UI/.+|docs/images/.*\.svg|App/task/ui_task\.(c|h)|ui_menu_tree\.txt|ui_cn_chars\.txt|MDK-ARM/STM32F407-Demo\.uvprojx|simulator/CMakeLists\.txt|scripts/check_generated_fresh\.py|scripts/check_font_charset\.py|\.pre-commit-config\.yaml)$ pass_filenames: false # ── 块注释嵌套(第 51 条,2026-10-27 加,task-29)───────────────────────────── # 查什么:C/C++ 块注释体内再出现 /* —— ARMCC 诊断 9-D「nested comment is not allowed」。 # 本仓的诱因是「在注释里写作用域 / 通配路径」这种常态写法:Drivers/BSP/UI/** 里那个 # /* 就是第二次注释的起点(C 的块注释不嵌套)。 # 为什么要闸:它**只在重编译那个 TU 时才暴露**,历史「0 警告」不代表安全;而且按包含者 # 数量刷屏——一份被广泛 include 的头里一处嵌套 = 每个包含者一行警告(实测一例扩散到 16 # 条,把整批真警告挤到看不见)。有了这根,提交时就报,不用等 /keil-build。 # 接线当日实证(冻结引用,别去翻构建日志——它每次 /keil-build 就被覆盖、也不入库):当日 02:22 # 那次构建日志报过 ..\App\algo\run_par.c(267): warning: #9-D: nested comment is not # allowed ——那是别人**未提交**的在飞状态。原文抄在 docs/门禁说明/nested-comment.md §2.1; # 已提交历史回放 281 份旧字节 0 命中 ⇒ 这类问题一直靠人工在编译后清,从没被机器拦过。 # 存量基线(只减不增,除这 2 处外**零豁免**):全仓 --all 实扫 403 个 C/H,新发 0,存量 # 2 处 = test/host/common/ch395f_fake.h:7 与 fatfs_fake.h:9(注释里写 test/host/**,属 # test/ 写域,等那侧改成语义短语;还清后删 BASELINE 行,--prune 打台账)。 # 自证:--selftest 四项(正例照得出 / 字符串与行注释不误报 / 基线吞得下 / 只减不增有牙), # 另有 10 项形态电池(含「注释里写 UI/*」「块注释内行首再开 /*」等真形态)。 # ⚠ 计数口径:本条使 YAML 解析条目 50 → **51**;配置里那条被注释掉的 clang-format 候选仍 # 在,所以**按文本 grep 声明行会得 52**(多算那条候选)——那不是口径。本行刻意不复现那个字面量, # 否则注释自己也会被 grep 数进去(2026-10-27 实测踩过:写过一次,计数立刻虚高 1)。 # 口径与自查命令见 docs/门禁说明/README.md §2 首条。 - id: nested-comment name: "块注释体内不得再出现 /*(ARMCC 9-D 嵌套注释)" entry: python scripts/check_nested_comment.py language: system types: [c] # ─── SKILL.md 体积与 reference/ 指针门禁(第 52 条,2026-10-29 加)───────────────────── # 为什么要有:dsh-mnemon 插件对技能正文有 65536 字节的**写侧**硬上限(validateSkillContent), # 但**加载侧不查**——超限的 SKILL.md 照常可读、照常进技能目录,只有一件事会坏:自进化每次 # 「生成草案」都在落盘那一步被拒,那一行永久卡在「生成中」、界面只剩转圈、连「忽略」都不出现 # (Draft/Dismiss 只在 pending 渲染)。实测 2026-10-29 一天内两个技能各卡一次(第二个 72,601 # 字节),每次白烧一个 30 分钟的草案 Agent。**在加这根之前,仓库里没有任何一处会告诉你某个 # 技能已经废了**——文件能读、能改、能提交,全绿。 # 判据:> 65536 FAIL(与插件同数、同口径:utf-8 字节数不是字符数);>= 49152(75%)WARN 不拦 # (越线通常只隔一次草案采纳:实测 49821 → 62806);正文里 (reference/xxx.md) 指向的文件不在 # → FAIL;带 UTF-8 BOM → FAIL(插件按 utf-8 解析,BOM 会让 frontmatter 读歪)。 # 全树扫不只看变更文件:钩子拿到 filenames 只当触发用,判定始终扫整个 .dsh/skills/——因为改 # reference/*.md 时 SKILL.md 没动,但它可能正是被那次外置撑过线/掏空的那一份。 # 为什么 entry 敢用裸 python:AGENTS.md「钩子 entry 的裸 python 不等于 .venv」那条教训针对的是 # 第三方依赖(fontTools);本脚本纯标准库,任意 python3 均可(实测 .venv 3.12 与 PATH 上 3.14 # 都跑通),所以不需要脚本内再自解析解释器。 # ⚠ 盲区:只管仓库内 .dsh/skills/;用户级 ~/.dsh/skills/ 不在仓库里,本钩子照不到(手工 # `node "$HOME/.dsh/scripts/mnemon-skill-ledger-doctor.mjs" --root .` 或本脚本 --root 都只报, # 要拦得另配 shell 钩子)。只认 markdown 链接形态的指针,纯文本里写的路径不算。 # 自证:--selftest 五形态(干净 0 / 75% 线 0 带 WARN / 超上限 1 / 悬空指针+BOM 1 / 根不存在 2)。 # 详解与限制 → docs/门禁说明/skill-size.md。 - id: skill-size name: "SKILL.md 不超插件写侧上限 65536 且 reference/ 指针不悬空" entry: python scripts/check_skill_size.py language: system files: ^\.dsh/skills/