#!/usr/bin/env python3 # -*- coding: utf-8 -*- """check_ids_new.py — 编号门禁·第二步:只查**新增行**的新编号(零语料、无语料维护) 背景(docs/编号规范.md §8.5):语义标识已是默认形态,1~6 项全库语料审计的成本高过收益。 本脚本替代它的**唯一实际用途**——拦住"新写一个自造编号":只看 git diff 的新增行。 判据(三条,都不依赖任何族清单 / 游标 / 属主登记处): 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/check_ids_new.py # 查暂存区(pre-commit 用这个) python scripts/check_ids_new.py --worktree # 查工作区未暂存的改动(自查) python scripts/check_ids_new.py --range A..B # 查一段提交的新增行 python scripts/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 REPO_ROOT = Path(__file__).resolve().parent.parent # --- 破折形态:首段白名单(活族 + 标准编号;新增族要先登记到这里,这就是"允许用号的唯一口子") 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", }) # --- 前缀后面接 -数字 其实是算术(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_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(" 确属设备名/协议名/位号,就把前缀登记进 scripts/check_ids_new.py 的白名单。") return 1 print("[OK] 新增行无未登记编号%s" % ("(%d 处停发族提示)" % len(warns) if warns else "")) return 0 if __name__ == "__main__": sys.exit(main())