Files
STM32F4-Base/.pre-commit-config.yaml
T
wandering1 dafc18f5ed feat(UI): W6 第一波——设计令牌层、页面注册表生成器与 UI 门禁补强
启动 docs/任务规划/README.md 的 W6 UI 扩展簇(用户裁定,覆盖 2026-10-11
的"本轮不排期")。四个工作包,全部验证通过。

## U1 设计令牌层(零行为变化)
新建 Drivers/BSP/UI/ui_tokens.h(20 个宏),框架 + 7 个页面 + App/task/
ui_task.c 全部接入。屏幕几何**复用 ui_port.h 既有 UI_SCREEN_W/H**,不重复
定义;令牌头自身只 include ui_port.h,不把 LVGL 拖进全部消费者。
补收了报告没列但代码里真重复的量:UI_TEXT_BAND_H 15(7 次)、列表左右边界
与每页行数(各 4 个页面逐字复制)、推流协议几何与显示几何同源(此前硬编码
3200 且注释写着"须与 UI_FB_SIZE 一致"——典型漂移隐患)、UI_TASK_TICK_MS
(ui_port_step 与 osDelay 必须同值,原先靠注释维系)。
故意不收页面私有几何与"同形不同义"的值(device_info 的 pitch=15 与列表
UI_ROW_PITCH=14 合并即改像素)。

## U3 页面注册表与生成器(删手工分发)
ui_page_registry.{c,h} 由生成器产出,**手工 menu_content_page_for() 已删
除**(仓库内仅存 2 处历史说明注释)。密码页按代码实况**单独出口**
(ui_page_registry_password())——它非节点绑定,任何 #P 叶子进同一页。
顺带落地此前判定"U1 阶段做不了"的共享 flat 样式 ui_style.{c,h}。
生成器除产注册表外**自动把新 .c 写进 uvprojx 与 CMakeLists.txt**,且
**双向对账**:加页打印 registered、删页打印 pruned,不留死条目;并双向
校验(#page 找不到实例报错、实例未被任何 #page 引用也报错)。

## U4 门禁补强(35 个钩子)
新增 font-charset 钩子(scripts/check_font_charset.py):复用
tools/font_gen 的同一套提取与 cmap 解析规则(importlib 直载,零第二套
规则),A 段查源码在用 vs 字符集、B 段查字符集 vs 字体 cmap。
registration-check 补第 ④ 类:实测原规则对"进了两端构建但没接描述符表"
的页面**全绿**——desc 被链接器剥离、编译链接全过、菜单永远打不开的死代码。
仍照不出的两项已如实写进文档(枚举漏加归 @build、菜单树漏挂归 golden)。

## 字体子集瘦身(D 案,4 留 3 删)
ui_cn_chars.txt 122→119 字(恰删 上/电/重,零新增)。异/常/片/芯 进
KEEP_EXTRA 带理由白名单:它们曾在 9eb1bd63 被写进中文自检描述落盘
/log/selftest.log(定长 64B 记录跨固件持久),删字形会使历史故障记录
渲染空白且不可逆。保留字按**必需**处理,反向有牙——谁只跑 extract_cn
把它们静默洗掉,钩子直接 FAIL 并逐字打印理由。
--strict 口径为"未知冗余 = 0"而非"冗余 = 0"。

## 验证
@build 0 错 0 警;ER_IROM1 232,052 B(较 HEAD −228 B)、RW_IRAM1
75,232 B **零增量**、RW_CCM 87.9% 未变。golden:菜单 8 状态全像素一致 +
内容页 11 页逐字节一致,**参考图 sha256 差异 0 条**(未用 --update 刷
基线);池水位菜单基线 5368 B、开页-关页往返 5448 B 与 U1 时相同。
38 个在途修改文件逐个核对 BOM/CRLF/U+FFFD 全部与 HEAD 一致。
L0 33 钩子 + L0.5 16 runner / 1104 断言全绿;check_doc_links --all
142 篇、无法判定 0;mem-budget / mem-numbers 已回填。

## 单位成本实测(U5 排期依据)
临时页 zzprobe 绑到"遥信量"叶子 → 只跑生成器 → 带页 @build 0/0 →
注册表出现 {5u, &ui_page_zzprobe_desc} → 回滚后 6 个产出文件 zzprobe
残留计数均 0、重编回基线。
**结论:新增一页 = 3 处人工(新 .c + 新 .h + 菜单树那一行加
 #page=<名>)+ 1 条生成器命令,手工编辑注册文件 0 处。**
据此修正报告原口径"8 处降到 1 处"——那个 1 只在"1 处对既有文件的修改"
意义下成立,页面自身 2 个文件本就要写,真实处数是 3。

## 文档同步与两处自我修正
§0.1 UI 行 B → B+;§6.1「内容页」「设计令牌」两行按实况回填;§6.5 U1/U3
状态更新;§6 顶部加"建议栏不可照抄"总警示(本轮四次证伪:ui_port.h 已有
同名宏、LV_COLOR_BLACK/WHITE 不存在且极性反了、27 页实为目标页数现存 7
个、生成器草图漏密码页非节点绑定)。
两处改口:① §6.5 U4 从我上一版**误写的 3/3 改回 2/3**——"golden 脚本早已
存在"不等于"已进自动门禁",两个 golden 脚本至今未挂 pre-commit,该缺口
单列为待办;② 原计划"分三笔提交"**作废**,U1 与 U3 改同一批文件、文件内
交叠,硬切会产生不可编译的中间态,故整波一笔。
2026-10-20 21:14:14 +08:00

501 lines
31 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 自动化代码审查配置
# 详见 docs/代码规范/pre-commit使用说明.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/tests/ref/`、`simulator/shot*.{bin,png}`、`__pycache__/`)
# **已在 .gitignore 里**,永远不会进 git 索引 → 无需在此重复豁免,故直接删掉该条。
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
# 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