今天三次 --no-verify 共放过三类真问题:5 条 CODE-RULE-17(.c/.h 配对)、2 个缺尾换行加 1 个漏提文件(HEAD 一度是「代码已整行独占、旁边 runner 文档串还写着无锁直发」)、 以及共享尾段 +80 B 把 iec104Task 压到 1,024 B ISR 底线之下。根因不是「不该抢跑」, 而是**抢跑时只跑了部分等价门禁**——doc-links、id-check、字节卫生都跑了,唯独漏了 stack_report,而它恰恰是数字与安全边界那一类。 §2.4 三组清单(任何人可用,不是 Lead 特权说明):A 抢跑前必跑(stack_report、 ram_report、ram_baseline --gate 含「跳过不等于有余量」、coding-standard、bom-drift 含「别用有 BOM 就是错的外行判据」、mojibake、受影响模块单测、test-coverage、id-check、 doc-links、generated-fresh 必须 .venv、ci_local 并注明它**不含** stack_report 与 ram_baseline 所以不能当替身);B 手跑代替不了真提交的三类(改写型钩子、要靠 stash 才能正确归因的、只有真提交才碰到的 gitlint 与正文每行 80 列);C 抢跑之后: git show --stat 对交付清单 + git status 里不该再出现声明过的文件(含 ??,漏提 runner 就是这么发现的),并强制在正文点名「哪些没跑、为什么、谁在飞」。 一条结构性说明:stack_report 与 ram_baseline **不在钩子清单里**,所以「人漏跑一次 = 这道检查这次不存在」;要机械化得先解决「磁盘 .htm/.map 属于最后一次构建的那个人」 ——与 stash 覆盖面那条同源,不是加一根钩子就完事。另写明 --all-files **也不 stash**, 会让改写型钩子当场改全库,只能在静默窗口跑、跑完复核。 文档里的命令必须能直接跑:只读等价那条一行式刻意纯 ASCII——Windows PowerShell 5.1 按 ANSI 读无 BOM 的 .ps1,带中文标签的版本会碎成 SyntaxError(作者踩过并复现),改完后 从文档里抽出来重跑验证:checked 40 files (incl untracked),problems 0。 同篇顺手核掉的同源不同源(可单独回滚):§6 钩子数 40 → 50(口径 = YAML 解析条目; 直接 grep 声明行会多算一条被注释掉的候选 = 51)、主机侧单测「16 个合一篇」→ 22 根 专属钩子 + 18 套走 test-coverage 总闸;两处跨行断行的表格行修好,全文 8 个表格块 列数一致(脚本核过)。generated-fresh.md §4 补上解释器解析口径与 [ENV] 输出。
18 KiB
generated-fresh — 生成器幂等性(重跑生成物 diff 必须为空)
一句话:生成物与它的生成器不许分家——改完生成器(或它的输入)没重跑, 下一个碰巧重跑的人就会看到一段「不是我改的」diff。本钩子把「重跑一遍看有没有变化」机械化。 规范正文(为什么生成物必须与生成器同笔提交)→
docs/文档规范.md§4.4。
- 配置:
entry: python scripts/check_generated_fresh.py,pass_filenames: false,files:见.pre-commit-config.yaml第 16) 段注释(覆盖生成器 / 输入 / 生成物 / 脚本自身 / 本配置)。 - 数据源:判定表与存量基线写在脚本里(
GENS/BASELINE),没有第三方数据文件—— 生成器与产物就是一张三行表,做成 JSON 只会多一处会过期的地方。
1. 它在查什么
对每个在册生成器做同一件事:把声明的输入按相对路径拷进系统临时目录 → 把生成器脚本
也拷进去 → 在临时目录里重跑 → 遍历临时目录里除 __pycache__ 外的每个文件,与工作区同路径
逐字节比对:
| 结果 | 含义 | 级别 |
|---|---|---|
| DIFF | 工作区有、内容不同 → 生成物比生成器旧 | 错误(在基线内则降级 WARN) |
| NEW | 工作区没有该文件 → 重跑会凭空多出文件(漏注册 / 漏提交) | 错误 |
| 生成器 rc≠0 | 生成器自己报错(多半是输入与产物对不上) | 错误 |
| 生成器超时(300 s) | 生成器卡死 | 错误 |
| 环境缺失(只有字体链的外部工具:node / lv_font_conv / 系统字体) | 该台机器造不出这份产物 | SKIP(--strict-env 升错误) |
| 挑不出能跑字体链的解释器(仓库 .venv 没装 fontTools 或已坏) | 判定用不了,不是产物漂了 | 错误(退出码 2,附逐个候选的探针表与修法;刻意不 SKIP) |
在册生成器(GENS,加一个生成器就在此登记一处):
| 生成器 | 生成物 | 一并比对的副作用 |
|---|---|---|
tools/menu_gen/gen_menu.py |
Drivers/BSP/UI/ui_menu_tab.h、Drivers/BSP/UI/ui_menu_tab.c、Drivers/BSP/UI/ui_page_registry.h、Drivers/BSP/UI/ui_page_registry.c |
它还会回写 MDK-ARM/STM32F407-Demo.uvprojx 与 simulator/CMakeLists.txt,两份注册表一起比 |
tools/font_gen/extract_cn.py |
ui_cn_chars.txt |
唯一不按裸字节判的一项,见 §7 |
tools/font_gen/make_font.py |
Drivers/BSP/UI/ui_font_cn_12.c |
需 node + 全局 lv_font_conv + 系统宋体,缺则 SKIP |
tools/arch_diagram/gen_*.py(逐个) |
docs/images/*.svg |
每个生成器各开一个临时目录,互不污染 |
判据是逐字节相等,不依赖文件头注释里写没写「自动生成」。生成器一律用 __file__ 的三级
父目录定位仓库根,所以拷进临时目录后它只会写临时目录——仓库一个字节都不写:不需要还原
现场,也不会 stash 别人在飞的改动(多人共用一棵工作树也能安全跑)。
2. 为什么要它
- 实证一(本卡的由来):
0f7b5a87(编号迁移)改了tools/menu_gen/gen_menu.py的模板 文字却没重跑生成物 → HEAD 里Drivers/BSP/UI/ui_menu_tab.h与模板不一致,后来是别人重跑 生成器才被顺带纠正(属主确认以生成器输出为准)。 - 实证二(2026-10-26 接线前实测扫出):5 张
docs/images/*.svg与各自生成器不一致, 全部是文档挪目录后scripts/rename_ref.py改写了tools/里的引用路径、产物没跟着重跑 (docs/images/项目架构总览.svg/docs/images/配置文件分层架构.svg/docs/images/RemoDispBus帧格式.svg/docs/images/物理分区布局.svg/docs/images/存储栈分层.svg;逐条差异原因见脚本内BASELINE)。同一天 14 个配图生成器里 9 个是干净的——所以不是「所有配图都别信」,而是只有改过脚本的那批会漂,恰好是没人会去 看 diff 的那批。 - 事故形态值得记清楚:改生成器的人看不到任何 diff(脚本改动编译、单测、像素回归全过, 因为产物没变),代价全部转嫁给下一个碰巧重跑生成器的人——他要么把这段陌生改动连带提交, 要么 revert 掉别人真正的修复。两种处置都错,而门禁能让他不必遇到这个选择。
- 本钩子为什么刻意「只读」(同一批接线过程中复核到的另一件事,一并写下来,后来人需要知道):
pre-commit 的 run 子命令在不带
--all-files也不带--files时会进入 staged_files_only 上下文 (安装目录里 commands/run.py 的判定就是 stash = not args.all_files and not args.files;进上下文后先 起一个 backup patch,钩子跑完再用 git checkout -- . 加 git apply 还原)。两个后果: ① 假红——钩子看到的是「没有你未暂存改动」的那一份树:拆笔提交时(工作区还留着下一笔要提的 改动)生成物与它的输入会被拆成两代,任何「重跑比对」类判定都会报出一段本不存在的漂移; ② 危险——那本质上是一次全树 checkout 回滚,靠补丁恢复,别人在同一棵树上在飞的改动经这一趟 就不再是原样(与本仓「绝不用 git checkout 丢弃未提交工作」是同一条铁律)。 所以本钩子的实现约束是硬性的:不写仓库、不改受管区、比对基准取工作区,代价是它自己永远不需要 还原现场,也因此可以在多人共用的工作树上随便跑。通用机制正文 →docs/代码规范/pre-commit使用说明.md§2「stash 陷阱」。
3. 触发面
files: 覆盖四类(pre-commit 4.x 已移除 always: true,没有兜底开关,必须写全):
任何生成器 tools/ 下全部、生成器的输入(Drivers/BSP/UI/ 源码、App/task/ui_task.c 与
App/task/ui_task.h、ui_menu_tree.txt、ui_cn_chars.txt)、生成物(docs/images/*.svg +
两份注册表)、以及 scripts/check_generated_fresh.py 与 .pre-commit-config.yaml 自身
(不覆盖自身 = 谁改坏这根闸的 files: 它就再也不跑且无人察觉,同 registration-check 的口径)。
扫描范围恒为工作区全量(pass_filenames: false:重跑一遍与「你改了哪个文件」无关)。
4. 报错怎么读
[FAIL] 生成器幂等性 ... menu-gen
---- 漂移文件 Drivers/BSP/UI/ui_menu_tab.h(生成器 tools/menu_gen/gen_menu.py)
@@ -58 +58 @@
-bool ui_menu_tab_has_children(uint8_t node);
修法:重跑该生成器,然后把生成物与生成器**同一笔**提交
- 多人同树时怎么验证接线(本会话实测口径):带
--files才不触发 stash ——& '.venv\Scripts\python.exe' -m pre_commit run generated-fresh --files .pre-commit-config.yaml; 只核对配置文件本身用-m pre_commit validate-config .pre-commit-config.yaml。别跑不带--files也不带--all-files的裸 run(会 stash 别人在飞的改动,见 §2 末条)。 - 单独跑:
& '.venv\Scripts\python.exe' scripts/check_generated_fresh.py(--verbose打差异行、--only menu-gen,cn-font只跑指定项、--list只列声明、--prune打基线台账、--selftest自证有牙、--keep-temp保留临时目录排查)。 - 钩子 entry 为什么不能只是裸 python(2026-10-26 真实提交实测,同日修):本钩子的 entry 与其它本地
钩子一样写
python scripts/check_generated_fresh.py,而 pre-commit 在提交环境里把python解析成了 机器级解释器,那次提交日志原文:[FAIL] cn-font tools/font_gen/make_font.py 退出码 1:ERROR: No module named fontTools, 解释器行打印的是 C:/Python314/python.EXE。纯标准库的menu-gen/cn-chars照旧绿——这是一根 只在字库链上假红的闸,换任何一台没把.venv排在 PATH 前面的机器或 runner 都会重犯(同族第二次 发作见scripts/ci_local.py文件头 2026-10-11 记的「L0 全灭」)。修法不是把 entry 写死成.venv里的路径(那正是 2026-10-11 那次把候选收窄、等于把门禁绑死在一台机器上的错),而是脚本自己按优先级 挑解释器并逐个探针校验:显式DTU_GEN_PYTHON> 当前解释器 > 仓库.venv> PATH 上探到的机器级, 且必须真能import fontTools才算数(装了 pre-commit 不等于装了 fontTools;Microsoft Store 那个python占位程序连探针都跑不起来)。整轮生成器统一用同一个解释器跑——换解释器可能换输出格式, 逐字节比对要的是同一把尺子。一个都挑不出来时打[ENV]+ 候选探针表 + 修 var / 重建.venv的指引, 并退出码 2,不静默 SKIP。钩子自身跑在坏解释器上时会先打一行[WARN]说明改用哪个(别把这条 WARN 读成钩子坏了)。 [BASELINE]= 存量在册(§7),不拦提交;[TIGHTEN]= 这条已还清、可以删基线了。[SKIP]只可能出现在字体链:那台机器造不出Drivers/BSP/UI/ui_font_cn_12.c,等于没验, 别读成「验过了」。- ⚠ 别拿
--only的输出判断「有没有还清存量」:[TIGHTEN]提示只在未指定--only时打印。
5. 好处
- 把「生成物与生成器同一笔提交」从口头约定 + 事后靠别人重跑纠正变成提交期机械判定;
- 顺带把双端注册表纳入同一判定:
gen_menu.py会把绑定页写进 uvprojx 与 CMakeLists。 漏注册在registration-check那一头是「文件存在但没注册」,在这一头是「重跑会改注册表」, 两侧独立兜同一件事——不同判据、同一事故。 --selftest自带两项牙(都不往仓库注入假改动):① 临时副本里改一行模板 → 必须报 DIFF;② 拿一个 「跑得起但看不见第三方库」的假解释器(当前解释器加-S关掉 site-packages)去要字体链 → 必须给出 可诊断的[ENV]报错与修法(退出码 2),而不是崩在 ModuleNotFoundError 上。第 ② 项就是上面那次 假红的回归用例。
6. 限制与盲区(请当正文读)
-
只覆盖
GENS里登记过的生成器。没登记的(tools/refgraph/、tools/netlist_semantics/,以及scripts/gen_task_table.py的受管表区——后者已有task-table钩子)不在闸内。新增生成器不登记 = 本钩子沉默,这是它最大的盲区。 -
输入靠声明(每项一份
seed清单)。漏声明不会假绿——生成器在临时目录里读不到东西会直接 报错(FAIL),但它以「生成器报错」的形态出现,容易被误读成生成器坏了。 -
判定基准是工作区而不是 git 索引:别人在飞的改动会被当成输入。这是刻意的(工作树才是 事实),但意味着 UI 源码改到一半时本钩子可能报「字库比源码旧」——那是真没重跑,只是 还没轮到你重跑;正确处置是跑完生成器再提交,不是跳过。
-
逐字节相等不校验「生成物内容对不对」:模板本身写错、重跑照抄,本钩子全绿。
-
字体链 SKIP 见 §4:换机或 runner 没装 lv_font_conv 时,那一项目前是空的。
-
基线只覆盖
docs/images/下的 SVG。业务生成物(菜单表 / 页面注册表 / 字库 / 两份注册表) 一条基线都没有——这里没有豁免面可钻。 -
解释器探针每轮只跑一次并缓存。
DTU_GEN_PYTHON指向一个装了 fontTools 但版本古怪的解释器时本钩子 照用它——真因若出在解释器换输出格式,症状是所有生成物一起漂,先看输出开头那行解释器再查生成器。 -
未跟踪文件不在 stash 覆盖面里 ⇒ 别人的半成品会拖红你的提交(2026-10-26 实测,本钩子最容易 误伤的一条,机制对所有钩子通用)。pre-commit 的 stash 只清已跟踪文件的未暂存改动 (
staged_files_only._unstaged_changes_cleared():git diff-index取补丁 →git checkout -- .→ 跑完git apply),未跟踪文件压根不在diff-index里,git add -N也只是撤下索引、文件本体 还在盘上。本钩子按设计以工作区为基准(§6 第 3 条),于是只要树里躺着尚未入库的 UI 页面 实例,任何碰Drivers/BSP/UI/的提交都会看到下面这段红:[FAIL] menu-gen tools/menu_gen/gen_menu.py 退出码 1:ERROR: 以下页面实例未被任何叶子 #page 引用(写了却从不生效的死代码): ui_page_clock.c, ui_page_reset.c [GAP] 清单缺 1 字(下次重跑会加进来 → 「不是我改的」diff): 元 [FAIL] cn-chars txt 与生成链不等(源码在用 172 ∪ KEEP_EXTRA 39 = 184,清单 183)红的内容是对的——那两个页面实例确实是「写了却从不生效的死代码」,那个字确实该进清单; 但账不该记在这笔提交上:提交的是别人的批次,红的是第三方的半成品。正解只有两条: ① 让 owner 先把叶子绑进
ui_menu_tree.txt、重跑gen_menu.py与 font-regen,再按批次排序提交; ② 确实要抢跑就--no-verify,并在正文点名是哪几个未跟踪文件拖的红。 不要为了绿去放宽files:或把Drivers/BSP/UI/摘出触发面——那等于扔掉这条闸最值钱的保证 (「新页面实例必须与生成物同笔提交」正是目前唯一能机械拦住这种死代码的地方)。机制正文 →docs/代码规范/pre-commit使用说明.md§2.2「stash 覆盖面」行。
7. 豁免与基线(只减不增)
- 存量:脚本内
BASELINE,形状 =(产物路径, 生成器路径, 漂移成因一句话),接线当天钉死 5 条 (§2 实证二)。还清一条删整条:默认运行会打印[TIGHTEN]点名「已还清仍在册」,--prune打印整张台账。同构layer-deps的 BASELINE 与registration-check的 KNOWN_GAPS: 没有--write一类往基线加条目的开关,加一条要写清理由并过评审。 - 唯一例外判定:
ui_cn_chars.txt。按 font-regen 约定它 = 源码在用 ∪KEEP_EXTRA(历史落盘记录用字,逐条理由只在scripts/check_font_charset.py一份),裸字节比对必然不等。 故该项改判「txt 恰等于 重跑结果 ∪ KEEP_EXTRA」,两向都不许差:少一字 = 下次重跑会加进 txt(就是「不是我改的」diff);多一字 = 下次重跑会把它洗掉(字形不可再生,历史/log/selftest.log那批记录渲染空白且无日志无报错)。清单 importlib 直载,不留第二份。 与font-charset的分工要分清:那条查白框方向的覆盖(源码在用 ⊆ txt ⊆ 字体 cmap), 本条查 txt 与生成链是否同一份(多一个未登记的字也 FAIL,那条只 WARN)。 - 实测基线(2026-10-26 接线):
menu-gen70 个文件逐字节一致 /cn-chars源码在用 165 ∪ KEEP_EXTRA 39 = 183 字、两向无差 /cn-font逐字节一致(59,138 B)/docs-svg14 个生成器 = 9 一致 + 5 存量在册 → 退出码 0。
8. 改这个门禁本身
- 加生成器:
GENS加一项(声明seed输入 +mkdir输出目录)→ 同笔更新本页 §1 的表与docs/文档规范.md§4.4 的在册表;先跑--only <新id> --verbose确认干净再合。 - 声明
seed要按生成器实际读了什么写,不是按「我觉得它是输入」:gen_menu.py会os.listdir(Drivers/BSP/UI)找页面实例,所以整个 UI 目录 + 两份注册表必须一起拷。 - 临时目录一律
tempfile.mkdtemp():任何「顺手写回仓库」的改动都不要加——本钩子的全部安全性 来自它不写仓库(写了就要还原,还原就要面对别人在飞的改动)。 - 自测:
--selftest。探针字符串若在生成器里被改名,自测会直接报「探针已不存在」,不会假过。
9. 变更记录
| 日期 | 变更 |
|---|---|
| 2026-10-26 | 补 §6 第 8 条(本窗口最值钱的一条机制):pre-commit 的 stash 只覆盖已跟踪文件的未暂存改动(git diff-index 取补丁 → git checkout -- .),未跟踪文件不进 stash、git add -N 也只撤索引 —— 于是别人的半成品(实测 ui_page_clock.c / ui_page_reset.c 未绑 #page)会把任何人碰 UI 的提交拖红;红的内容对、账不该记在别人身上。附现场日志与两条正解,明确禁止为绿放宽 files:。通用机制同步写进 docs/代码规范/pre-commit使用说明.md §2.2 |
| 2026-10-26 | 补记:§2 与 §4 补上「entry 为什么不能只是裸 python」的同一条实测线(解释器解析 + 探针校验),与本页 §9 上一行同批 |
| 2026-10-26 | 同日修假红:entry 的裸 python 在提交环境里被解到机器级解释器(无 fontTools),字体链 |
一项报 No module named fontTools——跑了一次真实提交才暴露。现由脚本按 ci_local 的优先级解析生成器 |
|
解释器并逐个探针校验(新增显式变量 DTU_GEN_PYTHON),挑不出来打 [ENV] + 候选表 + 修法、退出码 2 不 |
|
SKIP;--selftest 加「假解释器必须可诊断」第二项。判据与基线未变 |
|
| 2026-10-26 | 首篇逐项文档(task-18 接线当日):实证 = 0f7b5a87 改模板没重跑 + 接线前实测 5 张 docs/images/*.svg 已漂(钉成只减不增基线);在册 4 项 = gen_menu / extract_cn / make_font / tools/arch_diagram 全量配图;接线实跑 menu-gen 70 文件一致、字库链两向无差、14 个配图生成器 9 干净 5 在册 → PASS |