feat(门禁): 编号门禁 id-check 落地(36→37 钩子)+ 批 1 编号单词化改名
新增门禁本体 - scripts/check_ids.py:实现 docs/编号规范.md §8 八项校验;默认 warn-only, 唯一硬拦项是「可判定比例跌破基线」(防回退的本体) - scripts/id_registry.json:66 个前缀族,逐项来自 §4 总表 + 全库实测游标, 偏差一律写进 cursor_note,不凭记忆编造 - scripts/id_aliases.json:旧号 → 新号 / 作废标记(照搬 section_aliases.json 范式) - 顺带收编 check_test_traceability.py:正则补齐大写后缀与无模块码形态, 比对范围扩到 test/host 与 7 篇「单元测试说明.md」,另加 --board-only 回退旧口径 批 1 改名(用户授权解禁「不改写存量号」) - 代码规范条款 S1~S19 → CODE-RULE-nn:78 处 / 2 文件 - UI 像素回归状态 S0~S7 → MENU-STATE-nn:61 处 / 3 文件(冻结在 07) - 两族属主登记处 = 脚本自身的规则表 / STATES 表,文档只作引用方 - 新增 scripts/rename_ids.py(§7.3 规划脚本):映射表驱动、显式 scope、默认干跑、 自动写 scoped 别名,跳过 [示例]/[计划] 行与变更记录表,按原字节读写保 BOM 行尾 - 已应用映射表存档 scripts/rename_maps/batch1.json 作审计轨迹 有牙实测(证明门禁不是空转) - 注入 Z-99 / 裸形 / 悬空 / 越游标 / 未紧邻 / 跨文档重复定义 六例, 分别被校验 1/2/3/5/6/4 点名,--strict 退出码 1;临时档删除后工作树干净 验证与同步 - 可判定比例基线 82.3% → 82.7%(已 --update-baseline 入档);错误级 1312 → 1291 - ci_local.py 全绿(L0 + L0.5 共 1104 断言);ui-golden Passed; check_doc_links.py --all 无法判定 0 - 未改任何 C/H 源文件,故本次不需 @build - 文档:AGENTS.md 与 pre-commit使用说明.md 计数 36→37 并新增 §2.25 详解; 编号规范 §4.1 补两行、§8.1 落地记录、§8.2 规范与仓库偏差、§8.3 迁移批次记录
This commit is contained in:
@@ -13,6 +13,10 @@
|
||||
| `check_mojibake.py` | `Forbid U+FFFD mojibake` | 扫描 `U+FFFD` 乱码(`EF BF BD` 字节) |
|
||||
| `check_doc_links.py` | `doc-links` | **文档链接存在性** + 命名规范(D-3/D-7)+ **`§N.M` 章节引用**(口径见 `docs/文档规范.md` §3.5)。钩子已带 `--sections-strict`(自引用失效也拦);`--section-todo` 列出机器判不了目标的 `§`(带改写建议)。每次跑打印「可判定比例」行,防覆盖率回落 |
|
||||
| `section_aliases.json` | (被 `check_doc_links.py` 读) | 文档**昵称别名表**:「内存方案」→ `docs/内存管理/RAM使用分析与优化方案.md`。只登记无歧义昵称;登记一次即把该昵称的引用纳入硬门禁 |
|
||||
| `check_ids.py` | `id-check` | **编号门禁**:`docs/编号规范.md` §8 的八项校验(未登记前缀 / 形态 / 悬空 / 重复定义 / 越游标 / 未紧邻文档名 / 孤儿 / 可判定比例)。**warn-only**,`--strict` 才拦提交;比例跌破基线任何模式都失败。详表 → `docs/代码规范/pre-commit使用说明.md` §2.25 |
|
||||
| `id_registry.json` | (被 `check_ids.py` 读) | 前缀族**注册表**:属主登记处 / 形态正则 / 冻结 / 游标,逐项来自 `docs/编号规范.md` §4;`decidable_ratio_baseline` = 校验 8 的基线(只许涨)。与 `section_aliases.json` 同一套"别名表 + 可判定比例"范式 |
|
||||
| `id_aliases.json` | (被 `check_ids.py` 读) | 编号**别名/作废表**:旧字面量 → 现行字面量或 `status: void`(如 `N-32` 销项后仍被引用、`Trap 19` 从未存在、模块码 `PHY→NET`) |
|
||||
| `check_test_traceability.py` | (被 `id-check` 顺带调用) | **TC 用例双向追溯**:文档 ↔ 代码两侧的 TC 编号集合比对;2026-10-20 起范围扩到 `test/host/` 与 7 篇「单元测试说明.md」(`--board-only` 回退旧板级口径)。此前该脚本**未挂任何钩子** |
|
||||
| `gen_task_table.py` | `task-table` | FreeRTOS 任务表以 `Src/freertos.c` 为**唯一事实源**生成/校验(D-2/D-7) |
|
||||
| `check_mem_numbers.py` | `mem-numbers` | 内存/Flash 数字以 `.map` 为唯一事实源校验(D-7 体积档) |
|
||||
| `check_test_suite.py` | `test-suite-switch` | 测试套件开关=产品构建(防测试固件静默提交) |
|
||||
@@ -28,6 +32,7 @@
|
||||
|
||||
| 脚本 | 作用 |
|
||||
|---|---|
|
||||
| `rename_ids.py` | **编号(字面量)批量改名**:映射表 JSON 驱动,`files` 显式 scope(同一裸形字面量可能属多族,实测 `S3` 三义 → 全局替换必造新撞号);默认干跑,`--apply` 落盘并自动写 `id_aliases.json` 的 scoped 旧号→新号条目;`[示例]`/`[计划]` 行与变更记录表不改写;按原字节读写(保 BOM 与行尾)。用法与批次记录 → `docs/编号规范.md` §8.3 |
|
||||
| `rename_ref.py` | **改名/挪目录 + 全库引用自动改写**:默认 dry-run 预览,`--apply` 才落盘(`git mv` → 改写引用 → 自动跑 `check_doc_links --all` 复核)。覆盖反引号引用、Markdown 链接、图片 `../` 层级换算、`#include`、`uvprojx`/`CMakeLists.txt` 源列表。固定动作与边界 → `docs/文档规范.md` §3.6 |
|
||||
|
||||
> `check_doc_links.py` 除链接存在性外,还校验 **`§N.M` 章节引用**是否指向目标文档真实存在的编号标题(口径见 `docs/文档规范.md` §3.5)。
|
||||
|
||||
@@ -5,24 +5,24 @@ C 语言代码规范检查(《嵌入式C语言代码规范_V1.0》可机械化
|
||||
由 pre-commit 调用:python scripts/check_coding_standard.py <file...>,发现违规退出码 1。
|
||||
|
||||
检查项(括号内为规范条款号):
|
||||
S1 (2.3.1) .h 文件必须有 XXX_H 格式的头文件保护宏(禁止 __ 双下划线前缀)
|
||||
S2 (5.2) 文件开头必须有文件头注释(含 模块名称 / 作者)
|
||||
S3 (3.4.1) #define 宏名必须全大写 + 下划线 + 数字
|
||||
S4 (3.5.1) typedef 类型名必须以 _t 结尾
|
||||
S5 (6.3.2) switch 必须包含 default 分支
|
||||
S6 (7.5) 禁用不安全字符串/输入函数 strcpy/strcat/sprintf/gets/scanf/strtok
|
||||
S7 (6.3.3) 禁止使用 goto
|
||||
S8 (4.1.1) 禁止 Tab 缩进(使用 4 个空格)
|
||||
S9 (5.3) 函数定义必须有函数注释(含「函数功能」)
|
||||
S10 (3.3.1) 函数名必须全小写(HAL_ 厂商 API 回调名豁免)
|
||||
S11 (2.3.5) 禁止相对/绝对路径 include
|
||||
S12 (7.6) 禁止可变参数函数(...)
|
||||
S13 (7.7) 禁止递归函数
|
||||
S15 (6.1.4) 文件作用域指针变量必须初始化为 NULL
|
||||
S16 (3.2.2) 文件作用域非 static 全局变量必须 g_ 前缀
|
||||
S17 (2.2) 非 static 函数必须在配对 .h 中声明(HAL_ 豁免;无配对头则跳过)
|
||||
S18 (6.3.1) 禁止 Yoda 条件(字面量/常量必须在运算符右侧)
|
||||
S19 (4.4.2) 函数定义左大括号必须与右括号同行
|
||||
CODE-RULE-01 (2.3.1) .h 文件必须有 XXX_H 格式的头文件保护宏(禁止 __ 双下划线前缀)
|
||||
CODE-RULE-02 (5.2) 文件开头必须有文件头注释(含 模块名称 / 作者)
|
||||
CODE-RULE-03 (3.4.1) #define 宏名必须全大写 + 下划线 + 数字
|
||||
CODE-RULE-04 (3.5.1) typedef 类型名必须以 _t 结尾
|
||||
CODE-RULE-05 (6.3.2) switch 必须包含 default 分支
|
||||
CODE-RULE-06 (7.5) 禁用不安全字符串/输入函数 strcpy/strcat/sprintf/gets/scanf/strtok
|
||||
CODE-RULE-07 (6.3.3) 禁止使用 goto
|
||||
CODE-RULE-08 (4.1.1) 禁止 Tab 缩进(使用 4 个空格)
|
||||
CODE-RULE-09 (5.3) 函数定义必须有函数注释(含「函数功能」)
|
||||
CODE-RULE-10 (3.3.1) 函数名必须全小写(HAL_ 厂商 API 回调名豁免)
|
||||
CODE-RULE-11 (2.3.5) 禁止相对/绝对路径 include
|
||||
CODE-RULE-12 (7.6) 禁止可变参数函数(...)
|
||||
CODE-RULE-13 (7.7) 禁止递归函数
|
||||
CODE-RULE-15 (6.1.4) 文件作用域指针变量必须初始化为 NULL
|
||||
CODE-RULE-16 (3.2.2) 文件作用域非 static 全局变量必须 g_ 前缀
|
||||
CODE-RULE-17 (2.2) 非 static 函数必须在配对 .h 中声明(HAL_ 豁免;无配对头则跳过)
|
||||
CODE-RULE-18 (6.3.1) 禁止 Yoda 条件(字面量/常量必须在运算符右侧)
|
||||
CODE-RULE-19 (4.4.2) 函数定义左大括号必须与右括号同行
|
||||
|
||||
语义类规则(函数长度、单一职责、错误处理等)无法机械可靠判定,不在此脚本范围。
|
||||
"""
|
||||
@@ -114,36 +114,36 @@ def check_file(path):
|
||||
lines = text.splitlines()
|
||||
v = [] # (行号或'-', 规则, 说明)
|
||||
|
||||
# S1 (2.3.1) .h 头文件保护宏
|
||||
# CODE-RULE-01 (2.3.1) .h 头文件保护宏
|
||||
if path.endswith('.h') and \
|
||||
not re.search(r'#\s*ifndef\s+(?<!_)[A-Z][A-Z0-9_]*_H\b', text):
|
||||
v.append(('-', 'S1 2.3.1', '缺少 XXX_H 格式的头文件保护宏(禁止 __ 前缀)'))
|
||||
v.append(('-', 'CODE-RULE-01 2.3.1', '缺少 XXX_H 格式的头文件保护宏(禁止 __ 前缀)'))
|
||||
|
||||
# S2 (5.2) 文件头注释
|
||||
# CODE-RULE-02 (5.2) 文件头注释
|
||||
head = '\n'.join(lines[:60])
|
||||
if '模块名称' not in head and '作者' not in head:
|
||||
v.append(('-', 'S2 5.2', '缺少文件头注释(模块名称 / 作者)'))
|
||||
v.append(('-', 'CODE-RULE-02 5.2', '缺少文件头注释(模块名称 / 作者)'))
|
||||
|
||||
# S3 (3.4.1) 宏名全大写
|
||||
# CODE-RULE-03 (3.4.1) 宏名全大写
|
||||
for m in re.finditer(r'^\s*#\s*define\s+([A-Za-z_][A-Za-z0-9_]*)', text, re.M):
|
||||
if re.search(r'[a-z]', m.group(1)):
|
||||
ln = text[:m.start()].count('\n') + 1
|
||||
v.append((ln, 'S3 3.4.1', '宏名须全大写+下划线: %s' % m.group(1)))
|
||||
v.append((ln, 'CODE-RULE-03 3.4.1', '宏名须全大写+下划线: %s' % m.group(1)))
|
||||
|
||||
# S4 (3.5.1) typedef 类型名 _t 后缀
|
||||
# CODE-RULE-04 (3.5.1) typedef 类型名 _t 后缀
|
||||
for m in re.finditer(r'typedef\s+(?:struct|enum|union)\s*\{.*?\}\s'
|
||||
r'([A-Za-z_][A-Za-z0-9_]*)\s*;', text, re.S):
|
||||
if not m.group(1).endswith('_t'):
|
||||
ln = text[:m.start()].count('\n') + 1
|
||||
v.append((ln, 'S4 3.5.1', 'typedef 类型名须以 _t 结尾: %s' % m.group(1)))
|
||||
v.append((ln, 'CODE-RULE-04 3.5.1', 'typedef 类型名须以 _t 结尾: %s' % m.group(1)))
|
||||
for m in re.finditer(r'typedef\s+(?:unsigned\s+|signed\s+)?'
|
||||
r'(?:char|short|int|long|float|double)\s+\*?\s*'
|
||||
r'([A-Za-z_][A-Za-z0-9_]*)\s*;', text):
|
||||
if not m.group(1).endswith('_t'):
|
||||
ln = text[:m.start()].count('\n') + 1
|
||||
v.append((ln, 'S4 3.5.1', 'typedef 类型名须以 _t 结尾: %s' % m.group(1)))
|
||||
v.append((ln, 'CODE-RULE-04 3.5.1', 'typedef 类型名须以 _t 结尾: %s' % m.group(1)))
|
||||
|
||||
# S5 (6.3.2) switch 必须有 default
|
||||
# CODE-RULE-05 (6.3.2) switch 必须有 default
|
||||
for m in re.finditer(r'\bswitch\s*\(', text):
|
||||
i = text.find('{', m.start())
|
||||
if i == -1:
|
||||
@@ -159,45 +159,45 @@ def check_file(path):
|
||||
j += 1
|
||||
if not re.search(r'\bdefault\s*:', text[i:j + 1]):
|
||||
ln = text[:m.start()].count('\n') + 1
|
||||
v.append((ln, 'S5 6.3.2', 'switch 缺少 default 分支'))
|
||||
v.append((ln, 'CODE-RULE-05 6.3.2', 'switch 缺少 default 分支'))
|
||||
|
||||
# S6 (7.5) 不安全字符串/输入函数
|
||||
# CODE-RULE-06 (7.5) 不安全字符串/输入函数
|
||||
for m in re.finditer(r'\b(strcpy|strcat|sprintf|gets|scanf|strtok)\s*\(', text):
|
||||
ln = text[:m.start()].count('\n') + 1
|
||||
v.append((ln, 'S6 7.5', '不安全字符串/输入函数: %s (改用安全版本)'
|
||||
v.append((ln, 'CODE-RULE-06 7.5', '不安全字符串/输入函数: %s (改用安全版本)'
|
||||
% m.group(1)))
|
||||
|
||||
# S7 (6.3.3) 禁止 goto
|
||||
# CODE-RULE-07 (6.3.3) 禁止 goto
|
||||
m = re.search(r'\bgoto\s+[A-Za-z_]\w*', text)
|
||||
if m:
|
||||
ln = text[:m.start()].count('\n') + 1
|
||||
v.append((ln, 'S7 6.3.3', '禁止使用 goto'))
|
||||
v.append((ln, 'CODE-RULE-07 6.3.3', '禁止使用 goto'))
|
||||
|
||||
# S8 (4.1.1) 禁止 Tab
|
||||
# CODE-RULE-08 (4.1.1) 禁止 Tab
|
||||
if '\t' in text:
|
||||
for idx, line in enumerate(lines, 1):
|
||||
if '\t' in line:
|
||||
v.append((idx, 'S8 4.1.1', '禁止 Tab 缩进(使用 4 个空格)'))
|
||||
v.append((idx, 'CODE-RULE-08 4.1.1', '禁止 Tab 缩进(使用 4 个空格)'))
|
||||
break
|
||||
|
||||
# S9 (5.3) 函数定义必须有函数注释
|
||||
# CODE-RULE-09 (5.3) 函数定义必须有函数注释
|
||||
for idx, line in enumerate(lines, 1):
|
||||
if (FUNC_DEF_RE.match(line) and '=' not in line and not CTRL_RE.match(line)):
|
||||
window = '\n'.join(lines[max(0, idx - 26):idx - 1])
|
||||
if '函数功能' not in window:
|
||||
v.append((idx, 'S9 5.3', '函数定义缺少函数注释(函数功能): %s'
|
||||
v.append((idx, 'CODE-RULE-09 5.3', '函数定义缺少函数注释(函数功能): %s'
|
||||
% line.strip()[:50]))
|
||||
|
||||
masked = _mask(text)
|
||||
mlines = masked.splitlines()
|
||||
defs = _fn_defs(masked)
|
||||
|
||||
# S10 (3.3.1) 函数名必须全小写(HAL_ 厂商 API 回调名豁免)
|
||||
# CODE-RULE-10 (3.3.1) 函数名必须全小写(HAL_ 厂商 API 回调名豁免)
|
||||
for name, ln, _body, _pre, _bl, _pl in defs:
|
||||
if re.search(r'[A-Z]', name) and not name.startswith('HAL_'):
|
||||
v.append((ln, 'S10 3.3.1', '函数名须全小写: %s' % name))
|
||||
v.append((ln, 'CODE-RULE-10 3.3.1', '函数名须全小写: %s' % name))
|
||||
|
||||
# S11 (2.3.5) 禁止相对/绝对路径 include(基于原始文本,字符串掩码会吃掉引号)
|
||||
# CODE-RULE-11 (2.3.5) 禁止相对/绝对路径 include(基于原始文本,字符串掩码会吃掉引号)
|
||||
# 规则本意 = 禁"用路径找文件":`#include "../../x.h"`、`#include "App/foo/x.h"`、
|
||||
# `#include <C:/x.h>`、`#include <..\..\x.h>`。
|
||||
# ⚠ 2026-10-11 细化:原实现把**任何含 `/` 的 include** 都判违规,于是
|
||||
@@ -211,19 +211,19 @@ def check_file(path):
|
||||
if form == '"' and '/' in inc:
|
||||
bad = True
|
||||
if bad:
|
||||
v.append((_line(text, m.start()), 'S11 2.3.5',
|
||||
v.append((_line(text, m.start()), 'CODE-RULE-11 2.3.5',
|
||||
'禁止相对/绝对路径 include: %s' % inc))
|
||||
|
||||
# S12 (7.6) 禁止可变参数函数
|
||||
# CODE-RULE-12 (7.6) 禁止可变参数函数
|
||||
for m in re.finditer(r'\(\s*[^();{}]*\.\.\.[^();{}]*\)\s*[;{]', masked):
|
||||
v.append((_line(masked, m.start()), 'S12 7.6', '禁止可变参数函数(...)'))
|
||||
v.append((_line(masked, m.start()), 'CODE-RULE-12 7.6', '禁止可变参数函数(...)'))
|
||||
|
||||
# S13 (7.7) 禁止递归函数
|
||||
# CODE-RULE-13 (7.7) 禁止递归函数
|
||||
for name, ln, body, _pre, _bl, _pl in defs:
|
||||
if re.search(r'\b%s\s*\(' % re.escape(name), body):
|
||||
v.append((ln, 'S13 7.7', '递归函数: %s' % name))
|
||||
v.append((ln, 'CODE-RULE-13 7.7', '递归函数: %s' % name))
|
||||
|
||||
# S15 (6.1.4) 文件作用域指针变量必须初始化为 NULL
|
||||
# CODE-RULE-15 (6.1.4) 文件作用域指针变量必须初始化为 NULL
|
||||
for idx, line in enumerate(mlines, 1):
|
||||
if not line or line[0] in ' \t':
|
||||
continue
|
||||
@@ -235,10 +235,10 @@ def check_file(path):
|
||||
continue
|
||||
if '*' not in line or '=' in line:
|
||||
continue
|
||||
v.append((idx, 'S15 6.1.4', '文件作用域指针变量须初始化为 NULL: %s'
|
||||
v.append((idx, 'CODE-RULE-15 6.1.4', '文件作用域指针变量须初始化为 NULL: %s'
|
||||
% line.strip()[:50]))
|
||||
|
||||
# S16 (3.2.2) 文件作用域非 static 全局变量必须 g_ 前缀
|
||||
# CODE-RULE-16 (3.2.2) 文件作用域非 static 全局变量必须 g_ 前缀
|
||||
for idx, line in enumerate(mlines, 1):
|
||||
if not line or line[0] in ' \t' or line[0] in '}]':
|
||||
continue
|
||||
@@ -253,10 +253,10 @@ def check_file(path):
|
||||
if not m2:
|
||||
continue
|
||||
if not m2.group(1).startswith('g_'):
|
||||
v.append((idx, 'S16 3.2.2', '非 static 全局变量须 g_ 前缀: %s'
|
||||
v.append((idx, 'CODE-RULE-16 3.2.2', '非 static 全局变量须 g_ 前缀: %s'
|
||||
% line.strip()[:50]))
|
||||
|
||||
# S17 (2.2) 非 static 函数必须在配对头文件声明(HAL_ 豁免;无配对头则跳过)
|
||||
# CODE-RULE-17 (2.2) 非 static 函数必须在配对头文件声明(HAL_ 豁免;无配对头则跳过)
|
||||
if path.endswith('.c'):
|
||||
hpath = path[:-2] + '.h'
|
||||
if os.path.exists(hpath):
|
||||
@@ -271,19 +271,19 @@ def check_file(path):
|
||||
continue
|
||||
if re.search(r'\b%s\s*\(' % re.escape(name), htext):
|
||||
continue
|
||||
v.append((ln, 'S17 2.2', '非 static 函数须在配对头文件声明: %s' % name))
|
||||
v.append((ln, 'CODE-RULE-17 2.2', '非 static 函数须在配对头文件声明: %s' % name))
|
||||
|
||||
# S18 (6.3.1) 禁止 Yoda 条件(常量在右)
|
||||
# CODE-RULE-18 (6.3.1) 禁止 Yoda 条件(常量在右)
|
||||
for m in re.finditer(r'\b(0[xX][0-9a-fA-F]+(?:[uUlL]+)?|\d+(?:[uUlL]+)?'
|
||||
r'|NULL|true|false)\s*(==|!=|<=|>=|<|>)\s+[A-Za-z_]',
|
||||
masked):
|
||||
v.append((_line(masked, m.start()), 'S18 6.3.1',
|
||||
v.append((_line(masked, m.start()), 'CODE-RULE-18 6.3.1',
|
||||
'Yoda 条件(常量应在右): %s' % ' '.join(m.group(0).split())))
|
||||
|
||||
# S19 (4.4.2) 函数定义左大括号必须与右括号同行
|
||||
# CODE-RULE-19 (4.4.2) 函数定义左大括号必须与右括号同行
|
||||
for name, ln, _body, _pre, bl, pl in defs:
|
||||
if bl != pl:
|
||||
v.append((ln, 'S19 4.4.2', '函数定义左大括号须与右括号同行: %s' % name))
|
||||
v.append((ln, 'CODE-RULE-19 4.4.2', '函数定义左大括号须与右括号同行: %s' % name))
|
||||
|
||||
return v
|
||||
|
||||
|
||||
@@ -0,0 +1,657 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
check_ids.py — 编号门禁(docs/编号规范.md §8 的 8 项校验)。
|
||||
|
||||
两份配置(数据源 = 规范 §4 编号空间总表,逐项对应,不凭印象):
|
||||
scripts/id_registry.json 前缀族 → {形态正则, 属主登记处, 是否冻结, 游标, 作用域, 级别}
|
||||
scripts/id_aliases.json 旧字面量 → 现行字面量(照搬 scripts/section_aliases.json 的成功范式)
|
||||
|
||||
8 项校验(级别按规范 §8 表):
|
||||
1 出现未登记前缀(自造编号) 错误
|
||||
2 形态不合规:裸形 S3 / 空格形 Trap 07 / 中文形 陷阱 1 /
|
||||
补零错 / 档位词带后缀 P1-8 / 大写后缀 / 缺模块码 错误
|
||||
3 引用查无定义且不在别名表(悬空引用) 错误
|
||||
4 同一字面量被两个属主重复定义(N-16 类事故) 错误
|
||||
5 冻结前缀出现游标之外的新号 错误
|
||||
6 文档内编号引用未紧邻文档名 错误
|
||||
7 定义了但全库无引用(孤儿条目) 告警
|
||||
8 打印「编号引用可判定比例」 每次运行,只许涨不许跌
|
||||
|
||||
「定义」的机械判据:**字面量出现在该族的属主登记处文档里**(不依赖表格行首这类易漂移的
|
||||
形态)。因此 §5.5 的「销项 = 删行 + 清全库引用」一旦只做一半,校验 3 立刻报——这正是
|
||||
§1.2 里 N-31/N-32 撞号与悬空的成因。
|
||||
|
||||
排除(规范 §8 + §4.4):Lib/、Middlewares/、HAL/CMSIS、docs/芯片手册/、docs/硬件平台/
|
||||
(元器件位号 U1/C12/R3/L5/D7 与编号字面同形,是 B1/C1/L1 撞形的根源,必须整体排除)、
|
||||
构建产物。裸形盲区显式登记在 id_registry.json 的 `unmanaged_bare`,不是「检查通过」。
|
||||
|
||||
上线节奏(§8):默认 **warn-only**(退出码 0);`--strict` 才让校验 1~6 拦提交。
|
||||
第 8 项的比例跌破基线**任何模式都失败**(它就是防回退的那根线)。
|
||||
|
||||
用法(仓库根):
|
||||
& '.venv\\Scripts\\python.exe' scripts/check_ids.py # warn-only 全库
|
||||
& '.venv\\Scripts\\python.exe' scripts/check_ids.py --strict # 错误拦提交
|
||||
& '.venv\\Scripts\\python.exe' scripts/check_ids.py --top 30 --verbose # 多打 + 修法
|
||||
& '.venv\\Scripts\\python.exe' scripts/check_ids.py --update-baseline # 抬升第 8 项基线
|
||||
& '.venv\\Scripts\\python.exe' scripts/check_ids.py --only 1,3,5 # 只跑部分校验
|
||||
退出码:0 = 通过 / 仅告警;1 = 错误(--strict)或比例跌破基线;2 = 环境错误。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import fnmatch
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
# 控制台编码兜底:pre-commit 钩子环境的代码页是 936/GBK,打印中文或箭头会抛
|
||||
# UnicodeEncodeError → 钩子带 traceback 以退出码 1 死亡(同 check_doc_links.py 的教训)。
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
REGISTRY_FILE = REPO_ROOT / "scripts" / "id_registry.json"
|
||||
ALIASES_FILE = REPO_ROOT / "scripts" / "id_aliases.json"
|
||||
TRACE_SCRIPT = REPO_ROOT / "scripts" / "check_test_traceability.py"
|
||||
|
||||
# 候选池:任何「1~4 位大写字母 + 连字符 + 数字」先进来,再判是否登记过。
|
||||
# 三段式(TC-NET-1501 / TRAP-NET-18)单独一条——中段可能含数字(GD5F),
|
||||
# 只用两段式正则会截出 "NET-1501" 这种假前缀。
|
||||
CAND_MOD = re.compile(r"(?<![\w./\\-])([A-Z][A-Z0-9]{0,3})-([A-Z][A-Z0-9]{0,4})-(\d{1,4}[A-Za-z]?)(?![\w-])")
|
||||
CAND_HYPH = re.compile(r"(?<![\w./\\-])([A-Z][A-Z0-9]{0,3})-(\d{1,4}[A-Za-z]?)(?![\w-])")
|
||||
CAND_BARE = re.compile(r"(?<![\w.\-/])([A-Z])(?:\.(\d))?(1[0-9]|[0-9])(?![\w.\-])")
|
||||
|
||||
# 紧邻窗口(与 check_doc_links.py 的 SEC_MAX_GAP / SEC_GAP_CHARS 同口径)
|
||||
ADJ_WINDOW = 60
|
||||
# 第 8 项的容差(百分点):比例的分母随"这一批改了几篇文档"漂移,0.1pp 以内不算回退
|
||||
RATIO_TOL = 0.1
|
||||
|
||||
|
||||
def die(msg):
|
||||
print("[ERR] " + msg, file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
def load_json(path, what):
|
||||
if not path.is_file():
|
||||
raise SystemExit(die("缺少 %s(%s)" % (what, path.relative_to(REPO_ROOT))))
|
||||
try:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError) as exc:
|
||||
raise SystemExit(die("无法解析 %s:%s" % (path.relative_to(REPO_ROOT), exc)))
|
||||
|
||||
|
||||
class Finding(object):
|
||||
__slots__ = ("code", "severity", "rel", "lineno", "literal", "msg", "hint")
|
||||
|
||||
def __init__(self, code, severity, rel, lineno, literal, msg, hint=""):
|
||||
self.code = code
|
||||
self.severity = severity
|
||||
self.rel = rel
|
||||
self.lineno = lineno
|
||||
self.literal = literal
|
||||
self.msg = msg
|
||||
self.hint = hint
|
||||
|
||||
|
||||
def alias_hit(ck, literal, rel):
|
||||
"""别名命中判定:带 `scope` 的条目只在所列路径内生效。
|
||||
|
||||
为什么要限定作用域——同一旧字面量可能同时是**两个族的旧号**:实测 `S3` 在
|
||||
scripts/check_coding_standard.py 与 docs/代码规范/ 下是「代码规范条款」旧号,
|
||||
在 simulator/tests 与 docs/UI设计/ 下是「UI 像素回归状态」旧号。认成全局别名就会把
|
||||
另一族的真悬空引用洗成「已解析」——那等于用工具复制 §1.2 记的 N-31/N-32 撞号事故。
|
||||
"""
|
||||
for ent in ck.alias_map.get(literal, []):
|
||||
scope = ent.get("scope")
|
||||
if not scope or any(fnmatch.fnmatch(rel, g) for g in scope):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Checker(object):
|
||||
def __init__(self, registry, aliases, verbose=False):
|
||||
self.reg = registry
|
||||
self.alias_map = {}
|
||||
for lit, ent in ((aliases or {}).get("aliases", {})).items():
|
||||
# 同一旧字面量可能有**多条 scoped 别名**(实测 `S3` 既是被改名的代码规范条款、
|
||||
# 又是被改名的 UI 像素状态)→ 统一成列表,命中判定时按 scope 选条
|
||||
self.alias_map[lit] = ent if isinstance(ent, list) else [ent]
|
||||
self.verbose = verbose
|
||||
self.excl = tuple(registry.get("excluded_paths", []))
|
||||
self.excl_suffix = tuple(registry.get("excluded_suffixes", []))
|
||||
self.owner_excl = list(registry.get("owner_docs_exclude", []))
|
||||
self.noise = [re.compile(p) for p in registry.get("noise_literals", [])]
|
||||
self.spec_docs = set(registry.get("spec_docs", []))
|
||||
self.bare_letters = set(registry.get("bare_check_letters", []))
|
||||
self.bare_forbidden = registry.get("bare_forbidden", {})
|
||||
self.bare_exempt_files = set(registry.get("bare_exempt_files", []))
|
||||
self.malformed = [(m["name"], re.compile(m["pattern"]), m["severity"], m["why"],
|
||||
bool(m.get("docs_only")))
|
||||
for m in registry.get("malformed", [])]
|
||||
self.families = []
|
||||
for fam in registry["families"]:
|
||||
f = dict(fam)
|
||||
f["_re"] = re.compile(fam["pattern"])
|
||||
f["_full"] = re.compile("^(?:%s)$" % fam["pattern"])
|
||||
f["_owners"] = self._expand_owners(fam)
|
||||
f["_max"] = self._cursor_max(fam)
|
||||
self.families.append(f)
|
||||
# 长形态优先匹配:TC-UPPER 要先于 TC,避免大写后缀被当成合规
|
||||
self.families.sort(key=lambda x: (-len(x["pattern"]), x["name"]))
|
||||
self.findings = []
|
||||
self.stats = Counter()
|
||||
self.form_fired = set()
|
||||
self.refs = {} # literal -> set(rel),属主文档之外的引用处
|
||||
self.owner_tokens = defaultdict(set) # family -> 属主文档里出现的字面量(=定义集)
|
||||
self.file_cache = {}
|
||||
self.owner_docs = set()
|
||||
|
||||
# ---------- 属主登记处展开 ----------
|
||||
def _expand_owners(self, fam):
|
||||
paths = list(fam.get("owner_docs", []))
|
||||
for g in fam.get("owner_docs_glob", []):
|
||||
for hit in REPO_ROOT.glob(g):
|
||||
if hit.is_file():
|
||||
paths.append(hit.relative_to(REPO_ROOT).as_posix())
|
||||
# 属主文档本身不得落在排除目录(位号/芯片手册/生成物不算属主)
|
||||
# 另:模板规范类文档里的 `DP-01` / `TC-XXX` 是**写法示例**,不是条目定义
|
||||
out = [p for p in paths if not p.startswith(self.excl)]
|
||||
for e in self.owner_excl:
|
||||
out = [p for p in out if p != e]
|
||||
return sorted(set(out))
|
||||
|
||||
def _cursor_max(self, fam):
|
||||
cur = fam.get("cursor")
|
||||
if not cur:
|
||||
return None
|
||||
m = re.search(r"([0-9]+)\s*$", str(cur))
|
||||
return int(m.group(1)) if m else None
|
||||
|
||||
# ---------- 文件枚举(以 git 索引为准,生成物天然挡在外面)----------
|
||||
def iter_files(self):
|
||||
try:
|
||||
out = subprocess.run(["git", "ls-files", "-z"], cwd=str(REPO_ROOT),
|
||||
capture_output=True, check=True).stdout
|
||||
files = out.decode("utf-8", "replace").split("\0")
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
files = [p.relative_to(REPO_ROOT).as_posix() for p in REPO_ROOT.rglob("*")
|
||||
if p.is_file()]
|
||||
for f in files:
|
||||
if not f or f.startswith(self.excl) or f.endswith(self.excl_suffix):
|
||||
continue
|
||||
if "/__pycache__/" in ("/" + f) or f.endswith(".pyc"):
|
||||
continue
|
||||
if (REPO_ROOT / f).is_file():
|
||||
yield f
|
||||
|
||||
def read(self, rel):
|
||||
if rel not in self.file_cache:
|
||||
try:
|
||||
self.file_cache[rel] = (REPO_ROOT / rel).read_text(
|
||||
encoding="utf-8", errors="replace")
|
||||
except OSError as exc:
|
||||
print("[WARN] 读取失败 %s:%s" % (rel, exc), file=sys.stderr)
|
||||
self.file_cache[rel] = ""
|
||||
return self.file_cache[rel]
|
||||
|
||||
# ---------- 主流程(两遍:先建定义集,再判引用)----------
|
||||
def run(self):
|
||||
rels = list(self.iter_files())
|
||||
if not rels:
|
||||
return die("一个文件都没扫到(git ls-files 返回空?)")
|
||||
self.stats["files"] = len(rels)
|
||||
for rel in rels:
|
||||
self.read(rel)
|
||||
for fam in self.families:
|
||||
for owner in fam["_owners"]:
|
||||
self.owner_docs.add(owner)
|
||||
self.owner_tokens[fam["name"]].update(
|
||||
m.group(0) for m in fam["_re"].finditer(self.read(owner)))
|
||||
self.defined_all = set()
|
||||
for s in self.owner_tokens.values():
|
||||
self.defined_all |= s
|
||||
for rel in rels:
|
||||
for i, line in enumerate(self.read(rel).splitlines(), 1):
|
||||
self.scan_line(rel, i, line)
|
||||
self.check_collisions()
|
||||
self.check_orphans()
|
||||
return 0
|
||||
|
||||
def _is_noise(self, literal):
|
||||
return any(rx.match(literal) for rx in self.noise)
|
||||
|
||||
def scan_line(self, rel, lineno, line):
|
||||
if "[示例]" in line or "[计划]" in line:
|
||||
# 报错样例 / 尚未创建的条目:与 check_doc_links.py 共用同一套豁免约定(§5.2)
|
||||
self.stats["skip_example"] += len(CAND_HYPH.findall(line))
|
||||
self._scan_malformed(rel, lineno, line, skip=True)
|
||||
return
|
||||
spans = []
|
||||
for m in CAND_MOD.finditer(line):
|
||||
spans.append((m.start(), m.end(), m.group(0), True))
|
||||
covered = [(a, b) for a, b, _, _ in spans]
|
||||
for m in CAND_HYPH.finditer(line):
|
||||
if any(a <= m.start() and m.end() <= b for a, b in covered):
|
||||
continue
|
||||
spans.append((m.start(), m.end(), m.group(0), True))
|
||||
for m in CAND_BARE.finditer(line):
|
||||
if m.group(1) not in self.bare_letters:
|
||||
continue
|
||||
if any(a <= m.start() and m.end() <= b for a, b in covered):
|
||||
continue
|
||||
lit = m.group(1) + (("." + m.group(2)) if m.group(2) else "") + m.group(3)
|
||||
if "." in lit:
|
||||
continue
|
||||
spans.append((m.start(), m.end(), lit, False))
|
||||
spans.sort()
|
||||
|
||||
seen_here = set()
|
||||
for start, _end, literal, hyphenated in spans:
|
||||
if (literal, start) in seen_here:
|
||||
continue
|
||||
seen_here.add((literal, start))
|
||||
self.stats["refs_total"] += 1
|
||||
fams = [f for f in self.families if f["_full"].match(literal)]
|
||||
if not fams:
|
||||
if self._is_noise(literal):
|
||||
self.stats["noise_allowed"] += 1
|
||||
continue
|
||||
if not hyphenated:
|
||||
# 裸形盲区(§1.2 同字母多义 / §4.4 位号同形):显式不判,见 unmanaged_bare
|
||||
self.stats["skip_bare_unmanaged"] += 1
|
||||
continue
|
||||
if re.match(r"^P[0-3]-\d", literal):
|
||||
self.add(2, "error", rel, lineno, literal,
|
||||
"档位词带后缀:P0/P1/P2/P3 是严重度与优先级档位词,"
|
||||
"永不带 -<数字>(§4.4)",
|
||||
"只写 `P1`,严重度进表格列(§2 P-2)")
|
||||
self.stats["undecidable"] += 1
|
||||
else:
|
||||
self.add(1, "error", rel, lineno, literal,
|
||||
"未登记前缀:%s(§4.1 / §4.2 总表里没有这一族)" % literal,
|
||||
"归到既有类别;确属新类别先改 docs/编号规范.md §4,再进 registry")
|
||||
self.stats["undecidable"] += 1
|
||||
continue
|
||||
|
||||
fam = self._choose_family(literal, fams, rel, line, start)
|
||||
if fam["_owners"]:
|
||||
self.stats["decidable"] += 1
|
||||
else:
|
||||
self.stats["undecidable"] += 1
|
||||
self._check_form(rel, lineno, literal, fam)
|
||||
self._check_frozen(rel, lineno, literal, fam)
|
||||
self._check_dangling(rel, lineno, literal, fam)
|
||||
self._check_adjacent(rel, lineno, literal, fam, line, start)
|
||||
if rel not in fam["_owners"]:
|
||||
self.refs.setdefault(literal, set()).add(rel)
|
||||
|
||||
self._scan_malformed(rel, lineno, line)
|
||||
|
||||
def _choose_family(self, literal, fams, rel, line, start):
|
||||
"""同一字面量可能落进多族(登记簿 N-16 vs 报告 N-16):属主文档 > 紧邻属主 > 全局族。"""
|
||||
if len(fams) == 1:
|
||||
return fams[0]
|
||||
for f in fams:
|
||||
if rel in f["_owners"]:
|
||||
return f
|
||||
for f in fams:
|
||||
if f.get("adjacent_required") and self._has_adjacent_cue(f, line, start):
|
||||
return f
|
||||
for f in fams:
|
||||
if f.get("check_dangling") and f["_owners"]:
|
||||
return f
|
||||
for f in fams:
|
||||
if f["scope"] == "global":
|
||||
return f
|
||||
return fams[0]
|
||||
|
||||
def _cues(self, fam):
|
||||
cues = list(fam.get("adjacent_cues", []))
|
||||
for o in fam.get("owner_docs", []):
|
||||
stem = o.rsplit("/", 1)[-1]
|
||||
cues += [stem, stem.rsplit(".", 1)[0]]
|
||||
for o in fam["_owners"][:6]:
|
||||
stem = o.rsplit("/", 1)[-1]
|
||||
cues += [stem, stem.rsplit(".", 1)[0]]
|
||||
out, seen = [], set()
|
||||
for c in cues:
|
||||
if c and len(c) >= 2 and c not in seen:
|
||||
seen.add(c)
|
||||
out.append(c)
|
||||
return out
|
||||
|
||||
def _has_adjacent_cue(self, fam, line, start):
|
||||
window = line[max(0, start - ADJ_WINDOW):start]
|
||||
for c in self._cues(fam):
|
||||
if c in window:
|
||||
return True
|
||||
return False
|
||||
|
||||
# ---------- 单项校验 ----------
|
||||
def add(self, code, severity, rel, lineno, literal, msg, hint=""):
|
||||
self.findings.append(Finding(code, severity, rel, lineno, literal, msg, hint))
|
||||
self.stats["hit_%d" % code] += 1
|
||||
if code == 2:
|
||||
# 同一处的形态违规不再重复报悬空(同一个毛病报两次,会把 backlog 冲淡)
|
||||
self.form_fired.add((rel, lineno, literal))
|
||||
|
||||
def _check_form(self, rel, lineno, literal, fam):
|
||||
if rel in self.spec_docs:
|
||||
# 规范正文**必须**引用非法形态才能说明什么叫非法(`Trap 07`、`P1-8`…),
|
||||
# 在规范里判"形态不合规"等于让规范判死自己;其余文档照判。
|
||||
return
|
||||
# 裸形判定按**字面量本身**有无连字符——不能看族正则:`[0-4]` 这类字符类里
|
||||
# 就带连字符,按族判断会把 S-REVIEW 误认成"连字符族"(实测踩到过)。
|
||||
if "-" not in literal:
|
||||
self._check_bare_form(rel, lineno, literal, fam)
|
||||
digits = re.findall(r"([0-9]+)$", literal)
|
||||
width = fam.get("digit_width")
|
||||
if digits and width:
|
||||
lo, hi = width
|
||||
if not (lo <= len(digits[0]) <= hi):
|
||||
self.add(2, "error", rel, lineno, literal,
|
||||
"形态不合规:%s 的序号应为 %d~%d 位(§4.1 左补零,P-4)"
|
||||
% (literal, lo, hi), "补零后引用,属主登记处一并改")
|
||||
if fam.get("suffix") == "lower" and re.search(r"[A-Z]$", literal):
|
||||
self.add(2, "error", rel, lineno, literal,
|
||||
"形态不合规:%s 后缀必须是小写字母(§4.1)" % literal,
|
||||
"改 %s%s" % (literal[:-1], literal[-1].lower()))
|
||||
if fam.get("suffix") == "none" and re.search(r"[A-Za-z]$", literal):
|
||||
pass
|
||||
if fam.get("malformed"):
|
||||
self.add(2, "error", rel, lineno, literal,
|
||||
"形态不合规:%s —— %s(§4.1 / §6)" % (literal, fam["malformed"]),
|
||||
"按规范形态改写")
|
||||
|
||||
def _check_bare_form(self, rel, lineno, literal, fam):
|
||||
"""裸形:S3 之于 S-3(§2 P-4 禁止裸形)。两个条件同时成立才报,避免把
|
||||
「历史裸形族在属主文档里被引用」也算成违规:
|
||||
① 该裸形字面量**全库无任何属主登记处定义**(在案的历史裸形引用不算违规,
|
||||
它们由校验 3 以告警形式处理);
|
||||
② 它的连字符孪生形(S-3)确有在册定义——说明作者是在写一个登记簿条目,
|
||||
只是漏了连字符。也不在裸形豁免文件里。"""
|
||||
m = re.match(r"^([A-Z])([0-9]{1,2})$", literal)
|
||||
if not m:
|
||||
return
|
||||
letter = m.group(1)
|
||||
if letter not in self.bare_forbidden or rel in self.bare_exempt_files:
|
||||
return
|
||||
hyph = "%s-%s" % (letter, m.group(2))
|
||||
hyph_defined = any(hyph in self.owner_tokens[f["name"]] for f in self.families)
|
||||
if hyph_defined and literal not in self.defined_all:
|
||||
self.add(2, "error", rel, lineno, literal,
|
||||
"形态不合规:裸形 %s 是 %s 的非法写法(§2 P-4;%s)"
|
||||
% (literal, hyph, self.bare_forbidden[letter]),
|
||||
"写全 %s,或改语义标识(§3.3 / P-8)" % hyph)
|
||||
|
||||
def _check_frozen(self, rel, lineno, literal, fam):
|
||||
if not fam.get("frozen") or fam["_max"] is None:
|
||||
return
|
||||
if fam.get("cursor_owner_only") and rel not in fam["_owners"]:
|
||||
# 裸形族(B5/C6/D7/M1/U3/R3/V5…)与位号、档位词、其它文档的同名清单大面积同形
|
||||
# (§1.2 五义/四义)→ 越游标只在属主登记处文档内判,文档外不判(见 unmanaged_bare)
|
||||
return
|
||||
nums = re.findall(r"([0-9]+)", literal.rsplit("-", 1)[-1])
|
||||
if not nums:
|
||||
return
|
||||
if int(nums[-1]) > fam["_max"]:
|
||||
self.add(5, "error", rel, lineno, literal,
|
||||
"冻结前缀越游标:%s > %s(%s;§4.3 / §8 校验 5)"
|
||||
% (literal, fam["cursor"], fam["title"]),
|
||||
"该族已封闭,新条目改语义标识(§3.3);确需新号先改 docs/编号规范.md §4")
|
||||
|
||||
def _check_dangling(self, rel, lineno, literal, fam):
|
||||
if not fam.get("check_dangling") or not fam["_owners"] or rel in fam["_owners"]:
|
||||
return
|
||||
if rel in self.spec_docs:
|
||||
# 规范正文里的 `N-99`→`N-100`、`Trap 07` 这类是**写法示例**,不是引用
|
||||
self.stats["skip_spec_examples"] += 1
|
||||
return
|
||||
if (rel, lineno, literal) in self.form_fired:
|
||||
return
|
||||
if literal in self.defined_all:
|
||||
return
|
||||
if alias_hit(self, literal, rel) or literal in set(fam.get("allow_missing_defs", [])):
|
||||
self.stats["alias_hits"] += 1
|
||||
return
|
||||
sev = fam.get("dangling_outside_owner", "error") if rel not in fam["_owners"] else "error"
|
||||
self.add(3, sev, rel, lineno, literal,
|
||||
"悬空引用:%s 在属主登记处(%s)查无定义,也不在别名表里(§8 校验 3)"
|
||||
% (literal, " / ".join(fam["_owners"][:2])),
|
||||
"补定义行,或删引用(§5.5:销项 = 删行 + 清全库引用)")
|
||||
|
||||
def _check_adjacent(self, rel, lineno, literal, fam, line, start):
|
||||
if not fam.get("adjacent_required") or rel in fam["_owners"] or rel in self.spec_docs:
|
||||
return
|
||||
if "-" not in literal:
|
||||
# 只约束连字符形态的文档内编号:裸形(A2/B5/U3/D7)与位号、档位词大面积同形
|
||||
# (§1.2 五义/四义清单),全库要求紧邻文档名属 §9 阶段 1-③ 的改写批次。
|
||||
return
|
||||
if self._has_adjacent_cue(fam, line, start):
|
||||
return
|
||||
owner = fam["owner_docs"][0] if fam.get("owner_docs") else (
|
||||
fam["_owners"][0] if fam["_owners"] else "docs/<属主文档>.md")
|
||||
self.add(6, "error", rel, lineno, literal,
|
||||
"文档内编号未紧邻文档名:%s 属「%s」,跨文档引用必须写全属主(§5.2 / §8 校验 6)"
|
||||
% (literal, fam["title"]),
|
||||
"写成 `%s` %s(或 `%s NN`)" % (owner, literal, literal))
|
||||
|
||||
def _scan_malformed(self, rel, lineno, line, skip=False):
|
||||
is_md = rel.endswith(".md")
|
||||
if rel in self.spec_docs and not skip:
|
||||
# 同上:规范正文就是靠引用 `Trap 07` / `陷阱 1` / `gen 181` 这些非法形态
|
||||
# 才讲得清"什么算非法",在它自己头上判形态违规没有意义
|
||||
return
|
||||
for name, rx, sev, why, docs_only in self.malformed:
|
||||
if (docs_only and not is_md) or (skip and name != "空格形 Trap"):
|
||||
continue
|
||||
for m in rx.finditer(line):
|
||||
self.add(2, sev, rel, lineno, m.group(0).strip(),
|
||||
"形态不合规:%s —— %s" % (name, why), "按 §7.1 迁移表改写")
|
||||
self.stats["malformed_%s" % name] += 1
|
||||
|
||||
# ---------- 校验 4 / 7 ----------
|
||||
# 定义位:字面量出现在**表格首格 / 标题行 / 列表项行首**——正文里"提到过"不算定义,
|
||||
# 否则任何交叉引用都会被误判成重复定义(§8 校验 4 要抓的是"两处各建一条目")。
|
||||
DEF_ANCHOR = re.compile(r"^\s*(?:\|\s*\*{0,2}|#{1,6}\s+|[-*]\s+\*{0,2})"
|
||||
r"([A-Z][A-Z0-9]*(?:-[A-Za-z0-9]+)+)")
|
||||
|
||||
def check_collisions(self):
|
||||
"""同一字面量被两个属主重复定义(N-16 类事故 / §1.2 的 Trap 02 跨文档同号不同义)。
|
||||
规范 §4.3 已声明的同号不同义族对由 `collides_with` 登记,不计为事故。"""
|
||||
allowed = set()
|
||||
for fam in self.families:
|
||||
cw = fam.get("collides_with")
|
||||
if cw:
|
||||
allowed.add(frozenset((fam["class"], cw)))
|
||||
defs = defaultdict(list) # literal -> [(class, family_title, owner_doc)]
|
||||
for fam in self.families:
|
||||
for owner in fam["_owners"]:
|
||||
for line in self.read(owner).splitlines():
|
||||
m = self.DEF_ANCHOR.match(line)
|
||||
if m and fam["_full"].match(m.group(1)):
|
||||
defs[m.group(1)].append((fam["class"], fam["title"], owner))
|
||||
for lit, hits in sorted(defs.items()):
|
||||
classes = {c for c, _, _ in hits}
|
||||
docs = {d for _, _, d in hits}
|
||||
pairs = [(a, b) for a in classes for b in classes if a < b]
|
||||
clash = [p for p in pairs if frozenset(p) not in allowed]
|
||||
if clash:
|
||||
where = sorted(docs)[:2]
|
||||
self.add(4, "error", where[0], 0, lit,
|
||||
"重复定义:%s 同时被 %s 两个属主登记处定义(%s;§8 校验 4,"
|
||||
"N-16 类事故)" % (lit, " / ".join(sorted(classes)),
|
||||
", ".join(d.rsplit("/", 1)[-1] for d in where)),
|
||||
"保留单一属主;另一处改语义标识(§3.3)或引用紧邻文档名(§5.2)")
|
||||
elif len(classes) == 1 and len(docs) >= 2:
|
||||
self.add(4, "error", sorted(docs)[0], 0, lit,
|
||||
"重复定义:%s 在同一族「%s」的 %d 篇属主文档里各定义一次(%s;"
|
||||
"§8 校验 4,§1.2 的 Trap 02 跨文档同号不同义类事故)"
|
||||
% (lit, hits[0][1], len(docs),
|
||||
", ".join(sorted(d.rsplit("/", 1)[-1] for d in docs)[:3])),
|
||||
"只在一处定义;另一处改引用,或按 §6 补模块码把两族拆开")
|
||||
else:
|
||||
self.stats["collision_allowed"] += 1
|
||||
|
||||
def check_orphans(self):
|
||||
for fam in self.families:
|
||||
if not fam.get("check_dangling") or not fam["_owners"]:
|
||||
continue
|
||||
for lit in sorted(self.owner_tokens[fam["name"]]):
|
||||
outside = {r for r in self.refs.get(lit, set()) if r not in fam["_owners"]}
|
||||
if not outside:
|
||||
self.add(7, "warn", fam["_owners"][0], 0, lit,
|
||||
"孤儿条目:%s 定义在 %s,全库无引用(§8 校验 7)"
|
||||
% (lit, fam["_owners"][0]),
|
||||
"确认已闭环可销项(§5.5),或补上引用")
|
||||
|
||||
# ---------- 报告 ----------
|
||||
TITLES = {
|
||||
1: "校验 1 未登记前缀(自造编号)",
|
||||
2: "校验 2 形态不合规(裸形/空格形/中文形/补零/档位词带后缀)",
|
||||
3: "校验 3 悬空引用(查无定义且不在别名表)",
|
||||
4: "校验 4 重复定义(同一字面量两个属主)",
|
||||
5: "校验 5 冻结前缀越游标",
|
||||
6: "校验 6 文档内编号未紧邻文档名",
|
||||
7: "校验 7 孤儿条目(定义了但无引用)",
|
||||
}
|
||||
|
||||
def report(self, top, only):
|
||||
by_code = defaultdict(list)
|
||||
for f in self.findings:
|
||||
by_code[f.code].append(f)
|
||||
print("=== 编号门禁 check_ids.py(docs/编号规范.md §8)===")
|
||||
print("扫描 %d 个文件;编号字面量 %d 处(豁免 [示例]/[计划] 行 %d 处、"
|
||||
"非编号噪声 %d 处、裸形盲区 %d 处、命中别名表 %d 处)"
|
||||
% (self.stats["files"], self.stats["refs_total"], self.stats["skip_example"],
|
||||
self.stats["noise_allowed"], self.stats["skip_bare_unmanaged"],
|
||||
self.stats["alias_hits"]))
|
||||
print("已声明的同号不同义族对(§4.3):%d 个字面量,不计入校验 4"
|
||||
% self.stats["collision_allowed"])
|
||||
|
||||
for code in sorted(self.TITLES):
|
||||
if only and code not in only:
|
||||
continue
|
||||
items = [f for f in by_code.get(code, [])]
|
||||
n_err = sum(1 for f in items if f.severity == "error")
|
||||
n_warn = len(items) - n_err
|
||||
print("\n[%s] %s:%d 处(错误 %d / 告警 %d)"
|
||||
% ("WARN" if not n_err else "FAIL", self.TITLES[code],
|
||||
len(items), n_err, n_warn))
|
||||
for f in items[:top]:
|
||||
loc = "%s:%d" % (f.rel, f.lineno) if f.lineno else f.rel
|
||||
# 带上校验号 + 级别:--top 打出来的每一行都能独立追溯回规范 §8 的表
|
||||
print(" [%d-%s] %s %s" % (f.code, "E" if f.severity == "error" else "W",
|
||||
loc, f.msg))
|
||||
if self.verbose and f.hint:
|
||||
print(" → %s" % f.hint)
|
||||
if len(items) > top:
|
||||
print(" … 其余 %d 处略(--top N 多打;--verbose 带修法)" % (len(items) - top))
|
||||
|
||||
total = self.stats["refs_total"]
|
||||
# errs 恒按**全部** findings 统计:--only 是"局部排查"用的,不能因为它少扫两类
|
||||
# 就把门禁结论报成"无错误"(真踩过的坑:--only 1,3 让 --strict 误报绿)
|
||||
errs = sum(1 for f in self.findings
|
||||
if f.severity == "error" and f.code != 8)
|
||||
if only:
|
||||
errs = sum(1 for f in self.findings
|
||||
if f.severity == "error" and f.code in only)
|
||||
print("\n⚠ --only %s:本次只跑部分校验,**不构成门禁结论**"
|
||||
% ",".join(str(c) for c in sorted(only)))
|
||||
dec = self.stats["decidable"]
|
||||
ratio = (100.0 * dec / total) if total else 100.0
|
||||
base = self.reg.get("decidable_ratio_baseline")
|
||||
print("\n[比例] 校验 8 编号引用可判定 %d/%d = %.2f%%(基线 %s,容差 %.1fpp)"
|
||||
% (dec, total, ratio,
|
||||
"未建档" if base is None else "%.1f%%" % base, RATIO_TOL))
|
||||
print(" 可判定 = 字面量落进已登记前缀族**且该族属主登记处已存在**;"
|
||||
"未登记前缀、形态违规、属主登记处还是 [计划] 的都算不可判定 → 只许涨不许跌")
|
||||
return ratio, errs
|
||||
|
||||
|
||||
def parse_only(s):
|
||||
if not s:
|
||||
return set()
|
||||
return {int(x) for x in re.split(r"[,\s]+", s) if x.strip().isdigit()}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description="编号门禁:docs/编号规范.md §8 的 8 项校验(默认 warn-only)")
|
||||
ap.add_argument("--strict", action="store_true",
|
||||
help="校验 1~6 的错误改为拦提交(默认 warn-only,不拦)")
|
||||
ap.add_argument("--verbose", action="store_true", help="每条附带修法")
|
||||
ap.add_argument("--top", type=int, default=12, help="每类校验打印的条数(默认 12)")
|
||||
ap.add_argument("--only", default="", help="只跑指定校验,如 --only 1,3,5")
|
||||
ap.add_argument("--update-baseline", action="store_true",
|
||||
help="把本次可判定比例写进 id_registry.json 作为基线(只许涨)")
|
||||
ap.add_argument("--no-trace", action="store_true",
|
||||
help="跳过顺带收编的 TC 双向追溯(check_test_traceability.py)")
|
||||
args = ap.parse_args()
|
||||
|
||||
reg = load_json(REGISTRY_FILE, "编号注册表")
|
||||
aliases = load_json(ALIASES_FILE, "编号别名表")
|
||||
|
||||
ck = Checker(reg, aliases, verbose=args.verbose)
|
||||
rc = ck.run()
|
||||
if rc:
|
||||
return rc
|
||||
ratio, errs = ck.report(args.top, parse_only(args.only))
|
||||
|
||||
if args.update_baseline:
|
||||
base = reg.get("decidable_ratio_baseline")
|
||||
if base is not None and ratio + RATIO_TOL < base:
|
||||
print("[ERR] 拒绝下调基线:本次 %.2f%% < 基线 %.2f%%(§8 校验 8「只许涨不许跌」)"
|
||||
% (ratio, base), file=sys.stderr)
|
||||
return 1
|
||||
# 基线**下取整到 0.1 个百分点**:小数点第 2 位随"这一批改了哪几篇文档"抖动,
|
||||
# 用 round() 会把 82.34 进位成 82.3 再反过来把自己判死(实测踩过),故 floor。
|
||||
reg["decidable_ratio_baseline"] = math.floor(ratio * 10) / 10.0
|
||||
REGISTRY_FILE.write_text(json.dumps(reg, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8", newline="\n")
|
||||
print("[OK] 基线已写进 scripts/id_registry.json:%.1f%%(本次实测 %.2f%%)"
|
||||
% (reg["decidable_ratio_baseline"], ratio))
|
||||
|
||||
if not args.no_trace and TRACE_SCRIPT.is_file():
|
||||
print("\n=== 校验 8 附带:TC 用例双向追溯(顺带收编 check_test_traceability.py)===")
|
||||
try:
|
||||
# 子进程的 stdout 在管道下默认按本地代码页(936/GBK)编码 → 必须显式压成
|
||||
# UTF-8,否则这里按 utf-8 解出来全是乱码(实测踩到:[DOC-ONLY] 行打印成 mojibake)
|
||||
env = dict(os.environ, PYTHONIOENCODING="utf-8")
|
||||
p = subprocess.run([sys.executable, str(TRACE_SCRIPT)], cwd=str(REPO_ROOT),
|
||||
capture_output=True, text=True, encoding="utf-8",
|
||||
errors="replace", env=env)
|
||||
for ln in (p.stdout or "").splitlines():
|
||||
if ln.startswith(("代码侧", "文档侧", "交集", "[DOC-ONLY]", "[CODE-ONLY]",
|
||||
"[OK]", "[WARN]", "=== ")):
|
||||
print(" " + ln)
|
||||
if p.returncode == 2:
|
||||
print("[WARN] 追溯脚本环境错误(退出码 2),不影响本门禁结论", file=sys.stderr)
|
||||
except OSError as exc:
|
||||
print("[WARN] 无法运行追溯脚本:%s" % exc, file=sys.stderr)
|
||||
|
||||
base = reg.get("decidable_ratio_baseline")
|
||||
if base is not None and ratio + RATIO_TOL < base and not args.update_baseline:
|
||||
print("\n[FAIL] 校验 8:可判定比例跌破基线 %.2f%% < %.2f%%(只许涨不许跌,容差 %.1fpp)"
|
||||
% (ratio, base, RATIO_TOL))
|
||||
return 1
|
||||
if errs:
|
||||
if args.strict:
|
||||
print("\n[FAIL] 编号门禁:%d 处错误级问题(--strict 拦提交)" % errs)
|
||||
return 1
|
||||
print("\n[WARN] 编号 backlog:%d 处错误级问题,warn-only 不拦提交"
|
||||
"(§8 上线节奏:清账后加 --strict)" % errs)
|
||||
else:
|
||||
print("\n[OK] 校验 1~6 无错误")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -43,8 +43,14 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
CODE_DIR = REPO_ROOT / "test" / "board"
|
||||
DOC_DIR = REPO_ROOT / "docs" / "测试说明"
|
||||
|
||||
# TC 编号:TC-<PFX>-<NNN>。<PFX> 允许字母数字(NET/GD5F/STO/RTC…)
|
||||
TC_RE = re.compile(r"TC-[A-Za-z0-9]+-\d+")
|
||||
# TC 编号:TC-<PFX>-<NNN>。<PFX> 允许字母数字(NET/GD5F/STO/RTC…)。
|
||||
# 2026-10-20 收编进 id-check 门禁时补的三条(docs/编号规范.md §8「顺带收编」):
|
||||
# ① 规范 §4.1 的合规形态 `TC-[A-Z]{1,4}-\d{3,4}[a-z]?`(带小写后缀,TC-NET-1502b);
|
||||
# ② 现存的大写后缀形态(TC-AI-01A [示例],§7.1 待统一小写)也要能抓出来,否则整条漏扫;
|
||||
# ③ 无模块码形态(iec104 的 TC-014/TC-104,§7.2 列为孤儿)纳入比对而不是静默忽略。
|
||||
TC_RE = re.compile(r"TC-(?:[A-Z][A-Z0-9]{0,4}-\d{1,4}[A-Za-z]?|\d{2,4}[a-z]?)")
|
||||
# 规范合规形态(用于 --verbose 里区分"形态待迁移"与"纯追溯断链")
|
||||
TC_CANON_RE = re.compile(r"^TC-[A-Z]{1,4}-\d{3,4}[a-z]?$")
|
||||
# 占位符前缀(文档模板里的 `TC-XXX-…`)不参与比对
|
||||
PLACEHOLDER_PREFIXES = {"XXX"}
|
||||
|
||||
@@ -80,13 +86,24 @@ def main():
|
||||
help="额外打印各前缀的 TC 计数")
|
||||
ap.add_argument("--code-dir", default=str(CODE_DIR))
|
||||
ap.add_argument("--doc-dir", default=str(DOC_DIR))
|
||||
ap.add_argument("--board-only", action="store_true",
|
||||
help="只比板级(test/board/*.c ↔ *_Test_Guide.md),"
|
||||
"不含主机侧单测一侧(2026-10-20 起默认纳入 7 篇「单元测试说明.md」)")
|
||||
args = ap.parse_args()
|
||||
|
||||
code_dir = Path(args.code_dir)
|
||||
doc_dir = Path(args.doc_dir)
|
||||
host_dir = REPO_ROOT / "test" / "host"
|
||||
|
||||
code_files = sorted(code_dir.glob("*.c")) if code_dir.is_dir() else []
|
||||
doc_files = sorted(doc_dir.glob("*_Test_Guide.md")) if doc_dir.is_dir() else []
|
||||
if not args.board_only:
|
||||
# 主机侧单测一侧:7 篇「单元测试说明.md」+ test/host 下的测试代码(*.c / *_test.py /
|
||||
# run_unit_test.py)——TC 编号在两侧都是**追溯链的锚点**,缺一侧就是断链。
|
||||
doc_files += sorted(doc_dir.glob("*单元测试说明.md"))
|
||||
if host_dir.is_dir():
|
||||
code_files += sorted(host_dir.rglob("*.c"))
|
||||
code_files += sorted(host_dir.rglob("*.py"))
|
||||
|
||||
if not code_files:
|
||||
print(f"[ERR] 未找到代码侧文件:{code_dir}/*.c", file=sys.stderr)
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"generated_from": "docs/编号规范.md §7 迁移映射 + §4.3 冻结前缀;本表只登记**仓库里已实际出现**的旧字面量 → 现行字面量,不做尚未落地的批量改写(§7.2 阶段 1 一律保留存量)",
|
||||
"note": "旧号引用不因本表而改写:本表只让门禁把「旧号引用」判成可解析(校验 3 悬空引用 / 校验 4 重复定义)。批量改写属 §9 阶段 1,须逐批 dry-run + 三重复核(§7.3)。 带 scope 的条目只在所列文件内生效(同一裸形字面量可能属多族,全局别名会把真撞号洗成已解析)。 带 scope 的条目只在所列文件内生效(同一裸形字面量可能属多族,全局别名会把真撞号洗成已解析)。",
|
||||
"aliases": {
|
||||
"N-31": {
|
||||
"to": null,
|
||||
"status": "void",
|
||||
"note": "2026-10-11 的 Phase 7 复跑项已随销项消失;2026-10-14 同号被新条目复用造成撞号(docs/任务规划/README.md §0 注)→ 历史引用指向旧条目,新含义一律改语义标识"
|
||||
},
|
||||
"N-32": {
|
||||
"to": null,
|
||||
"status": "void",
|
||||
"note": "监听生命周期模块化条目已销项,但被 docs/网络/ 三篇与多处代码注释引用(docs/编号规范.md §1.2 列为悬空引用实锤)→ §7.2 待处置:改指 docs/网络/网络自愈架构设计.md §9.3 或就地展开"
|
||||
},
|
||||
"N-33": {
|
||||
"to": null,
|
||||
"status": "void",
|
||||
"note": "栈余量收口条目已随 M1 台架闭环销项(2026-10-12),登记簿行已删,结论在 docs/内存管理/RAM使用分析与优化方案.md §1.4"
|
||||
},
|
||||
"N-30": {
|
||||
"to": null,
|
||||
"status": "resolved-in-place",
|
||||
"note": "sock_reset 原语收口受阻项:登记簿行仍在(2026-10-14 建档),编号规范 §4.1 误记为已销项 → 以登记簿为准"
|
||||
},
|
||||
"Trap 19": {
|
||||
"to": null,
|
||||
"status": "void",
|
||||
"note": "从未存在的陷阱号被 docs/测试说明/CH395F_Test_Guide.md 引用(§1.2 / §7.2:删除或改指实际条目)"
|
||||
},
|
||||
"PHY": {
|
||||
"to": "NET",
|
||||
"status": "module-code",
|
||||
"note": "§6 废止模块码:TM-PHY-01~11 → TM-NET-01~11(历史文档不回改,新文档按 §6 表)"
|
||||
},
|
||||
"NAND": {
|
||||
"to": "GD5F",
|
||||
"status": "module-code",
|
||||
"note": "§6 废止模块码:TC-NAND-* / TM-NAND-* → GD5F"
|
||||
},
|
||||
"S0": {
|
||||
"to": "MENU-STATE-00",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"simulator/tests/check_menu_rows.py",
|
||||
"docs/UI设计/UI_自动化测试流程.md",
|
||||
"simulator/README.md"
|
||||
],
|
||||
"note": "批 1:UI 菜单像素回归状态号单词化(S0~S7 = 按键序列状态 → MENU-STATE-nn)(scripts/rename_ids.py 批:族 MENU-STATE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
"S1": [
|
||||
{
|
||||
"to": "MENU-STATE-01",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"simulator/tests/check_menu_rows.py",
|
||||
"docs/UI设计/UI_自动化测试流程.md",
|
||||
"simulator/README.md"
|
||||
],
|
||||
"note": "批 1:UI 菜单像素回归状态号单词化(S0~S7 = 按键序列状态 → MENU-STATE-nn)(scripts/rename_ids.py 批:族 MENU-STATE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
{
|
||||
"to": "CODE-RULE-01",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
}
|
||||
],
|
||||
"S2": [
|
||||
{
|
||||
"to": "MENU-STATE-02",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"simulator/tests/check_menu_rows.py",
|
||||
"docs/UI设计/UI_自动化测试流程.md",
|
||||
"simulator/README.md"
|
||||
],
|
||||
"note": "批 1:UI 菜单像素回归状态号单词化(S0~S7 = 按键序列状态 → MENU-STATE-nn)(scripts/rename_ids.py 批:族 MENU-STATE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
{
|
||||
"to": "CODE-RULE-02",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
}
|
||||
],
|
||||
"S3": [
|
||||
{
|
||||
"to": "MENU-STATE-03",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"simulator/tests/check_menu_rows.py",
|
||||
"docs/UI设计/UI_自动化测试流程.md",
|
||||
"simulator/README.md"
|
||||
],
|
||||
"note": "批 1:UI 菜单像素回归状态号单词化(S0~S7 = 按键序列状态 → MENU-STATE-nn)(scripts/rename_ids.py 批:族 MENU-STATE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
{
|
||||
"to": "CODE-RULE-03",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
}
|
||||
],
|
||||
"S4": [
|
||||
{
|
||||
"to": "MENU-STATE-04",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"simulator/tests/check_menu_rows.py",
|
||||
"docs/UI设计/UI_自动化测试流程.md",
|
||||
"simulator/README.md"
|
||||
],
|
||||
"note": "批 1:UI 菜单像素回归状态号单词化(S0~S7 = 按键序列状态 → MENU-STATE-nn)(scripts/rename_ids.py 批:族 MENU-STATE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
{
|
||||
"to": "CODE-RULE-04",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
}
|
||||
],
|
||||
"S5": [
|
||||
{
|
||||
"to": "MENU-STATE-05",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"simulator/tests/check_menu_rows.py",
|
||||
"docs/UI设计/UI_自动化测试流程.md",
|
||||
"simulator/README.md"
|
||||
],
|
||||
"note": "批 1:UI 菜单像素回归状态号单词化(S0~S7 = 按键序列状态 → MENU-STATE-nn)(scripts/rename_ids.py 批:族 MENU-STATE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
{
|
||||
"to": "CODE-RULE-05",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
}
|
||||
],
|
||||
"S6": [
|
||||
{
|
||||
"to": "MENU-STATE-06",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"simulator/tests/check_menu_rows.py",
|
||||
"docs/UI设计/UI_自动化测试流程.md",
|
||||
"simulator/README.md"
|
||||
],
|
||||
"note": "批 1:UI 菜单像素回归状态号单词化(S0~S7 = 按键序列状态 → MENU-STATE-nn)(scripts/rename_ids.py 批:族 MENU-STATE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
{
|
||||
"to": "CODE-RULE-06",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
}
|
||||
],
|
||||
"S7": [
|
||||
{
|
||||
"to": "MENU-STATE-07",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"simulator/tests/check_menu_rows.py",
|
||||
"docs/UI设计/UI_自动化测试流程.md",
|
||||
"simulator/README.md"
|
||||
],
|
||||
"note": "批 1:UI 菜单像素回归状态号单词化(S0~S7 = 按键序列状态 → MENU-STATE-nn)(scripts/rename_ids.py 批:族 MENU-STATE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
{
|
||||
"to": "CODE-RULE-07",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
}
|
||||
],
|
||||
"S10": {
|
||||
"to": "CODE-RULE-10",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
"S11": {
|
||||
"to": "CODE-RULE-11",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
"S12": {
|
||||
"to": "CODE-RULE-12",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
"S13": {
|
||||
"to": "CODE-RULE-13",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
"S15": {
|
||||
"to": "CODE-RULE-15",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
"S16": {
|
||||
"to": "CODE-RULE-16",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
"S17": {
|
||||
"to": "CODE-RULE-17",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
"S18": {
|
||||
"to": "CODE-RULE-18",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
"S19": {
|
||||
"to": "CODE-RULE-19",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
"S8": {
|
||||
"to": "CODE-RULE-08",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
},
|
||||
"S9": {
|
||||
"to": "CODE-RULE-09",
|
||||
"status": "renamed",
|
||||
"scope": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)(scripts/rename_ids.py 批:族 CODE-RULE)|旧号仅在本批 scope 内指向新号;scope 外同字面量属其它族,不受本条影响"
|
||||
}
|
||||
},
|
||||
"id_aliases": {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,251 @@
|
||||
#!/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. 同一个裸形字面量在不同文件里可能是**不同族**(实测 `S3` 既是代码规范条款、又是
|
||||
UI 像素回归状态、又是 2026-10-02 复审专项条目)→ **改名与别名都必须限定作用域**,
|
||||
全局替换会制造新的撞号(正是 §1.2 记录的 N-31/N-32 类事故)。
|
||||
|
||||
用法:
|
||||
& '.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/硬件平台/", "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):
|
||||
vals = {}
|
||||
for k, v in groups.items():
|
||||
if v is None:
|
||||
vals[k] = ""
|
||||
else:
|
||||
vals[k] = int(v) if v.isdigit() else 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", {})
|
||||
scope = [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 "全库生效(本批无 scope)")
|
||||
entry = {
|
||||
"to": sorted(news)[0] if len(news) == 1 else sorted(news),
|
||||
"status": "renamed",
|
||||
"scope": scope,
|
||||
"note": "%s(scripts/rename_ids.py 批:族 %s)|%s"
|
||||
% (batch.get("note", "批量改名"), batch.get("family"), scope_txt),
|
||||
}
|
||||
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())
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"batch": "批 1(2026-10-20 已 --apply)",
|
||||
"applied_by": "scripts/rename_ids.py",
|
||||
"record": "docs/编号规范.md §8.3",
|
||||
"note": "映射表即审计轨迹:谁改的、改了什么、作用域多大,全部可复演",
|
||||
"batches": [
|
||||
{
|
||||
"family": "CODE-RULE",
|
||||
"old": "\\bS(?P<n>1[0-9]|[1-9])\\b",
|
||||
"new": "CODE-RULE-{n:02d}",
|
||||
"files": [
|
||||
"scripts/check_coding_standard.py",
|
||||
"docs/代码规范/pre-commit使用说明.md"
|
||||
],
|
||||
"note": "批 1:代码规范条款号单词化(S1~S19 → CODE-RULE-nn)。原裸形 S 号与登记簿 S-x、UI 像素状态 S0~S7、2026-10-02 复审专项条目 S-x 四义撞形(docs/编号规范.md §1.2)"
|
||||
},
|
||||
{
|
||||
"family": "MENU-STATE",
|
||||
"old": "\\bS(?P<n>[0-7])\\b",
|
||||
"new": "MENU-STATE-{n:02d}",
|
||||
"files": [
|
||||
"simulator/tests/check_menu_rows.py",
|
||||
"docs/UI设计/UI_自动化测试流程.md",
|
||||
"simulator/README.md"
|
||||
],
|
||||
"note": "批 1:UI 菜单像素回归状态号单词化(S0~S7 = 按键序列状态 → MENU-STATE-nn)"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user