0f7b5a87fb
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号) 一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、 采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。 门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏 undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8 reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF; id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。 test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字, 是本批唯一改变构建产物的文件集合。 为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged 部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、 不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。 实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01% (基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 / 告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。 基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处 引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使 识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。 ⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二 根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有), 今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。 固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0; App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889 断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿; check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带 BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。 .cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法: Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。 同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%, RW_CCM 57,376 B / 87.9%,阈值 92%。
258 lines
11 KiB
Python
258 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""编号批量改名器(docs/编号规范.md §7.3 规划的 scripts/rename_ids.py)。
|
||
|
||
为什么必须有这个工具,而不是查找替换:
|
||
1. 编号是**跨文件**引用的——一次改名同时涉及属主登记处、引用方文档、代码注释、
|
||
门禁脚本文案;手改必漏(§3.6 给"改名"立规矩同此理由)。
|
||
2. 改完必须自动登记 `scripts/id_aliases.json`(旧号 → 新号),否则历史提交正文与
|
||
未改写的文档里的旧号会集体变成"悬空引用"(§8 校验 3)——别名表是改名的**前置条件**,
|
||
不是可选项。
|
||
3. 同一个裸形字面量在不同文件里可能是**不同族**(实测同一裸形既是代码规范条款、又是
|
||
UI 像素回归状态、又是 2026-10-02 复审专项条目)→ **改名与别名都必须限定作用域**,
|
||
全局替换会制造新的撞号(正是 §1.2 记录的「销项号被重新取走」那类事故)。
|
||
|
||
用法:
|
||
& '.venv\\Scripts\\python.exe' scripts/rename_ids.py --map .dsh/rename_batch1.json # 干跑(默认)
|
||
& '.venv\\Scripts\\python.exe' scripts/rename_ids.py --map .dsh/rename_batch1.json --apply # 落盘 + 写别名表
|
||
可选 --only-family 族名 --no-alias --list 每处改动的 文件:行 旧→新
|
||
|
||
映射表 JSON:
|
||
{"batches":[{"family":"CODE-RULE",
|
||
"old":"\\\\bS(?P<n>1?[0-9])\\\\b", # 必须含命名组,供 new 模板取值
|
||
"new":"CODE-RULE-{n:02d}", # str.format 模板,数字组自动转 int
|
||
"files":["scripts/xxx.py", "docs/**/*.md"], # 显式路径或 glob(作用域即边界)
|
||
"note":"为什么改、批次"}]}
|
||
|
||
不动的东西(刻意):
|
||
- 含 `[示例]` / `[计划]` 的行(与 check_doc_links.py / check_ids.py 共用同一套豁免约定)
|
||
- 变更记录表行(`| 2026-10-13 | …`)——历史快照按 §8.1 不改写,旧号靠别名表解析
|
||
- 第三方与生成物目录(Lib/、Middlewares/、docs/芯片手册/、docs/硬件平台/ 等)
|
||
编码:按原字节读写(保留 BOM 与行尾),只做 ASCII 子串替换。
|
||
退出码:0 = 成功(干跑或已落盘);1 = 映射表/正则有问题;2 = 环境错误。
|
||
"""
|
||
|
||
import argparse
|
||
import fnmatch
|
||
import json
|
||
import pathlib
|
||
import re
|
||
import sys
|
||
|
||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||
ALIASES_FILE = REPO_ROOT / "scripts" / "id_aliases.json"
|
||
|
||
# 与 scripts/id_registry.json 的 excluded_paths 同口径(改名同样不该碰这些语料)
|
||
EXCLUDED = [
|
||
"Lib/", "Middlewares/", "Drivers/STM32F4xx_HAL_Driver/", "Drivers/CMSIS/",
|
||
"MDK-ARM/RTE/", "docs/芯片手册/", "docs/内存管理/data/",
|
||
"simulator/tests/ref/", "node_modules/", "build/", "ci-artifacts/",
|
||
]
|
||
SKIP_LINE = re.compile(r"\[示例\]|\[计划\]|^\s*\|\s*20\d\d-\d\d-\d\d\s*\|")
|
||
|
||
|
||
def norm(path):
|
||
"""统一成仓库相对、正斜杠的路径,便于 glob 与别名 scope 一致。"""
|
||
return str(path).replace("\\", "/")
|
||
|
||
|
||
def tracked_text_files():
|
||
import subprocess
|
||
out = subprocess.run(["git", "ls-files", "-z"], cwd=str(REPO_ROOT),
|
||
capture_output=True)
|
||
files = [norm(x) for x in out.stdout.decode("utf-8").split("\0") if x]
|
||
keep = []
|
||
for rel in files:
|
||
if any(rel.startswith(p) for p in EXCLUDED):
|
||
continue
|
||
if rel.endswith((".png", ".jpg", ".jpeg", ".gif", ".bin", ".ico", ".pdf",
|
||
".zstd", ".map", ".axf", ".htm")):
|
||
continue
|
||
keep.append(rel)
|
||
return keep
|
||
|
||
|
||
def match_files(patterns, candidates):
|
||
hit = []
|
||
for pat in patterns:
|
||
p = norm(pat)
|
||
exact = p.lstrip("./")
|
||
if exact in candidates:
|
||
if exact not in hit:
|
||
hit.append(exact)
|
||
continue
|
||
for rel in candidates:
|
||
if fnmatch.fnmatch(rel, p) and rel not in hit:
|
||
hit.append(rel)
|
||
return hit
|
||
|
||
|
||
def render(template, groups):
|
||
"""按模板渲染新号。**只有模板带格式说明符时**才把数字组转 int——
|
||
否则 `TC-PROTSET-011` 会被渲染成丢前导零的形态(实测差点毁掉整批补零)。"""
|
||
vals = {}
|
||
for k, v in groups.items():
|
||
if v is None:
|
||
vals[k] = ""
|
||
elif v.isdigit() and re.search(r"\{%s:[^}]+\}" % re.escape(k), template):
|
||
vals[k] = int(v)
|
||
else:
|
||
vals[k] = v
|
||
return template.format(**vals)
|
||
|
||
|
||
def apply_batch(batch, candidates, args):
|
||
"""返回 (改动列表, 别名映射, 错误列表)。改动列表元素 = (rel, lineno, old, new)。"""
|
||
errs, edits, alias_map = [], [], {}
|
||
try:
|
||
rx = re.compile(batch["old"])
|
||
except re.error as exc:
|
||
errs.append("族 %s 的正则无法编译:%s" % (batch.get("family"), exc))
|
||
return edits, alias_map, errs
|
||
if "n" not in rx.groupindex and "(?P<" not in batch["old"]:
|
||
errs.append("族 %s 的 old 正则缺命名组,new 模板无法取值" % batch.get("family"))
|
||
return edits, alias_map, errs
|
||
files = match_files(batch.get("files", []), candidates)
|
||
if not files:
|
||
errs.append("族 %s:files 作用域没匹配到任何在库文件(检查 glob)"
|
||
% batch.get("family"))
|
||
for rel in files:
|
||
p = REPO_ROOT / rel
|
||
raw = p.read_bytes()
|
||
bom = raw.startswith(b"\xef\xbb\xbf")
|
||
try:
|
||
text = raw.decode("utf-8-sig")
|
||
except UnicodeDecodeError:
|
||
errs.append("%s 不是 UTF-8,拒绝改名" % rel)
|
||
continue
|
||
nl = "\r\n" if "\r\n" in text else "\n"
|
||
lines = text.split(nl)
|
||
changed = 0
|
||
for i, line in enumerate(lines, 1):
|
||
if SKIP_LINE.search(line):
|
||
continue
|
||
|
||
def sub(m):
|
||
new = render(batch["new"], m.groupdict())
|
||
edits.append((rel, i, m.group(0), new))
|
||
alias_map.setdefault(m.group(0), set()).add(new)
|
||
return new
|
||
|
||
newline_ = rx.sub(sub, line)
|
||
if newline_ != line:
|
||
lines[i - 1] = newline_
|
||
changed += 1
|
||
if changed and args.apply:
|
||
body = nl.join(lines)
|
||
data = body.encode("utf-8")
|
||
p.write_bytes((b"\xef\xbb\xbf" + data) if bom else data)
|
||
return edits, alias_map, errs
|
||
|
||
|
||
def merge_aliases(new_aliases, batch):
|
||
"""把本批 `旧号 → 新号` 并进 id_aliases.json,带 scope(同字面量多义的关键)。"""
|
||
doc = json.loads(ALIASES_FILE.read_text(encoding="utf-8"))
|
||
table = doc.setdefault("aliases", {})
|
||
# 已**完全限定**的字面量(如 `TC-PROTSET-011`)可以全局认别名;裸形必须带 scope
|
||
scope = [] if batch.get("global_alias") else [norm(x) for x in batch.get("files", [])]
|
||
added = 0
|
||
for old, news in sorted(new_aliases.items()):
|
||
scope_txt = ("旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||
if scope else "全库生效(本批字面量已完全限定,无歧义)")
|
||
entry = {
|
||
"to": sorted(news)[0] if len(news) == 1 else sorted(news),
|
||
"status": "renamed",
|
||
"note": "%s(scripts/rename_ids.py 批:族 %s)|%s"
|
||
% (batch.get("note", "批量改名"), batch.get("family"), scope_txt),
|
||
}
|
||
if scope:
|
||
entry["scope"] = scope
|
||
cur = table.get(old)
|
||
if cur is None:
|
||
table[old] = entry
|
||
added += 1
|
||
else:
|
||
entries = cur if isinstance(cur, list) else [cur]
|
||
if not any(e.get("scope") == scope and e.get("to") == entry["to"]
|
||
for e in entries):
|
||
entries.append(entry)
|
||
table[old] = entries[0] if len(entries) == 1 else entries
|
||
added += 1
|
||
if added:
|
||
doc["note"] = (doc.get("note", "") +
|
||
" 带 scope 的条目只在所列文件内生效(同一裸形字面量可能属多族,"
|
||
"全局别名会把真撞号洗成已解析)。").strip()
|
||
ALIASES_FILE.write_text(json.dumps(doc, ensure_ascii=False, indent=2) + "\n",
|
||
encoding="utf-8", newline="\n")
|
||
return added
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--map", dest="mapfile", required=True, help="映射表 JSON")
|
||
ap.add_argument("--apply", action="store_true", help="真正落盘(默认只干跑)")
|
||
ap.add_argument("--list", action="store_true", help="逐处打印 文件:行 旧→新")
|
||
ap.add_argument("--only-family", help="只跑指定族")
|
||
ap.add_argument("--no-alias", action="store_true", help="不写别名表(调试用)")
|
||
args = ap.parse_args()
|
||
|
||
try:
|
||
plan = json.loads(pathlib.Path(args.mapfile).read_text(encoding="utf-8"))
|
||
except (OSError, ValueError) as exc:
|
||
print("[ERR] 映射表读不了:%s" % exc, file=sys.stderr)
|
||
return 2
|
||
batches = plan.get("batches", [])
|
||
if args.only_family:
|
||
batches = [b for b in batches if b.get("family") == args.only_family]
|
||
candidates = tracked_text_files()
|
||
print("映射表 %d 批;候选文本文件 %d 个;模式:%s"
|
||
% (len(batches), len(candidates), "落盘" if args.apply else "干跑(不加 --apply 不写盘)"))
|
||
|
||
all_errs = []
|
||
grand = {}
|
||
for b in batches:
|
||
edits, alias_map, errs = apply_batch(b, candidates, args)
|
||
all_errs += errs
|
||
files = {}
|
||
for rel, _ln, old, new in edits:
|
||
files.setdefault(rel, []).append((old, new))
|
||
pairs = sorted({(o, n) for _r, _l, o, n in edits})
|
||
print("\n=== 族 %s:%d 处 / %d 个文件;%d 个不同旧号"
|
||
% (b.get("family"), len(edits), len(files), len(pairs)))
|
||
for rel, xs in sorted(files.items(), key=lambda kv: -len(kv[1])):
|
||
print(" %-56s %2d 处 例 %s → %s"
|
||
% (rel, len(xs), xs[0][0], xs[0][1]))
|
||
if args.list:
|
||
for rel, ln, old, new in edits:
|
||
print(" %s:%d %s → %s" % (rel, ln, old, new))
|
||
if not args.no_alias and self_apply_ok(args, b, alias_map):
|
||
n = merge_aliases(alias_map, b)
|
||
print(" 别名表:新增/更新 %d 条(scope 限定)" % n)
|
||
grand[b["family"]] = len(edits)
|
||
|
||
if all_errs:
|
||
print("\n[ERR] 问题 %d 条:" % len(all_errs), file=sys.stderr)
|
||
for e in all_errs:
|
||
print(" - " + e, file=sys.stderr)
|
||
return 1
|
||
print("\n合计:%s;%s" % (", ".join("%s=%d" % kv for kv in grand.items()),
|
||
"已落盘,请接着登记注册表族并重跑 scripts/check_ids.py"
|
||
if args.apply else "干跑结束,确认后加 --apply"))
|
||
print("提醒:改名后必跑 check_ids.py + check_doc_links.py --all + ci_local.py;"
|
||
"改到 test/board 或 Drivers/ 还要 @build。")
|
||
return 0
|
||
|
||
|
||
def self_apply_ok(args, batch, alias_map):
|
||
"""干跑时不写别名表——否则干跑也会污染别名表。"""
|
||
if not args.apply:
|
||
return False
|
||
return bool(alias_map)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
for s in (sys.stdout, sys.stderr):
|
||
if hasattr(s, "reconfigure"):
|
||
s.reconfigure(encoding="utf-8", errors="replace")
|
||
sys.exit(main())
|