fix(门禁): 批 5 追溯器口径补齐——变更记录行不参与双向比对(未改任何号)

结论先行:我上一轮说「批 2 模块码单词化漏扫了 4 篇属主文档」——**被证据推翻**

- 依据是 check_test_traceability.py 报的 DOC-ONLY 38 处里有 8 处旧模块码
  (TC-AI-070/080/090、TC-PH-001/030、TC-TS-001、TC-VL-001),我据此写好了
  逐号点名的改名映射表准备落盘
- 改名工具干跑 **0 处命中**:查下去这 8 处**全部在变更记录行**(| 2026-10-10 | … |)里,
  写的是**当年**的用例名;按编号规范 §8.1 冻结历史不回改,旧号靠别名解析——
  而 TC-AI-070 → TC-ADCIN-070 这批**早在批 2 就已登记在 id_aliases.json**
- 所以照原计划改等于**销毁历史记录、收益为零**。真缺口是判据:追溯器是唯一
  没有「变更记录 / [示例] / [计划] 行豁免」的一支(check_ids / rename_ids /
  doc-links 都有),它把冻结历史当活引用 → 系统性假阳性

改了什么(判据,不改数据)
- check_test_traceability.py 加 SKIP_LINE(与另三支脚本同源的豁免正则),两侧采集
  都改为逐行判定,被豁免的号收进 frozen_ids 留痕,--verbose 全量列出——
  **豁免不等于已对应**,别拿它当收敛
- 顺带把 has_intent() 的判定范围从整文件收窄到**同号所在行**(意图必须写在号旁边,
  原先整文件里任何位置有人话就算过关,太松)

实测(关键是"没吞掉真对应")
- DOC-ONLY 38 → **29**(−9 全是冻结行假阳性,**不是真变好**);总差异 45 → 36
- **交集 260 → 260 不变**:豁免只吐冻结行,没有让任何一条真实对应消失
- 代码侧 578 个号不变(识别面没缩,这是改名/改判据最容易踩的坑)
- 错误级 632 不变(追溯不参与错误级计数);可判定比例 83.02%;--strict 通过

剩 29 处是真问题,但**不是编号问题**(要人裁决,不由门禁决定)
- 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)**全库无任何载体**:文档声明了用例而代码/台架/模拟器都没有实现

另一个我自己的错误,记进 §8.3 而不只是提交信息
- 补别名时把 8 个键写到了 JSON **顶层**(真容器是 aliases,515 条)→ 条目完全无效;
  是 assert k not in d['aliases'] 拦下的,删掉无效键后 id_aliases.json 字节与 HEAD 一致
- 教训:DOC-ONLY 这类两侧字面比对差异,先判它是不是冻结行/别名可解析,再谈改数据;
  改名工具 0 命中与 assert 失败都是**信号**,不是小故障

文档:docs/编号规范.md §8.1 口径修正记录 + §8.3 批 5 行与经过段(含被推翻的判断)+ 变更记录;
docs/代码规范/pre-commit使用说明.md §2.25 顺带收编行同步;脚本 docstring 同步

验证:check_doc_links.py --all 143 篇 0 失效;id-check --strict 通过;三个改动文件
BOM=否、CR=0;未改 C/H,不需 @build;台账无需收账(本轮 0 处销账)
This commit is contained in:
2026-10-21 13:25:06 +08:00
parent 52976bdec3
commit 43fbfa140b
3 changed files with 55 additions and 7 deletions
+29 -6
View File
@@ -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