#!/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"(? 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())