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:
@@ -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())
|
||||
Reference in New Issue
Block a user