Files
STM32F4-Base/.pre-commit-config.yaml
T
wandering1 0f7b5a87fb chore(编号): 存量编号全量转语义短语,门禁识别面与语料同批重打
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号)
一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、
采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。

门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏
undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8
reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF;
id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。

test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字,
是本批唯一改变构建产物的文件集合。

为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged
部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、
不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。

实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01%
(基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 /
告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。

基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处
引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使
识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。

⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二
根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有),
今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。

固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0;
App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889
断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿;
check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带
BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。

.cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法:
Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。

同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%,
RW_CCM 57,376 B / 87.9%,阈值 92%。
2026-10-26 12:28:14 +08:00

756 lines
53 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# pre-commit 自动化代码审查配置
# 通用机制(阶段 / stash 陷阱 / 触发面 / 增量拦截三段式)→ docs/代码规范/pre-commit使用说明.md §2
# 每个门禁一篇详解(含限制与盲区)→ docs/门禁说明/README.md
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
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)
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/芯片手册/
)
# 数值: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) 相量层(L2)主机侧单元测试(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 输入层(L1)主机侧单元测试(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 采样有效性校验(L1-6)主机侧单元测试(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) 通道标定层(L1.5,「数据可信」卡 2/卡 4)主机侧单元测试(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) 量纲回归(「数据可信」卡 7,道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) 遥测快照层(L2 发布侧,「数据可信」卡 8,道D)主机侧单元测试(2026-10-25 加)
# 背景:卡 8 的 DoD 一半是并发与结构类判据——双区切换无撕裂(撕裂窗
# 200 ms 量级,台架人工比对观测不到)、seq 只增、§4.5 布局与字段偏移
# (卡 9/UI 直接按整体拷贝消费,破布局 = 破协议)、CLIPPED 品质汇总
# (削顶位图要等卡 12 标定才真实出现)。主机侧以 #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) 运行参数段(「数据可信」卡 10,道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) 保护定值模型(P1 / L5-1 载体)主机侧单元测试(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) 保护元件框架(L3 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) 频率测量层(L2.5,B8)主机侧单元测试(2026-10-12 加)
# 自适应窗测频(K_eff 对齐负序泄漏纹波周期)的 J1~J6 判据
# (扫频/噪声/收敛/阶跃/浮空 valid=0/钳制恢复)是纯合成信号
# 可穷举的精度契约;板端无标准源照不出来(规划 §3.3 L3-5)。
- 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 抽象就是为 L0.5 全离线
# 穷举设计(分层约束);板端复现这些分支要真主站+真等待。
# 触发面含数据源依赖(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
# 卡 10 起本 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 L4)。
- 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 状态 + 内容页 11 页 黄金参考)
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 张黄金参考 +
# 真机留档帧,目录不在版本控制里、缺就整段跳过),秒级、无构建产物依赖。
# ⚠ 不挂完整 --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 加,docs/编号规范.md §8 的 8 项校验)
# 背景:§1.2 用仓内证据列了 10 类编号事故(同字母多义 / 跨文档同号不同义 /
# 销项删行后残留悬空引用 / 自造前缀 / 会话临时号入册…),而 36 个钩子里
# **只有 TC 一族**有机械检查(check_test_traceability.py,且未挂钩子、正则还漏 [示例]
# `TC-AI-01A` [示例] 这类大写后缀)——E/N/S/B/C/D/A/U/O/gen/Trap 全部裸奔。本钩子把
# 规范 §8 的 8 项机械化:配置两份 `scripts/id_registry.json`(前缀 → 属主登记处
# /形态/冻结/游标,逐项来自规范 §4 总表)+ `scripts/id_aliases.json`(旧号→新号,
# 照搬 section_aliases.json 的成功范式)。
# 判据要点:「定义」= 字面量出现在该族**属主登记处文档**里;于是 §5.5 的
# 「销项 = 删行 + 清全库引用」只做一半,校验 3 立刻报(销项号被重新取走那类撞号正是这条)。
# 盲区显式登记在 registry 的 `blind_spots` / `unmanaged_bare`:裸形同字母多义
# (§1.2:P1 五义 / L1 四义 / UI 状态机号四义)与元器件位号同形(§4.4),机械全库判定
# 必然一边倒误报 → 裸形族只在属主文档内判越游标、文档外降告警;收口属 §9 阶段
# 1-② 的迁移批次。**不是「检查通过」,是记在案上的盲区。**
# ⚠ 当前 warn-only(§8 上线节奏:先清 backlog → 清零后 entry 加 --strict 拦提交,
# 与 doc-links 章节校验同一条路径)。校验 8 的「可判定比例」跌破
# id_registry.json 里的基线时**任何模式都失败**——那根线就是防回退的本体。
# ⚠ pre-commit 4.x 已移除 `always: true` → files 必须覆盖所有能改编号的触发面
# (任意 md/c/h/py/yaml/json/txt + 本配置文件);扫描范围恒为全库
# (悬空/孤儿/重复定义是跨文件判定,pass_filenames: false)。
- id: id-check
name: 编号门禁(docs/编号规范.md §8 八项校验)
entry: python scripts/check_ids.py --strict
language: system
files: \.(md|c|h|py|ya?ml|json|txt)$|^\.pre-commit-config\.yaml$
pass_filenames: false
# 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 套**,并且守 §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 的既有做法)。
# ⚠ 读输出的坑(2026-10-24 连踩两次才定位):本钩子没写 stages: → 默认属于**所有阶段**,
# 于是同一次提交的输出里它出现两次:pre-commit 阶段那一遍才可能实跑;commit-msg 阶段
# 那一遍的"文件"是提交信息文件,几乎必然 (no files to check)Skipped。**只看输出结尾会
# 把"实跑通过"读成"根本没跑"**——判它跑没跑要认**第一次**出现的那一行。
# 实跑证据:只改本文件的提交(e327636 / c4ca292)里 test-coverage 与 bsp-err 都是 Passed。
- id: test-coverage
name: 主机单测覆盖率(增量拦截:已覆盖行只增不减)
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