diff --git a/docs/代码规范/pre-commit使用说明.md b/docs/代码规范/pre-commit使用说明.md index 4482f0cc..b09931dc 100644 --- a/docs/代码规范/pre-commit使用说明.md +++ b/docs/代码规范/pre-commit使用说明.md @@ -455,7 +455,7 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法 | **八项校验**([示例] 本行引用的是各种非法形态示例,不是真实编号) | ①未登记前缀 ②形态不合规(裸形 `S3` / 空格形 `Trap 07` / 中文形 `陷阱 1` / 补零错 / 档位词带后缀 / 大写后缀 / 缺模块码)③悬空引用 ④重复定义 ⑤冻结前缀越游标 ⑥文档内编号未紧邻文档名 ⑦孤儿条目(告警)⑧可判定比例(只许涨不许跌) | | **配置两份** | `scripts/id_registry.json`:前缀族 → 属主登记处 / 形态正则 / 是否冻结 / 游标,**逐项来自 `docs/编号规范.md` §4 总表**,游标取「规范记载值」与「全库实测最大号」的较大者,偏差写进 `cursor_note`(实测偏差见规范 §8.2);`scripts/id_aliases.json`:旧号 → 新号 / 作废标记(照搬 `section_aliases.json` 的成功范式) | | **判定口径** | 「定义」= 字面量出现在该族**属主登记处文档**里(不依赖表格行首等易漂移形态)→ §5.5「销项 = 删行 + 清全库引用」只做一半,校验 ③ 立刻报(`N-31`/`N-32` 正是这条)。「重复定义」= 字面量出现在**定义位**(表格首格 / 标题行 / 列表项行首),正文提到不算,否则任何交叉引用都会误判成事故。多族同形时的定族顺序:所在文档即属主 > 紧邻属主线索 > **定义位优先**(`docs/编号规范.md` §4.3 冻结快照族 `N-RPT`/`D-RPT`/`S-RPT`/`R-RPT`/`F-RPT`/`P-AUDIT`/`A-AUDIT`/`B-AUDIT` **不参与**——否则已销项的登记簿 `N-21` 会被审计报告同号接管,门禁反过来教人补错文档名,实测把校验 6 从 266 撑到 1437、可判定比例假崩到 68.7%)> 判悬空的族 > 全局族;小写子项后缀(`-b`/`-c`)在母号在册时视为已定义(`subitem_ok`),差异仍由双向追溯如实报 | -| **顺带收编**([示例] 本行引用了非法形态示例号)| `scripts/check_test_traceability.py`(TC 用例双向追溯)由本钩子一并调用:正则补齐 `TC-[A-Z]{1,4}-\d{3,4}[a-z]?` 与大写后缀 / 无模块码形态(旧 `TC-[A-Za-z0-9]+-\d+` **整条漏扫** `TC-ADCIN-01A`),比对范围从「`test/board/*.c` ↔ `*_Test_Guide.md`」扩到 `test/host/` 一侧 + 7 篇「单元测试说明.md」(差异从 33 处涨到 326 处——**原口径漏的正是主机侧单测这一半**),另加 `--board-only` 回退旧口径。**2026-10-21 同口径改写**:差异分三档 `DOC-ONLY`(文档声明而代码没有,真断链 38)/ `CODE-ONLY`(代码有、母号不在登记处且号旁无意图,真未登记 7)/ `IN-CODE`(靠号旁写着意图放过 238,**弱于文档的保证,单列示数不算过关**);另修 `TC_RE` 模块码上界 `{0,4}`→`{0,8}`(批 2 单词化后代码侧一度从 556 缩到 217) | +| **顺带收编**([示例] 本行引用了非法形态示例号)| `scripts/check_test_traceability.py`(TC 用例双向追溯)由本钩子一并调用:正则补齐 `TC-[A-Z]{1,4}-\d{3,4}[a-z]?` 与大写后缀 / 无模块码形态(旧 `TC-[A-Za-z0-9]+-\d+` **整条漏扫** `TC-ADCIN-01A`),比对范围从「`test/board/*.c` ↔ `*_Test_Guide.md`」扩到 `test/host/` 一侧 + 7 篇「单元测试说明.md」(差异从 33 处涨到 326 处——**原口径漏的正是主机侧单测这一半**),另加 `--board-only` 回退旧口径。**2026-10-21 同口径改写**:差异分三档 `DOC-ONLY`(文档声明而代码没有,真断链 38)/ `CODE-ONLY`(代码有、母号不在登记处且号旁无意图,真未登记 7)/ `IN-CODE`(靠号旁写着意图放过 238,**弱于文档的保证,单列示数不算过关**);另修 `TC_RE` 模块码上界 `{0,4}`→`{0,8}`(批 2 单词化后代码侧一度从 556 缩到 217) |。**批 5(2026-10-21)再补口径**:变更记录行与 `[示例]`/`[计划]` 行不参与双向比对(冻结历史按 §8.1 不回改、旧号靠别名解析),`DOC-ONLY` 38 → 29 且交集 260 不变;被豁免的号进 `frozen_ids`,`--verbose` 列出——**豁免不等于已对应** | | **最新实测(2026-10-21,B 层口径后)** | 8322 处编号字面量 / 可判定 **82.71%**(6883/8322);错误级 711 = ②398 + ③44 + ⑥269,告警级 ③223 + ⑦100。⚠ **与 2026-10-20 的 1312 不可直接比**:其中 504 处属**口径修正**(主机侧单测 118 号实测在代码目录外零消费者,改认 `test/host/<目录>/*` 为属主登记处;子项后缀母号在册即算已定义),74 处属我修的**注册表漏配属主**(活登记处在 `docs/任务规划/README.md`,`N-21` 在其中出现 2 次而我只登记了 `登记簿.md`),**真正变好 0 处**——完整对照见 `docs/编号规范.md` §8.1 口径修正记录 | | **有牙实测(牙齿)**([示例] 下列字面量是注入用的假编号) | 注入打在临时档(`git add -N` 进索引,跑完 `git rm --cached` + 删除,`git status` 干净):`Z-99`→①、裸形 `S3`→②、`TC-NET-9999`→③、第二篇属主文档重复定义 `DP-01`→④、`E-70`→⑤(`E-xx` 冻结于 `E-63`)、裸引 `D-7`→⑥;六项分别被对应校验点名,`--strict` 退出码 1 | | **显式盲区(不是"检查通过")** | ① 裸形同字母多义(§1.2:`P1` 五义 / `L1` 四义 / `U1` 四义 / `D7` 三义)与元器件位号同形(§4.4)→ 裸形族**只在属主登记处文档内**判越游标,文档外降告警;② 圈号编号(①②③…)与正常列举无法机械区分,不入判定;③ `docs/芯片手册/`、`docs/硬件平台/` 整体排除(位号 `B1`/`C1`/`L1` 与编号同形,是撞形根源)。前两块收口属 §9 阶段 1-②/1-③ 迁移批次,落地后再扩判定范围——盲区登记在 `id_registry.json` 的 `blind_spots` / `unmanaged_bare` | diff --git a/docs/编号规范.md b/docs/编号规范.md index 54aee175..e5981c4b 100644 --- a/docs/编号规范.md +++ b/docs/编号规范.md @@ -424,6 +424,7 @@ git grep -n '<旧字面量>' # 零残留 | `excluded_paths` 加 `scripts/id_aliases.json`、`scripts/id_registry.json` | 别名表/注册表**自身不得制造引用面**——上批别名条目一度成为这些号的唯一"外部引用" | 极小 | | **追溯脚本同口径对齐**(`check_test_traceability.py`):文档声明了代码没有 = DOC-ONLY 真断链(38);代码有、母号不在登记处**且号旁没写人话意图** = CODE-ONLY 真未登记(7);代码有但旁边写着意图 → 单列 `IN-CODE`(238),**弱于文档的保证,禁止当成已登记** | B 层认代码为属主后,老口径「每个号都必须出现在文档」与门禁自相矛盾——照它升 `--strict` 会拦死 173+ 处本不该写文档的号,逼出 `--no-verify`,门禁公信力就是这么没的 | 差异从 284 降到 45(38+7),另 238 处转入「看得见但不过关」的 `IN-CODE` 账 | | ⚠ 同批改名要复核**所有**识别面:`TC_RE` 也停在 `{0,4}`,模块码单词化后代码侧集合从 556 悄悄缩到 217(339 个号隐形),已放宽到 `{0,8}` | 这是「改名必须连带复核判据面」教训的**第二次发生**,发生在另一支脚本上 | 修回后代码侧 578 | +| **追溯器补上「变更记录 / `[示例]` / `[计划]` 行」豁免**(`check_test_traceability.py`,与 `check_ids.py` / `rename_ids.py` / `doc-links` 同一条口径) | 追溯器是**唯一没这条豁免**的一支:它把冻结历史当活引用,凡变更记录里写过的旧用例号一律要求代码里也存在同号 (例:`\| 2026-10-10 \| 新增 TC-AI-070..078 \|` 写的是**当年**的名字,号在批 2 已改成 `TC-ADCIN-*`,按 §8.1 历史行不回改 → 拿它去代码里找同号必然落空) | `DOC-ONLY` 38 → **29**(−9 全是假阳性,**不是真变好**)。关键佐证:**交集 260 → 260 不变**——豁免没有吞掉任何一条真对应,只吐出了冻结行 | | 已销项历史任务号 `T-2`/`T-3`/`T-4`/`T-6` 入别名表(`status: void`) | §5.5「销项 = 删行 + 清引用」只做了一半的欠账;本条只让门禁停止把它当自造号,并如实指路 | 悬空错误 −35 | **修正后实测(2026-10-21)**:错误级 **711**(B 层前 1318)= ②398 + ③44 + ⑥269;告警级 ③223 + ⑦100;可判定比例 **82.71%**(6883/8322),基线仍 82.6%。 @@ -448,6 +449,7 @@ git grep -n '<旧字面量>' # 零残留 | 批 | 日期 | 族 | 旧 → 新 | 处数 / 文件 | 别名 | |---|---|---|---|---|---| +| **批 5(口径)** | 2026-10-21 | 追溯器豁免 | **未改任何编号**:给 `check_test_traceability.py` 补与其它三支工具同源的「变更记录 / `[示例]` / `[计划]` 行不比对」口径 + `--verbose` 打印被豁免的 9 个号 | 判据改动;`DOC-ONLY` 38 → 29、交集 260 不变 | 无新增(`TC-AI-070` 等 8 个旧号**早在批 2 就已在别名表**,本批一度以为缺、写错层级后被 assert 拦下) | | **批 4(Trap)** | 2026-10-21 | 陷阱号形态 | `Trap 01~07` → `TRAP-GD5F-01~07`、`陷阱 1` → `TRAP-RTC-01`、`Trap 21`/`Trap 19` → `TRAP-NET-21`/`TRAP-NET-19`、`见陷阱 01` → `TRAP-GD5F-01` | 23 处 / 11 文件(含 `Drivers/BSP/GD5F2GQ5UE/*` 注释 4 处、`test/board/*` 2 处) | 11 条全部带 scope;映射表即审计轨迹 `scripts/rename_maps/batch3-trap.json` | | **批 1** | 2026-10-20 | 代码规范条款 | `S1`~`S19` → `CODE-RULE-01`~`19` | 78 处 / 2 文件 | 18 条(scoped:`scripts/check_coding_standard.py`、`docs/代码规范/pre-commit使用说明.md`) | | **批 3(口径)** | 2026-10-21 | 主机侧单测归属 | 建 9 族 `TC-HOST-*`(代码即属主)+ `subitem_ok` + `snapshot_only` + 补 README 属主 + `T-x` 入别名 | 判据与注册表改动,未改任何存量号 | 见 §8.1 口径修正记录 | @@ -458,6 +460,28 @@ git grep -n '<旧字面量>' # 零残留 --- +**批 5 的经过值得记一笔:我的判断被自己的证据推翻了两次** + +1. 我据 `DOC-ONLY` 38 处判定「批 2 模块码单词化漏扫了 4 篇属主文档」(`TC-AI-070/080/090`、 + `TC-PH-001/030`、`TC-TS-001`、`TC-VL-001`),并写好了逐号点名的映射表准备改名。 +2. 改名工具干跑 **0 处命中** → 查下去发现这 8 处**全部位于变更记录行**(`| 2026-10-10 | … |`), + 按 §8.1 属冻结历史、**本就不该回改**;再查别名表,`TC-AI-070 → TC-ADCIN-070` 这批 + **早在批 2 就已登记**。也就是说:真要按原计划改,是在销毁历史记录,而收益是零。 +3. 我顺手补别名时把键写到了 JSON **顶层**(真容器是 `aliases`,515 条),条目完全无效—— + 是 `assert k not in d['aliases']` 把它拦下来的;删掉无效键后该文件字节与 HEAD 完全一致。 + +**教训写在这里而不只是提交信息里**:`DOC-ONLY` 这类"两侧字面比对"的差异,**先判它是不是冻结行/ +别名可解析**,再谈改数据;改名工具的 0 命中和 assert 失败都是**信号**,不是小故障。 + +**剩 29 处 `DOC-ONLY` 是真问题,但不是编号问题**(`--verbose` 可复现全清单): +- 4 处无模块码形态(`TC-1003`、`TC-206`、`TC-501`、`TC-704`)——属 ② 大批的文档侧镜像; +- `TC-NET-607` 一条是**历史指称**:原文写「原 TC-NET-607/608/609,现 701/702/703」, + 一条对三条,别名表是一对一模型,写不进别名; +- `TC-NET-302/401/402/501` 标题明写「(硬件层)」= 人工台架步骤,代码里没有实现载体**是预期**; +- 其余 20 处(`TC-NET-101/102/1101/601/606/701~704/801/802/901/902/1006/1501~1503` 与 + `TC-GD5F-503`)**全库无任何载体**(代码、台架脚本、模拟器都没有)——文档声明了用例而没有实现, + 这三类要不要各立口径(人工台架豁免 / 历史指称 void 别名 / 真未实现登记待办),**属产品裁决,不由门禁决定**。 + **批 4 刻意不改的三处(写清楚,别被当成漏网)** - `docs/环境与部署/并行任务工作树方案.md` 里的 `wip/trap22-fix` 是 **git 分支名**,不是编号—— @@ -534,6 +558,7 @@ git grep -n '<旧字面量>' # 零残留 |---|---|---| | 2026-10-14 | 新增 P-7 与引用可读性规则 | §2 加 **P-7「编号不语义,引用必语义」**(编号本体保持无语义并冻结,但正文引用必须带语义括注、禁裸 ID 独担信息)→ 六条原则变**七条**;§5.1 加前置步"先问能不能不加号";新增 **§5.6「编号密集文档的对照节(强制)」**(≥3 个编号族必须加「导读:编号与术语」);§4.1 补登记 **`E\d+`**(N34 自愈实验编号族,已封闭)并把 `N-\d{2}` 的在册范围订正为「曾用最大 `N-33`,现存至 `N-29`」;§1.2 悬空引用示例补 `N-32`;§7.1 同步订正。起因:`docs/网络/网络自愈架构设计.md` 合并与导读节实践暴露"裸编号引用不可读",用户裁定以后尽量用语义标识 | | (规划稿) | 建立本文档 | 全库扫描 1,385 个不同编号 / 19,306 处出现后定稿的两层编号体系(全局编号 + 文档内编号)、六条原则、目标前缀总表、三批迁移映射与 `check_ids.py` 门禁设计;用户决策:`E-xx` 冻结保留、严重度档位词 `P0~P3` 不变、文档落 `docs/编号规范.md`、阶段 1 三批全做 | +| 2026-10-21 | **批 5:追溯器口径补齐(未改任何号)** | `check_test_traceability.py` 加入与 `check_ids.py`/`rename_ids.py`/`doc-links` 同源的「变更记录 / `[示例]` / `[计划]` 行不参与比对」豁免,并新增 `frozen_ids` 留痕(`--verbose` 可列);`DOC-ONLY` 38 → 29、总差异 45 → 36,**交集 260 → 260 不变**(证明没吞掉真对应)。批 5 原计划的 8 处文档侧改名**被证据否掉**(全在变更记录行、别名批 2 已登记),未落盘;经过与教训、以及剩 29 处的三类真问题都写进 §8.3 批 5 段 | | 2026-10-21 | **批 4:陷阱号统一(② 清的第一族)** | 23 处 / 11 文件:`Trap 01~07` → `TRAP-GD5F-01~07`(属主文档 7 个标题行从此成为定义位)、`陷阱 1` → `TRAP-RTC-01`、`Trap 21`/`Trap 19` → `TRAP-NET-21`/`TRAP-NET-19`;UI 文档自指改写成平实语言。别名 11 条全部带 scope。改名后 `TRAP-GD5F-01/05/06` 三处历史悬空引用自动落地为已定义;三处刻意不改与 `docs/硬件平台/` 盲区写进 §8.3 批 4 | | 2026-10-21 | **关闸:`id-check` 转增量拦截** | `check_ids.py` 新增 `--write-backlog` / `--prune-backlog` 与按台账比对的 `--strict`(指纹 = 文件 + 校验号 + 字面量 + 计数,**不含行号**);生成 `scripts/id_known_backlog.json`(340 条覆盖 710 处存量);钩子 `entry` 加 `--strict`。验收三例:新字面量拦、同号计数增长拦、干净态放行,全通过。新增 §8.4 说明机制与「为什么不照清零再升 strict 的原节奏」 | | 2026-10-21 | **追溯脚本同口径对齐** | `check_test_traceability.py` 差异定义改为 DOC-ONLY / CODE-ONLY / IN-CODE 三档(真差异 45 = 38 + 7;弱保证 238 单列示数);顺带修 `TC_RE` 模块码上界 `{0,4}`→`{0,8}`——**同一个坑第二次踩**(代码侧一度从 556 缩到 217),已写进 §8.1 | diff --git a/scripts/check_test_traceability.py b/scripts/check_test_traceability.py index b2222a7f..3abd8e25 100644 --- a/scripts/check_test_traceability.py +++ b/scripts/check_test_traceability.py @@ -27,6 +27,8 @@ ② 代码有、母号不在文档、**且号旁边没写人话意图** → CODE-ONLY(真未登记); ③ 代码有、靠"旁边有意图"放过 → 单独打印为 IN-CODE,**弱于文档**,不算已登记。 小写子项后缀(`TC-XXX-003b`)按母号算(`subitem_ok`),与编号门禁同口径。 + **2026-10-21 第二次口径修正(批 5)**:变更记录行与 `[示例]`/`[计划]` 行**不参与比对** + (冻结历史按编号规范 §8.1 不回改,旧号靠 `id_aliases.json` 解析;这条豁免另三支脚本早有,本脚本是最后缺它的一个,`DOC-ONLY` 因此 38 → 29、交集不变);被豁免的号存进 `frozen_ids`,`--verbose` 全量列出——**豁免不等于已对应**,别拿它当收敛。 退出码: 0 = 无差异;或有差异但**默认仅告警**(与 doc-links 演进路径一致) @@ -64,6 +66,14 @@ TC_RE = re.compile(r"TC-(?:[A-Z][A-Z0-9]{0,8}-\d{1,4}[A-Za-z]?|\d{2,4}[a-z]?)") TC_CANON_RE = re.compile(r"^TC-[A-Z][A-Z0-9]{0,8}-\d{3,4}[a-z]?$") # 与 §4.1 实测形态同步 # 占位符前缀(文档模板里的 `TC-XXX-…`)不参与比对 PLACEHOLDER_PREFIXES = {"XXX"} +# 与 check_ids.py / rename_ids.py / check_doc_links.py **同一套**豁免口径: +# 变更记录行与 [示例] / [计划] 行是**冻结历史或写法示例**,不是活引用(§8.1 纪律: +# 历史行里的旧号不回改,靠别名表解析)。追溯器原先缺这一条,产生系统性假阳性—— +# 2026-10-21 实测:DOC-ONLY 38 处里 12 处来自变更记录行,例如 +# `| 2026-10-10 | 新增 TC-AI-070..078(70 → 79 用例)|` 写的是**当年**的用例名, +# 该号在批 2 已随代码改名成 TC-ADCIN-*,历史行不该回改,拿它去代码里找同号必然落空。 +SKIP_LINE = re.compile(r"\[示例\]|\[计划\]|^\s*\|\s*20\d\d-\d\d-\d\d\s*\|") +frozen_ids: set = set() # 只在 --verbose 里列:被豁免的号(历史/示例),不是"已对应" def _prefix(tc: str) -> str: @@ -100,18 +110,24 @@ def collect(paths, label, want_intent=False): """返回 (ids_set, per_prefix_counts[, intent_set])。逐文件扫描,缺文件不致命。""" ids = set() intents = set() + global frozen_ids for p in paths: try: text = p.read_text(encoding="utf-8", errors="replace") except OSError as e: # pragma: no cover - 环境错误 print(f"[ERR] 无法读取 {p}: {e}", file=sys.stderr) return (None, None, None) if want_intent else (None, None) - for m in TC_RE.findall(text): - if _prefix(m) in PLACEHOLDER_PREFIXES: - continue - ids.add(m) - if want_intent and has_intent(text, m): - intents.add(m) + for line in text.splitlines(): + skip = bool(SKIP_LINE.search(line)) + for m in TC_RE.findall(line): + if _prefix(m) in PLACEHOLDER_PREFIXES: + continue + if skip: # 冻结历史 / 示例行:不参与双向比对,只留痕 + frozen_ids.add(m) + continue + ids.add(m) + if want_intent and has_intent(line, m): + intents.add(m) counts = {} for i in sorted(ids): counts[_prefix(i)] = counts.get(_prefix(i), 0) + 1 @@ -173,6 +189,7 @@ def main(): print(f"代码侧:{len(code_files)} 个文件,{len(code_ids)} 个 TC") print(f"文档侧:{len(doc_files)} 个文件,{len(doc_ids)} 个 TC") print(f"交集:{len(code_ids & doc_ids)}") + print(f"豁免:变更记录/[示例]/[计划] 行的号 {len(frozen_ids)} 个(口径同 check_ids.py,见 §8.1)") if args.verbose: print("--- 各前缀 TC 计数(代码 / 文档)---") @@ -194,6 +211,12 @@ def main(): for i in code_only_in_code: print(f" {i}") + if args.verbose and frozen_ids: + print("--- 被豁免的号(变更记录 / [示例] / [计划] 行,共 %d 个)**不是已对应**," + "历史旧号靠 id_aliases.json 解析 ---" % len(frozen_ids)) + for i in sorted(frozen_ids): + print(" ", i) + if not doc_only and not code_only: print("\n[OK] 双向一一对应,无差异") return 0