gate: 按用户裁定整体退役编号门禁 id-check-new(钩子 52 → 51)

配置:摘掉该钩子,原位置留一条退役注记(不复述数钩子数的字面量)。
YAML 解析口径实测:声明 51 / 带 manual 3 / 提交期实跑 48。

脚本:git mv 进 scripts/archive/id_gate/check_ids_new.py,与已退役的
check_ids.py 同档;docstring 改退役口径,REPO_ROOT 因归档位置深两级
改取 parents[3],失败提示与自述路径同步改写。从归档位置实测:
--self-test 5 正例拦下 / 29 反例放行 rc=0;--worktree 与暂存区模式 rc=0。

文档(七份):AGENTS.md 六处——总数、加钩流水(顺带补上 2026-10-29
漏记的 skill-size 一笔)、枚举项删除、本行相加数、提交期实跑、门禁索引
与主题表两行;门禁说明 README 八处——声明数、实跑数、计数流水、grep
陷阱的举例数、§2 标题、姿态表「只查新增」行、两行索引状态;编号规范
七处——引导块改「本文即唯一事实源」、族总表一处、§8 标题与正文、
§8.5 补第④步、④ 之后无机械牙那段、速查;门禁说明 id-check-new.md
整篇重写为退役留档(查什么 / 落地前的噪声回放 / 退役五条理由 / 手工
跑法 / 此后靠什么 / 认下丢了什么);门禁说明 id-check.md 与归档目录
README、check_ids.py 里「现行门禁 = id-check-new」的指针同步改写。

技能(三份):id-family-semantic-migration 一行、stale-doc-count-sweep
的 pitfalls 与 steps 各一行改手工跑法(steps 第 5 条那笔历史计数例不动)。

此后编号形态没有机器面:判据只剩 docs/编号规范.md §1 三句自问、§4.1
族总表与人工评审;2026-10-14 那类"销项删行后号被重新取走"不再有提交期
警报——这条代价已写进留档,别当成"检查没了随便写"。

纯文档与门禁配置,未跑 @build。全程未 push。
This commit is contained in:
2026-10-29 20:22:47 +08:00
parent 5c64cf0859
commit 51d89cf9d3
13 changed files with 134 additions and 145 deletions
+264
View File
@@ -0,0 +1,264 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""check_ids_new.py — 编号门禁·新增行(**已退役 2026-10-29,用户裁定**;本脚本降为手工报告工具)
退役口径:钩子已从 .pre-commit-config.yaml 摘除(YAML 解析条目 52 → 51),编号形态此后
**没有机器面**——靠 docs/编号规范.md §1 的三句自问 + 评审。留档说明 → docs/门禁说明/id-check-new.md。
本脚本与 check_ids.py(2026-10-28 退役的全库八项校验)同处一档:手工复查「这批改动 / 这段历史
里有没有野生编号」时才跑,跑不跑都不影响提交。
判据(三条,都不依赖任何族清单 / 游标 / 属主登记处):
1. 破折形态(形如 前缀-数字,或 前缀-段-数字):首段不在 KNOWN_DASH → FAIL;
在 RETIRED_DASH(已停发族)→ 只 WARN(不拦,提示改语义短语)。
2. 裸形态(大写字母紧跟数字,如 TIM3 / YX15 / TPAFE5160):前缀不在 KNOWN_BARE → FAIL。
3. 单字母+数字(C12 / U15 / L3 / R3)→ **放行**:元件位号与层次号同形,这是本脚本的已知盲区
(与旧门禁的"裸形盲区"同源,见 docs/门禁说明/id-check-new.md §5)。
用法(仓库根;钩子摘除后没有自动调用者,四条都是手工):
python scripts/archive/id_gate/check_ids_new.py # 查暂存区
python scripts/archive/id_gate/check_ids_new.py --worktree # 查工作区未暂存的改动
python scripts/archive/id_gate/check_ids_new.py --range A..B # 查一段提交的新增行
python scripts/archive/id_gate/check_ids_new.py --self-test # 内建正反例自检(改脚本后必跑)
退出码:0 = 通过 / 仅告警;1 = 新增了未知编号;2 = 环境错误。
豁免:行内含 [示例] / [计划] 的行按冻结口径跳过;确需保留的写法在同一行写 [编号豁免]。
"""
import argparse
import re
import subprocess
import sys
from pathlib import Path
# Windows 控制台默认 GBK,文档行里可能带 emoji(❌/✅)→ 打出来会 UnicodeEncodeError。
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
except (AttributeError, ValueError):
pass
# 归档位置比原位置深两级(scripts/archive/id_gate/)→ 上溯到仓库根要 parents[3]
REPO_ROOT = Path(__file__).resolve().parents[3]
# --- 破折形态:首段白名单(活族 + 标准编号;新增族要先登记到这里,这就是"允许用号的唯一口子")
KNOWN_DASH = frozenset({
"FR", # 功能需求号(docs/产品需求/产品需求设计文档.md §4.1 在册保留)
"NFR", # 非功能需求号
"TC", # 主机侧用例号(已停发,但存量引用最多)
"TRAP", # 陷阱台账(TRAP-NET-xx / TRAP-ADC-xx)
"CODE", # 代码规范条目(CODE-RULE-xx)
"MENU", # 菜单状态(MENU-STATE-xx,像素回归引用)
"UTF", # 编码名(UTF-8)
"RS", # 串行标准名(RS-232 / RS-485)
"SHA", # 摘要算法名(SHA-256)
"CRC", # 校验算法名(CRC-16)
"DMA", # DMA 控制器编号(DMA-2,少量文档写法)
})
# --- 已停发的族:新写号只 WARN(存量引用仍大量存在,硬拦会把正常编辑也挡了)
RETIRED_DASH = frozenset({
"N", "S", "T", "A", "E", "D", "L", "P", "F", "H", "Z", "O", # 登记簿 / 专项 / 层次 / 原则等旧族
"GAP", "DR", "MS", "WI", # 其余已转语义短语的族
})
# --- 裸形态:前缀白名单(外设 / 器件 / 协议 / 保护元件 / 点号;按实测普查枚举,几乎不增长)
KNOWN_BARE = frozenset({
# 外设与内核
"TIM", "ADC", "SPI", "DMA", "USART", "UART", "I2C", "CAN", "GPIO", "NVIC", "APB", "AHB", "EXTI",
"IRAM", "IWDG", "WWDG", "FPU", "SRAM", "CCM", "ROM", "RAM", "FLASH", "POR", "BOR", "PIN",
# 引脚名(PA10 / PB10 / PC4 / PD12 / PE3 / PG9 / PH1 ...)
"PA", "PB", "PC", "PD", "PE", "PF", "PG", "PH",
# 器件 / 模块 / 工具链
"STM", "TPAFE", "CH", "SD", "GD", "W25", "SD2506", "LVGL", "RTOS", "OS", "UV", "VS", "SDL", "PROF",
# 协议 / 标准 / 编码
"IEC", "MODBUS", "TCP", "UDP", "FTP", "SNTP", "SOE", "COS", "CRC", "SHA", "IEEE", "ISO", "UTF", "ARGB", "RGB",
# 串行标准(RS232 / RS485 常写成不带连字符的形式)
"RS", "CAN",
# 遥信 / 遥控点号与保护元件代号(协议数据,不是标识符)
"YX", "YK", "YT", "YH", "OC", "UV", "OV", "CP", "VS", "PF", "SV", "AC", "DC", "IA", "IB", "IC", "UN", "UA", "UB", "UC",
# 统计 / 队列 / 结构计数
"AIN", "CTR", "SQRT", "SQR", "LQFP", "DIV", "SCALE", "INTS", "HEX", "TLV", "LNK", "PHASE", "BALA", "ECCS",
"CN", "LED", "DB", "FAT", "AX", "SUB", "CLS", "IN", "BLE", "PH", "SYS", "TE", "LPN", "FF", "HW", "ZL", "RX",
"WSL", "WIN", "GPXTI", "COT", "WRTC", "RM", "CR", "SM", "IP", "COM", "SOCK", "LBA", "BOOT", "EXTKO", "INT",
# RCC_CSR 复位标志位号(BOR25 / PIN26 / POR27 / SFT28 / IWDG29 / WWDG30)
"SFT",
# UART 采样模式位名(OVER8)
"OVER",
# 录波段文件名:固件生成的 100 个号轮转(WAVE00 到 WAVE99 的 .BIN,App/algo/wave_store.c 拼名
# FTP 根下 /wave/ 目录;文档与日志里都要写这个字面量,2026-10-29 登记)
"WAVE",
})
# --- 前缀后面接 -数字 其实是算术(SIZE-2 / LEN-1),不是编号
ARITH = frozenset({"SIZE", "MAX", "MIN", "LEN", "CNT", "NUM", "IDX", "POS", "PTR", "TOP", "END",
"LOW", "HIGH", "ROW", "COL", "BASE", "MASK", "HALF", "NUM", "VAL", "BIT", "BYTE"})
EXCLUDED_DIRS = (
"Lib/", "Middlewares/", "Drivers/CMSIS/", "Drivers/STM32F4xx_HAL_Driver/",
"docs/芯片手册/", "docs/硬件平台/", "docs/images/", "docs/内存管理/data/",
"MDK-ARM/RTE/", "simulator/SDL2/", "simulator/build/", "simulator/tests/ref/",
".venv/", "ci-artifacts/", "test/board/third_party/", "scripts/rename_maps/",
"tools/netlist_semantics/", ".dsh/skills/", "__pycache__/",
"scripts/archive/", # 2026-10-28 退役的全库编号语料:里面全是编号字面量,天经地义
)
EXCLUDED_FILES = frozenset({
"scripts/check_ids_new.py", # 本脚本自带正反例白名单,不能自我拦截(归档后整目录已在
# EXCLUDED_DIRS 的 scripts/archive/ 里,这行只是历史留笔)
})
EXCLUDED_SUFFIX = (".svg", ".png", ".jpg", ".jpeg", ".gif", ".bin", ".map", ".htm", ".html",
".tsv", ".log", ".o", ".axf", ".hex", ".ico", ".ttf", ".woff", ".pyc",
".sct", ".lin", ".s", ".bat", ".ps1", ".json")
DASH_RE = re.compile(r"(?<![\w/.-])([A-Z]{1,6})((?:-[A-Z0-9]{1,8})*)-(\d{1,3})(?![\w/-])")
BARE_RE = re.compile(r"(?<![\w/.-])([A-Z]{2,6})(\d{1,4})(?![\w/-])")
SKIP_MARKERS = ("[示例]", "[计划]", "[编号豁免]")
def iter_added_lines(spec):
"""产出 (path, lineno, text):只吐新增行(不含 +++ 头)。spec 见 parse_args。"""
if spec["mode"] == "cached":
cmd = ["git", "-c", "core.quotepath=false", "diff", "--cached", "-U0",
"--diff-filter=ACMR", "--no-color"]
elif spec["mode"] == "worktree":
cmd = ["git", "-c", "core.quotepath=false", "diff", "-U0", "--diff-filter=ACMR", "--no-color"]
else:
cmd = ["git", "-c", "core.quotepath=false", "diff", "-U0", "--diff-filter=ACMR",
"--no-color", spec["range"]]
try:
out = subprocess.run(cmd, cwd=str(REPO_ROOT), capture_output=True, text=True,
encoding="utf-8", errors="replace")
except OSError as exc:
print("[ERR] 无法运行 git:%s" % exc, file=sys.stderr)
raise SystemExit(2)
if out.returncode != 0:
print("[ERR] git 返回 %d:%s" % (out.returncode, (out.stderr or "").strip()[:300]),
file=sys.stderr)
raise SystemExit(2)
path = None
for raw in (out.stdout or "").splitlines():
if raw.startswith("+++ "):
path = raw[4:].strip()
if path.startswith("b/"):
path = path[2:]
continue
if raw.startswith("--- ") or raw.startswith("@@"):
m = re.search(r"\+(\d+)", raw) if raw.startswith("@@") else None
lineno = int(m.group(1)) if m else 0
continue
if raw.startswith("+") and path:
yield path, lineno, raw[1:]
lineno += 1
def path_in_scope(rel):
if rel in EXCLUDED_FILES or rel.startswith(EXCLUDED_DIRS):
return False
return not rel.endswith(EXCLUDED_SUFFIX)
def classify_dash(token):
m = DASH_RE.fullmatch(token)
if not m:
return None
prefix = m.group(1)
if prefix in ARITH or m.group(1) + m.group(2) in ARITH:
return None
if prefix in KNOWN_DASH:
return None
if prefix in RETIRED_DASH:
return "warn"
return "fail"
def classify_bare(token):
m = BARE_RE.fullmatch(token)
if not m:
return None
prefix = m.group(1)
if len(prefix) < 2:
return None
if prefix in KNOWN_BARE:
return None
return "fail"
def scan_lines(lines, show_ok=False):
fails, warns = [], []
for rel, lineno, text in lines:
if not path_in_scope(rel):
continue
if any(mark in text for mark in SKIP_MARKERS):
continue
stripped = re.sub(r"0x[0-9A-Fa-f]+", " ", text)
for m in re.finditer(r"(?<![\w/.-])([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-[0-9]{1,3})(?![\w/-])", stripped):
verdict = classify_dash(m.group(1))
if verdict == "fail":
fails.append((rel, lineno, m.group(1), text.strip()[:110]))
elif verdict == "warn":
warns.append((rel, lineno, m.group(1)))
for m in re.finditer(r"(?<![\w/.-])([A-Z]{2,6}[0-9]{1,4})(?![\w/-])", stripped):
if classify_bare(m.group(1)) == "fail":
fails.append((rel, lineno, m.group(1), text.strip()[:110]))
return fails, warns
SELF_TEST_POSITIVE = ["ZZ-9", "NEWFAM-1", "QZ12", "XYZ-3-1", "AB12"]
SELF_TEST_NEGATIVE = ["TC-NET-204", "TRAP-NET-18", "TRAP-ADC-01", "CODE-RULE-20", "MENU-STATE-01",
"FR-UI-2", "UTF-8", "RS-485", "SHA-256", "CRC-16", "TIM3", "YX15", "YK11",
"TPAFE5160", "GD5F2GQ5UE", "CH395F", "SD2506", "STM32F407", "USART1", "PA10",
"C12", "U15", "L3", "R3", "D7", "SIZE-2", "LEN-1", "GPIO_PIN_5", "ADC_CHANNEL_14"]
def self_test():
ok = True
lines = [("t.md", 1, "新增 %s 编号" % t) for t in SELF_TEST_POSITIVE]
lines += [("t.md", 100 + i, "引用 %s 与 %s" % (t, t)) for i, t in enumerate(SELF_TEST_NEGATIVE)]
fails, warns = scan_lines(lines)
hit = {f[2] for f in fails}
for t in SELF_TEST_POSITIVE:
if t not in hit:
print("[FAIL] 自检:正例未被拦下:%s" % t)
ok = False
for t in SELF_TEST_NEGATIVE:
if t in hit:
print("[FAIL] 自检:该放行的被拦了:%s" % t)
ok = False
print("[%s] 自检:%d 个正例全部拦下、%d 个反例全部放行"
% ("OK" if ok else "FAIL", len(SELF_TEST_POSITIVE), len(SELF_TEST_NEGATIVE)))
return 0 if ok else 1
def main(argv=None):
ap = argparse.ArgumentParser(description="只查新增行的新编号(无全库语料)")
ap.add_argument("--worktree", action="store_true", help="查工作区未暂存的改动")
ap.add_argument("--range", metavar="A..B", help="查一段提交(默认查暂存区)")
ap.add_argument("--self-test", action="store_true", help="跑内建正反例自检")
ap.add_argument("--verbose", action="store_true", help="把命中行的原文也打出来")
args = ap.parse_args(argv)
if args.self_test:
return self_test()
spec = {"mode": "range" if args.range else ("worktree" if args.worktree else "cached"),
"range": args.range}
fails, warns = scan_lines(iter_added_lines(spec))
if warns:
print("[WARN] 新增行里出现已停发族的编号 %d 处(不拦提交,但请改成语义短语):" % len(warns))
for rel, lineno, tok in warns[:10]:
print(" %s:%d %s" % (rel, lineno, tok))
if len(warns) > 10:
print(" … 其余 %d 处略" % (len(warns) - 10))
if fails:
print("[FAIL] 新增行里有 %d 处**未登记形态**的字母+数字标识:" % len(fails))
for rel, lineno, tok, text in fails[:20]:
print(" %s:%d %s" % (rel, lineno, tok))
if args.verbose:
print(" %s" % text)
print(" → 语义标识是默认形态:改写成短语(docs/编号规范.md §1);")
print(" 确属设备名/协议名/位号,就改形态(写语义短语)——本门禁已于 2026-10-29 退役,")
print(" 不再是提交钩子;要再拿它当尺子就手工跑 scripts/archive/id_gate/check_ids_new.py。")
return 1
print("[OK] 新增行无未登记编号%s"
% ("(%d 处停发族提示)" % len(warns) if warns else ""))
return 0
if __name__ == "__main__":
sys.exit(main())