docs(门禁): 抢跑等价门禁自查表落地——把「谁漏跑一次,这道检查这次就不存在」写死

今天三次 --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] 输出。
This commit is contained in:
2026-10-26 23:42:41 +08:00
parent b1280e3cd1
commit 41c277a8ae
2 changed files with 96 additions and 14 deletions
+73 -14
View File
@@ -88,16 +88,13 @@ git commit
| 机制 | 口径 |
|---|---|
| **两个阶段** | pre-commit 阶段 39 个钩子 + commit-msg 阶段 `gitlint`;`default_install_hook_types: [pre-commit, commit-msg]` 是显式声明的——换机后 `pre-commit install` 才会带上 commit-msg 钩子 |
| **一根钩子在输出里出现两次** | 未声明 `stages:` 的钩子**默认属于所有阶段**,pre-commit 与 commit-msg
两遍都会列出它;第二遍的"文件"是提交信息文件,几乎必然 `(no files to check)Skipped`。**只看
输出结尾会把"实跑通过"读成"没跑"**——判某钩子跑没跑要认它**第一次**出现的那行(实测
2026-10-24:只改 `.pre-commit-config.yaml` 的提交里 `test-coverage` 与 `bsp-err` 都是 Passed,
尾巴那一遍是 Skipped) |
| **stash 陷阱** | pre-commit 跑钩子前会 stash 未暂存改动,钩子看到的是"没有你中间状态"的树。**改代码 + 同时更新基线/受管区的,必须先 `git add` 再跑钩子**,否则会拿到假报警(本仓踩过,`docs/门禁说明/bsp-err.md` §4 记了现场) |
| **两个阶段** | pre-commit 阶段 **49** 个钩子 + commit-msg 阶段 `gitlint`(口径 = YAML 解析后的 hook 条目数 **50** 减去 gitlint;自查命令与「声明 / 文档 / 实跑」三数同源核对 → `docs/门禁说明/README.md` §2);`default_install_hook_types: [pre-commit, commit-msg]` 是显式声明的——换机后 `pre-commit install` 才会带上 commit-msg 钩子 |
| **一根钩子在输出里出现两次** | 未声明 `stages:` 的钩子**默认属于所有阶段**,pre-commit 与 commit-msg 两遍都会列出它;第二遍的"文件"是提交信息文件,几乎必然 `(no files to check)Skipped`。**只看输出结尾会把"实跑通过"读成"没跑"**——判某钩子跑没跑要认它**第一次**出现的那行(实测 2026-10-24:只改 `.pre-commit-config.yaml` 的提交里 `test-coverage` 与 `bsp-err` 都是 Passed,尾巴那一遍是 Skipped) |
| **stash 陷阱** | pre-commit 跑钩子前会 stash 未暂存改动,钩子看到的是"没有你中间状态"的树。**改代码 + 同时更新基线/受管区的,必须先 `git add` 再跑钩子**,否则会拿到假报警(本仓踩过,`docs/门禁说明/bsp-err.md` §4 记了现场)。**它藏不住什么,见下一行** |
| **stash 覆盖面:只覆盖已跟踪文件的未暂存改动**(2026-10-26 读源码定案,**对所有钩子通用**) | 机制在 pre-commit 的 `staged_files_only._unstaged_changes_cleared()`:`git diff-index <tree>` 取补丁 → `git checkout -- .` → 跑完 `git apply` 还原。所以「钩子实跑 = 只看到 HEAD + 本笔 staged」这句**对已跟踪文件成立、对未跟踪文件不成立**:未跟踪文件压根不在 `diff-index` 里,**不会被藏**;`git add -N`(intent-to-add)也救不了——pre-commit 只 `git rm --cached` 把它撤下索引(`_intent_to_add_cleared()`),**文件本体仍在盘上**。后果:别人写了尚未入库的文件(新页面实例 / 新单测目录 / 新文档)躺在树里时,**任何一笔碰到该文件所属钩子 `files:` 的提交都会被这份半成品拖红**——红的内容没错,但账不该记在这笔提交上。实测:`ui_page_clock.c`、`ui_page_reset.c` 处于未跟踪且未绑 `#page` 期间,`generated-fresh` 对**所有人**的 UI 提交都会 FAIL(现场日志 `docs/门禁说明/generated-fresh.md` §6 第 8 条)。三条结论:① 提交排序——会让全树变红的半成品要么先落地(绑引用 + 重跑生成器)要么先挪出工作树;② 凡「判定基准取工作区」的钩子(重跑比对类、全库统计类)在多人同树下必须配**静默窗口**;③ 真要 `--no-verify` 抢跑,正文必须点名是哪几个**未跟踪**文件拖的红,别让它被读成「钩子不可信」 |
| **`always: true` 已废** | pre-commit 4.x 对 `local` 钩子里这个键**只告警 `Unexpected key(s)` 然后忽略** → "想总是跑"只能把 `files:` 写全(教训 → §5.5) |
| **改写型与只读型的豁免不对称** | YAML 锚点 `&cubemx-generated` 只豁免 `trailing-whitespace` 与 `end-of-file-fixer`;`cppcheck` / `no-mojibake` / `cubemx-survival` / `task-table` / `mem-numbers` **仍覆盖** `Src/`、`Inc/` |
| **增量拦截三段式** | 规则恒生效 + 存量钉在基线/台账 + `--prune` 点名"已还清、请删行";**脚本刻意没有 `--write`**(不提供"一条命令把现状洗成合法"的入口)。使用者:`layer-deps`(6)、`bsp-err`(38)、`id-check`(台账 640 处,随 `--prune-backlog` 变动) |
| **增量拦截三段式** | 规则恒生效 + 存量钉在基线/台账 + `--prune` 点名"已还清、请删行";**脚本刻意没有 `--write`**(不提供"一条命令把现状洗成合法"的入口)。使用者(2026-10-26 实跑核对):`layer-deps`(**基线内 3** + ALLOW 永久豁免 3,共见 6 处)、`bsp-err`(38 条)、`id-check`(工作区台账已还清 = **0 条指纹 / 0 处**;HEAD 那版是 303 条指纹 / 651 处)。原写「layer-deps(6)」「id-check 台账 640 处」都是没跟着还账更新的旧数 |
| **没有 `.map` 即跳过** | `mem-numbers` / `mem-budget` 在无 Keil 构建的机器(含远端 CI)上是**跳过**,不是"通过"——别把跳过读成有余量 |
| **配置自己坏 = 全部静默不跑** | 改完 `.pre-commit-config.yaml` 的验收不是过 `check-yaml`,而是实跑 `-m pre_commit run --all-files`,确认目标钩子**出现在输出里**且状态不是 `Skipped` |
| **范围策略** | 第三方与生成物只靠 `.gitignore` 与顶层 `exclude`(不在各钩子里重复写目录名);权威口径 → `AGENTS.md` 提交门禁一节 + §5.5 |
@@ -107,8 +104,69 @@ git commit
- 提交前等价自查(L0 + L0b + L0.5):`scripts/ci_local.py`(用 `.venv` 解释器跑)。
- 单独跑某一条:`-m pre_commit run <hook-id> --files <改动的文件>`;全库:`--all-files`。
- 临时跳过只在紧急情况,且必须在提交说明里写明跳过了什么、为什么、随后怎么补(`--no-verify` 会跳过全部 39 条,包括 `gitlint`)。
- 临时跳过只在紧急情况,且必须在提交说明里写明跳过了什么、为什么、随后怎么补——**跳之前先跑 §2.4 自查表**(`--no-verify` 一次跳过 49 条,连 `gitlint` 一起跳,见 §2.2「两个阶段」行的 49/50 口径)。
### 2.4 抢跑自查表(`--no-verify` 之前自己跑一遍:只读、不 stash、别人正在写盘也安全)
`--no-verify` 跳过的不是「一条格式检查」,是**全部 49 条**(外加 commit-msg 阶段的 `gitlint`)。
2026-10-26 本仓连着抢了三笔,放走三条本该被拦的真问题,都在提交正文里可复核:
| 抢跑那笔 | 本该由谁拦下 | 后果什么时候才看得见 |
|---|---|---|
| `e7136cbe` | `coding-standard`(5 条 CODE-RULE-17:模块 `.c`/`.h` 配对)+ `end-of-file-fixer`(2 个文件缺尾换行) | 靠下一笔补;还**漏提**了 `test/host/console_log/run_unit_test.py` ⇒ HEAD 里代码已改成「整行独占」、旁边 runner 的文档串却还写着「无锁直发」——**比少一条断言更坏**,下一个人会被它教出错误心智模型 |
| `1d4b6a17` | `scripts/stack_report.py`(**不在钩子里,只能靠人跑**):控制台「整行独占」给 `dbg_console` 共享尾段加深 80 B 帧 → `iec104Task` 静态最坏链 1,544、margin **1,016 < 1,024** ISR 底线 → 退出码 1(实测差 8 B 过线) | **这是安全边界,不是风格问题**。2026-10-26 21:3x 用本机 20:39 那次构建的 `.htm` 复跑,`iec104Task` 仍是 `1016 / 39% / FAIL`、脚本退出码 1 ⇒ **这笔债还挂着**(谁来还:抬 `iec104Task` 栈要过 `mem-budget` 与 `docs/内存管理/` 台账,或把日志帧深压回去) |
所以纪律不是「以后别抢跑」,而是:**跳闸前把下面 A 组自己跑一遍**——它们全部只读、不 stash、别人正在写盘时
也能安全跑。对照 §2.2「stash 覆盖面」那一行:真提交会把**你自己**未暂存的改动藏起来,所以手跑通常比真提交
**更严**(不是更松);而别人**未跟踪**的半成品两边都藏不住 ⇒ 红了先认是谁的文件,既别替人背账,也别顺手把
别人的账混进你这一笔。
#### A. 抢跑前必跑(每条都是实跑验证过的入口)
| 查什么 | 命令(仓库根执行) | 判据 / 退出码 |
|---|---|---|
| **栈与最坏链**(今天漏的就是这条) | `& '.venv\Scripts\python.exe' scripts/stack_report.py` | 判据 `margin = cfg − max(静态最坏, 运行峰值) ≥ max(1024 B ISR, 30% × cfg)`;**0=全过 / 1=有任务不过 / 2=解析失败** |
| RAM 静态占用 | `& '.venv\Scripts\python.exe' scripts/ram_report.py` | 涨了的段要能指到具体模块与函数 |
| RAM 总量红线(`mem-budget` 的入口) | `& '.venv\Scripts\python.exe' scripts/ram_baseline.py --gate` | >92% 即 1;⚠ **0 包含「没有 `.map` 就跳过」——跳过不等于有余量** |
| 代码规范(含 CODE-RULE-17 / 20) | `& '.venv\Scripts\python.exe' scripts/check_coding_standard.py <本笔的 .c/.h>` | 有违规即 1 |
| BOM 漂移 | `& '.venv\Scripts\python.exe' scripts/check_bom_drift.py --all` | HEAD 有 BOM 而工作区没有 → 1;⚠ **别用「有 BOM 就是错」的外行判据**——`Drivers/BSP/UI/*`、`App/task/ui_task.c` 等本就该带 BOM(§2.2 与 AGENTS 铁律) |
| 乱码 U+FFFD | `& '.venv\Scripts\python.exe' scripts/check_mojibake.py <本笔文件>` | 出现即 1 |
| **字节卫生 = 改写型钩子的只读等价** | 下面的一行式 + `git diff --check` | 一次查完**待提交清单(含未跟踪新文件)**的 CR / 缺尾换行 / U+FFFD / 行尾空格,全程不写盘 |
| 受影响模块的主机单测 | `& '.venv\Scripts\python.exe' test/host/<模块>/run_unit_test.py` | 只跑你碰的那几套;**改过骨架** `test/host/common/unit_test_harness.py` 就要跑全套(等价 22 根钩子同时触发) |
| 全量单测 + 覆盖率 | `& '.venv\Scripts\python.exe' scripts/check_test_coverage.py` | 0=全过且覆盖率未回退 / 1=有失败或回退 / 2=环境(缺 `cl.exe` / mscoverage) |
| 编号 | `& '.venv\Scripts\python.exe' scripts/check_ids.py --strict` | 增量拦截:台账之外新错即 1;顺带核「可判定比例」与「不可判定绝对数」两行(2026-10-26 余量只剩 12 处) |
| 文档链接与章节 | `& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all` | 0 通过 / 1 有问题(钩子还多带 `--sections-strict`) |
| 生成器幂等 | `& '.venv\Scripts\python.exe' scripts/check_generated_fresh.py` | 重跑到临时目录逐字节比对;**必须 `.venv` 解释器**,否则字库链假红 → `docs/门禁说明/generated-fresh.md` §4 |
| 一把梭(上面多数静态项 + 主机单测) | `& '.venv\Scripts\python.exe' scripts/ci_local.py` | 非 0 就别抢跑(但它**不含** `stack_report` / `ram_baseline`,数字与安全边界要另跑) |
> ⚠ **这张表为什么必须存在**:`stack_report` 与 `ram_baseline --gate` 这类**根本不在钩子清单里**(它们要 Keil 构建产物,
> 只能靠人在 `@build` 后跑),所以「人漏跑一次」直接等于「这道检查这一次不存在」。想机械化,先要解决「磁盘上的
> `.htm` / `.map` 属于最后一次构建的人」这个归因问题——与 §2.2「stash 覆盖面」那条同源,不是加一根钩子就完事
> (`mem-budget` 今天红在「台账与 `.map` 不同码」正是同一个形状)。
一行式字节卫生(**整条刻意写成纯 ASCII**——粘进 PowerShell 与存成 `.ps1` 两种方式实测都过;含中文版在 Windows PowerShell 5.1 下按 ANSI 读 `.ps1` 会碎成语法错误,所以别加中文标签)。
实测 2026-10-26:全树 39 个待提交文件(**含 `??` 未跟踪**)→ `problems 0`,退出码 0:
```powershell
& '.venv\Scripts\python.exe' -c "import subprocess as sp,os,sys;st=sp.run(['git','status','--porcelain'],capture_output=True,text=True,encoding='utf-8',errors='replace').stdout.splitlines();ext=('.c','.h','.py','.md','.txt','.yaml','.json');fs=[l[3:].strip().strip(chr(34)) for l in st if l[3:].strip().endswith(ext)];fs=[p for p in fs if os.path.isfile(p)];bad=[];[bad.append((p,''.join(t).strip())) for p in fs for b in [open(p,'rb').read()] for t in [[('CR ' if b'\r' in b else '')+('NOEOL ' if not b.endswith(b'\n') else '')+('FFFD ' if b'\xef\xbf\xbd' in b else '')+('TWS ' if any(x.endswith(b' ') or x.endswith(b'\t') for x in b.split(b'\n')) else '')]] if t[0]];print('byte-hygiene: checked',len(fs),'files (incl untracked), problems',len(bad));[print(' ',p,'=>',t) for p,t in bad]"
```
#### B. 手跑**代替不了**真提交的三类,抢跑时得自己补
| 类别 | 为什么不等价 | 抢跑时怎么做 |
|---|---|---|
| **改写型钩子** `trailing-whitespace` / `end-of-file-fixer` | 它们会**改文件**。真提交里改的是 stash 之后的树;而手跑 `--files` **不 stash**(§2.2 那两行),直接落在工作区——**列了别人在飞的文件就等于替人改文件** | 只列自己的文件:`-m pre_commit run <id> --files <自己的文件>`;或先用 A 组的一行式查出问题自己修,别让钩子替你动盘 |
| **要靠 stash 才能正确归因**的判定:`mem-numbers` / `mem-budget`(读磁盘 `.map`)、`stack_report`(读磁盘 `.htm`)、`ui-golden` / `uinav-selftest`(要模拟器 exe 与台架锁)、`test-coverage`(会编译,可能撞别人的 `.obj`) | 磁盘上的 `.map` / `.htm` / `.exe` 属于**最后一次构建的人**,不是你的 ⇒ 手跑红常常是替别人背账(今天 `mem-budget` 就红在「台账与 `.map` 不同码」,因为 `.map` 里混着三条在飞源码道) | **照样要跑**(宁可多跑),但**红先归因**:`git status --porcelain` 看是谁在飞、`.htm` 的时间戳是哪次构建;确认不属本笔就在提交正文点名,**不要改基线数字去凑绿** |
| **只有真提交才会碰到的面**:`gitlint`(commit-msg 阶段)与未声明 `stages:` 会跑两遍的钩子 | 手跑脚本查不到提交信息规范(标题 ≤100、**正文每行 ≤80**) | 提交信息一律 `git commit -F <UTF-8 无 BOM 文件>` 并自查最长行;或先老实提交一次让 `gitlint` 说话,再改成 `--no-verify` |
#### C. 抢跑之后(三条都来自今天的真事,不是设想)
1. **对交付清单**:`git show --stat --oneline HEAD` 与提交正文声明的文件**逐条对上**;再 `git status --porcelain`,
里面**不该再出现你声明过的文件**(含 `??` 未跟踪)——「漏提 runner」正是这一步才会发现的。
2. **正文点名三件事**:哪些门禁没跑、为什么没跑、当时谁在飞。今天三笔都写了,**保留为强制**,不是可选项。
3. **静默窗口补一次全量**:`& '.venv\Scripts\python.exe' -m pre_commit run --all-files`。⚠ `--all-files`
**同样不 stash**,而且会让**改写型钩子当场改全库文件**——只能在确认没人写盘的窗口跑,跑完 `git status`
复核被改动的是不是只有你那几笔(别人在飞时跑它 = 替人改文件,比重跑生成器更危险)。
---
## 3. 日常工作流
@@ -172,7 +230,9 @@ git commit -m "紧急修复" --no-verify
SKIP=cppcheck git commit -m "紧急修复"
```
> ⚠️ **不推荐**,仅用于紧急修复。跳过后的代码应在下一次提交中补上检查。
> ⚠️ **不推荐**:`--no-verify` 一次跳过 49 条(连 `gitlint` 一起),不是「跳过格式检查」。**跳之前必须先跑 §2.4 抢跑自查表**
> (尤其 `stack_report.py`——它不在钩子清单里,只有人会跑;数字与安全边界类问题不会自己冒出来)。跳之后按 §2.4 C 组
> 补齐三件事:`git status` 对交付清单、正文点名「哪些没跑 / 为什么 / 谁在飞」、静默窗口补一次 `run --all-files`。
---
@@ -236,7 +296,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
| 文件 | 位置 | 说明 |
|---|---|---|
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**40 个钩子**,2026-10-24 清点:新增 `test-coverage` 覆盖率总闸)。逐项详解在 `docs/门禁说明/`(索引 `docs/门禁说明/README.md`;主机侧单测 16 个合一篇,含各套件来历;UI 侧另有 `font-charset` 与 `ui-golden` 两篇——后者此前**在旧手册里连小节都没有**,只活在配置注释里,本次补齐) |
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**50 个钩子**;口径 = YAML 解析后的 hook 条目数,自查命令 → `docs/门禁说明/README.md` §2。原写「40 个,2026-10-24 清点」是当时的快照数、之后加钩子没同步——正是 §2.2 计数口径行要避免的事)。逐项详解在 `docs/门禁说明/`(索引 `docs/门禁说明/README.md`;主机侧单测 **22 根**合一篇,含各套件来历与「18 套无专属钩子」台账 `docs/门禁说明/host-unit-tests.md` §6;UI 侧另有 `font-charset` 与 `ui-golden` 两篇——后者此前**在旧手册里连小节都没有**,只活在配置注释里,本次补齐) |
| `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) |
| `.cubemx_survival_baseline.json` | 仓库根 | CubeMX 易失定制基线(哈希;`--write` 重建) |
| `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 |
@@ -250,8 +310,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
| `scripts/check_font_charset.py` | scripts/ | UI 字符集差集检查(复用 `tools/font_gen/` 提取与 cmap 解析规则,防缺字白框) |
| `scripts/check_layer_deps.py` | scripts/ | 分层方向门禁(BSP 反向 include App 的增量拦截,存量基线只减不增) |
| `scripts/check_bsp_err.py` | scripts/ | BSP 错误码与超时命名门禁(三条规则的增量拦截,存量 19 条基线只减不增;`--emit-baseline` 建基线) |
| `docs/代码规范/嵌入式C语言代码规范_V1.0.md` | docs/代码规范/ | 代码规范原文(CODE-RULE-01~CODE-RULE-20 规则来源;`CODE-RULE-20` 出处不在规范正文而在
AGENTS.md 平台约束,见 `docs/门禁说明/coding-standard.md`) |
| `docs/代码规范/嵌入式C语言代码规范_V1.0.md` | docs/代码规范/ | 代码规范原文(CODE-RULE-01~CODE-RULE-20 规则来源;`CODE-RULE-20` 出处不在规范正文而在 AGENTS.md 平台约束,见 `docs/门禁说明/coding-standard.md`) |
| `docs/代码规范/cppcheck使用说明.md` | docs/代码规范/ | cppcheck 钩子详解(规则/配置/抑制) |
| `docs/文档规范.md` | docs/ | 文档体系规范(链接形态/命名/门禁要求) |
| `docs/代码规范/pre-commit使用说明.md` | 本文档 | 部署与使用说明 |
@@ -265,7 +324,7 @@ AGENTS.md 平台约束,见 `docs/门禁说明/coding-standard.md`) |
全库扫描: pre-commit run --all-files
单钩子扫描: pre-commit run <hook-id> --all-files
扫描单个文件: pre-commit run <hook-id> --files <path>
跳过钩子: git commit --no-verify (紧急)
跳过钩子: git commit --no-verify (紧急;跳之前先跑 §2.4 自查表)
查看已安装钩子: ls .git/hooks/
卸载钩子: pre-commit uninstall
更新钩子版本: pre-commit autoupdate
+23
View File
@@ -136,6 +136,27 @@
**一条基线都没有**——这里没有豁免面可钻。
7. 解释器探针每轮只跑一次并缓存。`DTU_GEN_PYTHON` 指向一个装了 fontTools 但版本古怪的解释器时本钩子
照用它——真因若出在解释器换输出格式,症状是**所有生成物一起漂**,先看输出开头那行解释器再查生成器。
8. **未跟踪文件不在 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. 豁免与基线(只减不增)
@@ -168,6 +189,8 @@
| 日期 | 变更 |
|---|---|
| 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 不