Files
STM32F4-Base/.pre-commit-config.yaml
T
wandering1 ffd462ef3c feat(保护标定): 卡5 未标定闭锁 + 卡6 下游三常数重定 + 卡7 量纲回归单测
卡 5(protect_task.c 第⑤闭锁源,「数据可信」§4.3 决策 1=闭锁):
- UA/UN/IA 的 adc_cal VALID 未全置位 → prot_elm_init + 本拍不 eval;每拍现查
  flags、无自有状态(卡 12 标定写入后自动解锁);判据链优先级 = 采样可信在前、
  标定可用在后(数据都是旧的谈不上标定)
- 边沿日志用 DBG_INFO 而非 [ERR]:未标定是出厂常态、非故障,且 @test 禁用
  [ERR](「默认构建保护不动作 = 期望」的台架判据同步项已在 MR 报 lead);
  SOE 分型 SOE_T_CAL_BLOCK/RESUME(soe_rec.h 枚举尾追加,值 6/7——reader 侧
  显示标签映射归道B reader,MR 已报)
- 台架旁路 = 编译期宏 DTU_BENCH(§4.3:不进 cfg 不进 104;本仓所有提交目标
  都不定义它 = 产品恒强制闭锁)

卡 6(下游三常数按工程量重定,依据同源 docs/任务规划/数据可信.md §2.3):
- FREQ_AMP_MIN(freq.h):语义从拟定码值域重定为二次值 V——50 V = 0.5×Un
  (标称 100 V),断保险/PT 空载/浮空必低于之;数值沿用 50.0f 属巧合并在
  注释明写;eng=0 过渡期本门恒失效 → OF/UF 按必不启动填充,与卡 5 同向安全
- k_yc[].deadband(iec104_pts.c):9 点按「额定 0.5 %」规则重定:电压类
  0.5 %×100 V=0.5、电流类 0.5 %×1 A=0.005、频率维持 0.5 Hz 绝对值(非额定
  比,卡 10 运行参数化);标称基准与 prot_set 头部同源,防两处真相
- g_prot_set_meta[](prot_set.c):数值/范围复核结论=不动(0~4×标称自洽),
  文件头补量纲口径块:A/V = 二次值、标称 100 V/1 A、OV120=1.2Un/UV20=.2Un/
  3U0 15=.15Un、OC1/2/3=10/5/2×Ir、OL=1.0×Ir;PS_ID_COUNT/blob VER 未动
- iec104 套件 TC-008 死区用例同步重定标(100.4 带内 / 101.25 越带),97/97

卡 7(test/host/prot_dim 新套件,真实链 adc_cal→ring→phasor→prot_elm):
- 用例从 ADC 码值出发、显式写出 code↔二次值互逆换算(dim_code vs apply),
  折算点被旁路或量纲再错位断言立刻翻转(M4 判据⑥「不该动作时不动作」)
- 合成 ROM 覆写(#undef K_Q16 宏)模拟卡 12 标定后板级口径,走 adc_cal_set
  正规路径置 VALID;b=-30 码全链对消进 rms 断言
- 38/38:基线静默(含 OL 低定值陷阱)、反错配对照(raw 码值灌环复现历史事故
  必假动作 vs 经折算必静默)、OC1/2/3+OL+OV+UV+3U0 七类逐一:越限定值→
  受控启动拍→dly-2 不动作/+5 动作→返回系数带内保持→跌破释放、双件聚合位图
  独立性(撤 IA 只回 OC1)

测试与注册:prot-dim-unit-test 钩子 + TC-HOST-PROTDIM 族(游标 010,B 层口径
同 ADCHEALTH/ADCCAL 先例);无新 MCU 源文件(uvprojx/SIM_EXEMPT 零改动)

gate:@build 0 错 0 警(Code=215640 RO=21388 RW=1220 ZI=131980 vs 基线
215440/19780/1220/131980 → Flash +1808 B、RAM 零增量);protectTask 静态最坏
链 872 B/margin 1176 B(57%)PASS,adcTask 不变;staged 门禁除 mem-numbers/
mem-budget(台账漂移,回填归 lead)外全绿:
gate: only mem-budget/mem-numbers pending lead ledger backfill
BOM/CR/U+FFFD/字面量 ASCII 已逐文件自查(本轮未触碰带 BOM 文件)
2026-10-25 05:22:17 +08:00

648 lines
44 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) 文档一致性门禁(D-7)
# 存在性:md 互链 / 反引号路径是否指向真实文件(防迁移/删除后残留死链)
# 章节:`§N.M` 必须能在目标文档的编号标题里找到(--sections-strict:自引用失效也拦)
- id: doc-links
name: 文档链接存在性 (D-3/D-7)
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 一致(D-2/D-7)
- id: task-table
name: FreeRTOS 任务表一致性 (D-2/D-7)
entry: python scripts/gen_task_table.py --check
language: system
files: ^(README\.md|AGENTS\.md|Src/freertos\.c)$
pass_filenames: false
# 体积:内存/Flash 数字须与最新 .map 一致(D-7 第三档)
# 只在内存文档或代码变动时触发;.map 未生成时脚本自身给出明确提示
- id: mem-numbers
name: 内存/Flash 数字一致性 (D-7 体积档)
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 加,随 F-1/F-2/F-3 落地)
# 背景:`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
# 7g) 时间同步(time_sync)主机侧单元测试(2026-10-10 加)
# 背景:time_sync.c 是 TCP 时间同步协议状态机,也是 N-17 号看门狗风险的**全部
# 逻辑贡献者**——"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
# 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 加,N-19 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 加,N-19)
# 锁定 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 全离线
# 穷举设计(A-4 约束 1);板端复现这些分支要真主站+真等待。
# 触发面含数据源依赖(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
files: ^(App/protocol/iec104/iec104_(asdu|pts|link)\.(c|h)|App/algo/(prot_set|prot_elm|phasor|sample_ring|adc_input|freq)\.(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 加,N-21 / 报告 N-12+N-13)
# 背景:移植层六个 HAL 入口是**纯地址算术**,此前对越界页/块号(含
# DHARA_PAGE_NONE 哨兵)**不做任何上界校验**——加偏移送进 SPI 命令后被地址位
# 截断成某个真实页 → 静默读写/搬走无辜数据;而 is_free 旧实现只看页首 64 B,
# 会把"页首 0xFF、后段有数据"的已编程页误判空闲 → dhara 往已编程页再编程
# = 数据丢失。两类缺陷真机**极难复现**(要恰好撞上特定页形态 / 被污染的 meta),
# 板端只能表现为"某次复位后丢数据",无从定位。
# 本钩子用 MSVC 离线编译真实 nand_ftl.c(#include 纳入)+ **RAM 假芯片**
# (32 块 x 64 页 x 2048 B):把"双编程"做成可数事件(N-12 探针)、记录全部
# 访问次数,于是"越界不产生任何芯片访问""越界标坏块次数为 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
# 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 T-1
- 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 加,登记簿 N-23 / 审查报告 N-06 残余)
# 背景:新 .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 加,登记簿 N-27 前半 / 审查报告 §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 的行为,盲区登记 A-5 残余)。
# ⚠ pre-commit 4.x 无 `always: true` → files 覆盖所有能改变 RAM 的触发面:
# 全部自研代码目录 + lv_conf.h + 测试板配置 + 脚本/tsv/台账自身 + 本配置文件。
- id: mem-budget
name: RAM 预算 92% 阈值门禁 (N-27)
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 U4 残余 / §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
# 12) UI 像素回归门禁(2026-10-20 加,W6 U4 收尾第三件 / 报告 §6.5 U4、§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
# 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 立刻报(N-31/N-32 正是这条)。
# 盲区显式登记在 registry 的 `blind_spots` / `unmanaged_bare`:裸形同字母多义
# (§1.2:P1 五义 / L1 四义 / U1 四义)与元器件位号同形(§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 加,报告 §0.4 结构债 S-1 的"防扩大"那一半)
# 背景: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 错误码与超时命名约定(报告 S-5 的机械检查那一半)。
# 四项检查:公开 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