diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index edec0f57..a9bf27a0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -521,3 +521,30 @@ repos: language: system files: ^(Drivers/BSP/UI/|App/task/ui_task\.(c|h)$|simulator/(tests|sim|src)/|simulator/main\.c$|simulator/CMakeLists\.txt$|simulator/lv_conf\.h$|scripts/check_ui_golden\.py$|\.pre-commit-config\.yaml$) pass_filenames: false + + # 13) 编号门禁(2026-10-20 加,docs/编号规范.md §8 的 8 项校验) + # 背景:§1.2 用仓内证据列了 10 类编号事故(同字母多义 / 跨文档同号不同义 / + # 销项删行后残留悬空引用 / 自造前缀 / 会话临时号入册…),而 36 个钩子里 + # **只有 TC 一族**有机械检查(check_test_traceability.py,且未挂钩子、正则还漏 [示例] + # `TC-AI-01A` [示例] 这类大写后缀)——E/N/S/B/C/D/A/U/O/gen/Trap 全部裸奔。本钩子把 + # 规范 §8 的 8 项机械化:配置两份 `scripts/id_registry.json`(前缀 → 属主登记处 + # /形态/冻结/游标,逐项来自规范 §4 总表)+ `scripts/id_aliases.json`(旧号→新号, + # 照搬 section_aliases.json 的成功范式)。 + # 判据要点:「定义」= 字面量出现在该族**属主登记处文档**里;于是 §5.5 的 + # 「销项 = 删行 + 清全库引用」只做一半,校验 3 立刻报(N-31/N-32 正是这条)。 + # 盲区显式登记在 registry 的 `blind_spots` / `unmanaged_bare`:裸形同字母多义 + # (§1.2:P1 五义 / L1 四义 / U1 四义)与元器件位号同形(§4.4),机械全库判定 + # 必然一边倒误报 → 裸形族只在属主文档内判越游标、文档外降告警;收口属 §9 阶段 + # 1-② 的迁移批次。**不是「检查通过」,是记在案上的盲区。** + # ⚠ 当前 warn-only(§8 上线节奏:先清 backlog → 清零后 entry 加 --strict 拦提交, + # 与 doc-links 章节校验同一条路径)。校验 8 的「可判定比例」跌破 + # id_registry.json 里的基线时**任何模式都失败**——那根线就是防回退的本体。 + # ⚠ pre-commit 4.x 已移除 `always: true` → files 必须覆盖所有能改编号的触发面 + # (任意 md/c/h/py/yaml/json/txt + 本配置文件);扫描范围恒为全库 + # (悬空/孤儿/重复定义是跨文件判定,pass_filenames: false)。 + - id: id-check + name: 编号门禁(docs/编号规范.md §8 八项校验) + entry: python scripts/check_ids.py + language: system + files: \.(md|c|h|py|ya?ml|json|txt)$|^\.pre-commit-config\.yaml$ + pass_filenames: false diff --git a/AGENTS.md b/AGENTS.md index c4d2f715..c8c4cebe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,7 +90,7 @@ ## 提交门禁(pre-commit) -- 任一失败即拦截,共 **36 个钩子**(逐个详解 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 16 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,N-23/U4)+ `mem-budget`(RAM 92% 预算门禁,N-27)+ `font-charset`(UI 字符集差集检查,防缺字白框,U4)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 11 页黄金参考,U4) +- 任一失败即拦截,共 **37 个钩子**(逐个详解 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check`(**编号门禁**,`docs/编号规范.md` §8 八项校验;**当前 warn-only**,可判定比例跌破 `id_registry.json` 基线才失败)+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 16 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,N-23/U4)+ `mem-budget`(RAM 92% 预算门禁,N-27)+ `font-charset`(UI 字符集差集检查,防缺字白框,U4)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 11 页黄金参考,U4) - **范围 = 项目代码**:第三方(`Lib/`、`Middlewares/`、CMSIS、HAL、`lftpd`、`dhara`、`MDK-ARM`)排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`。**`simulator/` 已纳入全部钩子**(2026-10-11)——生成物只靠 `.gitignore` 挡,**不在 `exclude` 里写目录名**(教训见 `docs/代码规范/pre-commit使用说明.md` §5.5) - ⚠ **pre-commit 4.x 已移除 `always: true`**(实测对 `local` 钩子只告警 `Unexpected key(s)` 并跳过)→ 想"总是跑"的钩子**必须让 `files:` 覆盖到所有会触发它的文件**,没有兜底开关 - **改写型钩子(`trailing-whitespace` + `end-of-file-fixer`)豁免 CubeMX/Keil 生成文件**(配置里 YAML 锚点 `&cubemx-generated`);⚠ **只读检查不受影响**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` / `mem-numbers` 仍覆盖 `Src/`、`Inc/` @@ -153,7 +153,7 @@ - **登记簿类文档「完成即销项」**:`docs/任务规划/README.md` 这类待办登记簿,事项修复/关闭后**直接从表中删除该行**(不保留"已消化"行)——结论与证据应已沉淀进对应模块文档/问题记录。销项时必须:① 先核对目标文档确已含完整记录 ② 同步清理其他文档对该编号的引用 ③ 改完跑 `check_doc_links.py --all` + pre-commit。**新条目标识(用户裁定 2026-10-14)**:`#` 列填**语义短语**(如 `沉降窗 800ms 上限只留痕`),**不再新增字母+数字编号**——"编号不回收"只写规则却不留痕迹就不可执行(销项删行后仓库无痕迹,实测 `N-31` 于 2026-10-14 被复用撞号),语义标识自带唯一性;存量编号冻结沿用、**已随销项消失的号永久作废** → `docs/任务规划/README.md` §0 - 图形走 **`draw-diagram` skill**:精确布局图用 `tools/arch_diagram/gen_*.py` 生成器(产物放 `docs/images/`,脚本与 .svg 一并提交);拓扑图用 Mermaid;**勿手绘 ASCII 框图**。文件引用关系图走 `tools/refgraph`(`& 'tools\refgraph\gen_refgraph.bat'`) -- **改名 / 挪目录一律走 `scripts/rename_ref.py`(先 dry-run,确认后 `--apply`)**:`git mv` + 自动改写全库引用(反引号引用 / Markdown 链接 / 图片 `../` 层级换算 / `#include` / `uvprojx` `` / `CMakeLists.txt`),落盘后自动跑 `check_doc_links --all` 复核——手工扫引用既慢又漏(一次改名历史上波及 10~30 文件)。引用章节用「`路径.md` §N.M」**紧邻写法**(`doc-links` 校验编号是否还在:紧邻/裸名/前缀/别名/自引用**全部拦提交**(钩子带 `--sections-strict`);每次跑打印「可判定比例」,`--section-todo` 出待改清单,昵称歧义就登记 `scripts/section_aliases.json` 或写清楚)→ `docs/文档规范.md` §3.5 / §3.6 +- **改名 / 挪目录一律走 `scripts/rename_ref.py`(先 dry-run,确认后 `--apply`)**:`git mv` + 自动改写全库引用(反引号引用 / Markdown 链接 / 图片 `../` 层级换算 / `#include` / `uvprojx` `` / `CMakeLists.txt`),落盘后自动跑 `check_doc_links --all` 复核——手工扫引用既慢又漏(一次改名历史上波及 10~30 文件)。引用章节用「`路径.md` §N.M」**紧邻写法**(`doc-links` 校验编号是否还在:紧邻/裸名/前缀/别名/自引用**全部拦提交**(钩子带 `--sections-strict`);每次跑打印「可判定比例」,`--section-todo` 出待改清单,昵称歧义就登记 `scripts/section_aliases.json` 或写清楚)→ `docs/文档规范.md` §3.5 / §3.6。**编号改名走 `scripts/rename_ids.py`**(映射表驱动、**显式 scope**、干跑默认、自动写 `scripts/id_aliases.json` 的 scoped 旧号→新号条目;`[示例]`/`[计划]` 行与变更记录表不改写;**绝不全局查找替换**——同一裸形字面量可能同时属多个族,全局替换等于造新撞号,实测 `S3` 三义) - 文档写作规范(编码/BOM/链接/表格/一致性钩子)→ `docs/文档规范.md`;全量索引与推荐阅读顺序 → `docs/README.md` ## 边界(Ask first / Never) @@ -173,7 +173,8 @@ | ADC 采样节拍 / 通道映射 / V0.1 硬件缺陷 | `docs/子系统设计/ADC采样节拍设计说明.md`、`docs/硬件平台/ADC通道映射说明.md` | | 存储栈(FatFs / dhara / lftpd / 两条 FatFs 陷阱) | `docs/架构设计/STM32F4-Base存储架构说明.md` | | RAM / 栈预算 / 容量台账 | `docs/内存管理/RAM使用分析与优化方案.md`、`内存内容分配策略.md` | -| pre-commit 36 个钩子逐个详解 | `docs/代码规范/pre-commit使用说明.md` | +| pre-commit 37 个钩子逐个详解 | `docs/代码规范/pre-commit使用说明.md` | +| 实体编号怎么写 / 前缀总表 / `id-check` 八项校验 | `docs/编号规范.md`(§4 总表、§8 门禁与实测基线) | | 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` | | 自动化测试 / 实机测试 / 板级套件 | `docs/测试说明/自动化测试流程.md`、`实机测试实施指南.md`、`board-test-suite` skill | | 烧录与复位语义 | `docs/测试说明/ST-LINK_CLI_复位使用说明.md` | diff --git a/docs/UI设计/UI_自动化测试流程.md b/docs/UI设计/UI_自动化测试流程.md index 2f3ec3d7..a98320ae 100644 --- a/docs/UI设计/UI_自动化测试流程.md +++ b/docs/UI设计/UI_自动化测试流程.md @@ -25,7 +25,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 | 步 | 脚本/命令 | 门禁内容 | 预期输出 | |---|---|---|---| -| 1 | `simulator/tests/check_menu_rows.py` | 菜单 8 状态:选中行白带 / 非选行黑底白字 / 框体 1px 边框 / 往返像素一致(S3==S1、S6==S1、S7==S2)+ S0 池基线 | `PASS: 8 状态全部断言通过(8 图,池基线 5368 B)` | +| 1 | `simulator/tests/check_menu_rows.py` | 菜单 8 状态:选中行白带 / 非选行黑底白字 / 框体 1px 边框 / 往返像素一致(MENU-STATE-03==MENU-STATE-01、MENU-STATE-06==MENU-STATE-01、MENU-STATE-07==MENU-STATE-02)+ MENU-STATE-00 池基线 | `PASS: 8 状态全部断言通过(8 图,池基线 5368 B)` | | 2 | `simulator/tests/check_content_pages.py` | ① 文本级断言(字形模板匹配,50 条,独立于 `ref/`)→ ② 11 页截图与 `ref/` 黄金参考**逐字节**比对 + 开页-关页往返池水位 | `PASS: 11 内容页 + 往返池水位全部通过`(文本断言先于像素比对,失败即中止) | | 3 | `keil-build` skill(`keil_build.py`) | MCU 端编译(UI 源码双端共用,改一端必须过另一端) | 退出码 0 = BUILD_OK | | 4 | `scripts/check_coding_standard.py` | 代码规范 V1.0 机械检查 | PASS | diff --git a/docs/代码规范/pre-commit使用说明.md b/docs/代码规范/pre-commit使用说明.md index 3a4e0e0a..d7b25ce7 100644 --- a/docs/代码规范/pre-commit使用说明.md +++ b/docs/代码规范/pre-commit使用说明.md @@ -153,30 +153,30 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法 |---|---| | **触发条件** | 自有代码 .c/.h 文件(App/、Drivers/BSP/,第三方与 CubeMX 生成文件除外)违反下列规则 | | **处理方式** | **阻止提交**(必须修复) | -| **典型报错** | `L123 [S3 3.4.1] 宏名须全大写+下划线: net_htonl` | +| **典型报错** | `L123 [CODE-RULE-03 3.4.1] 宏名须全大写+下划线: net_htonl` | **检查项(S 编号与脚本输出一致)**: | 编号 | 规范条款 | 规则 | |---|---|---| -| 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(2026-10-11 细化判定形态:禁的是"用路径找文件"——引号 include 带 `/`、任意形态带 `..` / `\` / 绝对路径或盘符;`` 这类**尖括号系统头允许带子目录**,由 `-I` 解析。原实现把任何含 `/` 的 include 一律判违规,误伤模拟器侧 2 处;全仓库复核:细化后仅这 2 处项目代码判定发生变化,其余 100 处均在已排除的 `Lib/lvgl/`) | -| 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(2026-10-11 细化判定形态:禁的是"用路径找文件"——引号 include 带 `/`、任意形态带 `..` / `\` / 绝对路径或盘符;`` 这类**尖括号系统头允许带子目录**,由 `-I` 解析。原实现把任何含 `/` 的 include 一律判违规,误伤模拟器侧 2 处;全仓库复核:细化后仅这 2 处项目代码判定发生变化,其余 100 处均在已排除的 `Lib/lvgl/`) | +| 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 | 函数定义左大括号必须与右括号同行 | **不在检查范围**(语义类规则,无法机械可靠判定):函数长度/单一职责、参数数量、错误处理、注释掉的代码等,仍需人工评审把关。 @@ -446,6 +446,21 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法 | **首次落地实测(牙齿)** | 注入均打在**临时副本**(`--chars` / `--font` 覆盖,不碰仓库文件):① txt 删 ▶ → FAIL 点名 `▶(U+25B6)`;② 字体副本删 U+25B6 单码位 cmap 条目 → FAIL 同字;③ 字体副本删 U+538B 的稀疏 `unicode_list` 偏移 → FAIL 点名 `压(U+538B)`;基线 PASS。落地时 txt 内有 **7 个历史冗余字**(上/常/异/片/电/芯/重,源码已不用)→ 仅 WARN 不拦,下次 font-regen 顺带瘦身 | | **运行器** | `scripts/check_font_charset.py`(纯标准库;`--chars` / `--font` 指临时副本做注入测试、`--strict` 升级冗余字;退出码 0=通过 / 1=白框缺口 / 2=环境错误) | +### 2.25 id-check — 编号门禁(2026-10-20 加,`docs/编号规范.md` §8) + +| 项 | 说明 | +|---|---| +| **触发条件** | 任意 `.md` / `.c` / `.h` / `.py` / `.yaml` / `.json` / `.txt` 或本配置文件变动(pre-commit 4.x 无 `always: true`,`files:` 按扩展名覆盖全触发面);**`pass_filenames: false`**——悬空引用 / 孤儿条目 / 重复定义是**跨文件**判定,只喂变更文件必然判错,故每次恒扫全库(实测 536 个文件 1.1 s,够快) | +| **处理方式** | **当前 warn-only**(`docs/编号规范.md` §8 上线节奏:先清 backlog → 清零后 `entry` 加 `--strict` 拦提交,与 `doc-links` 章节校验同一条路径)。**唯一例外**:校验 8 的「可判定比例」跌破 `scripts/id_registry.json` 里的基线(82.3%)→ **任何模式都 FAIL**,这根线就是防回退的本体 | +| **八项校验**([示例] 本行引用的是各种非法形态示例,不是真实编号) | ①未登记前缀 ②形态不合规(裸形 `S3` / 空格形 `Trap 07` / 中文形 `陷阱 1` / 补零错 / 档位词带后缀 / 大写后缀 / 缺模块码)③悬空引用 ④重复定义 ⑤冻结前缀越游标 ⑥文档内编号未紧邻文档名 ⑦孤儿条目(告警)⑧可判定比例(只许涨不许跌) | +| **配置两份** | `scripts/id_registry.json`:前缀族 → 属主登记处 / 形态正则 / 是否冻结 / 游标,**逐项来自 `docs/编号规范.md` §4 总表**,游标取「规范记载值」与「全库实测最大号」的较大者,偏差写进 `cursor_note`(实测偏差见规范 §8.2);`scripts/id_aliases.json`:旧号 → 新号 / 作废标记(照搬 `section_aliases.json` 的成功范式) | +| **判定口径** | 「定义」= 字面量出现在该族**属主登记处文档**里(不依赖表格行首等易漂移形态)→ §5.5「销项 = 删行 + 清全库引用」只做一半,校验 ③ 立刻报(`N-31`/`N-32` 正是这条)。「重复定义」= 字面量出现在**定义位**(表格首格 / 标题行 / 列表项行首),正文提到不算,否则任何交叉引用都会误判成事故 | +| **顺带收编** | `scripts/check_test_traceability.py`(TC 用例双向追溯)由本钩子一并调用:正则补齐 `TC-[A-Z]{1,4}-\d{3,4}[a-z]?` 与大写后缀 / 无模块码形态(旧 `TC-[A-Za-z0-9]+-\d+` **整条漏扫** `TC-AI-01A`),比对范围从「`test/board/*.c` ↔ `*_Test_Guide.md`」扩到 `test/host/` 一侧 + 7 篇「单元测试说明.md」(差异从 33 处涨到 326 处——**原口径漏的正是主机侧单测这一半**),另加 `--board-only` 回退旧口径 | +| **首轮实测(2026-10-20)** | 8271 处编号字面量 / 可判定 **82.3%**(6808/8271);错误级:②416 ③626 ⑥269,告警级:③239 ⑦88。错误集中在 `TC-PS-*`/`TC-PE-*`/`TC-AH-*`/`TC-SOE-*`(**主机侧单测有码无文档**,正是 §7.2 列的"缺文档 = 追溯断链")与报告自有编号被裸引(`D-x`/`R-x`/`F-x`/`P0-x`,属 §9 阶段 1-③)。逐项数字与口径见 `docs/编号规范.md` §8.1 | +| **有牙实测(牙齿)**([示例] 下列字面量是注入用的假编号) | 注入打在临时档(`git add -N` 进索引,跑完 `git rm --cached` + 删除,`git status` 干净):`Z-99`→①、裸形 `S3`→②、`TC-NET-9999`→③、第二篇属主文档重复定义 `DP-01`→④、`E-70`→⑤(`E-xx` 冻结于 `E-63`)、裸引 `D-7`→⑥;六项分别被对应校验点名,`--strict` 退出码 1 | +| **显式盲区(不是"检查通过")** | ① 裸形同字母多义(§1.2:`P1` 五义 / `L1` 四义 / `U1` 四义 / `D7` 三义)与元器件位号同形(§4.4)→ 裸形族**只在属主登记处文档内**判越游标,文档外降告警;② 圈号编号(①②③…)与正常列举无法机械区分,不入判定;③ `docs/芯片手册/`、`docs/硬件平台/` 整体排除(位号 `B1`/`C1`/`L1` 与编号同形,是撞形根源)。前两块收口属 §9 阶段 1-②/1-③ 迁移批次,落地后再扩判定范围——盲区登记在 `id_registry.json` 的 `blind_spots` / `unmanaged_bare` | +| **运行器** | `scripts/check_ids.py`(纯标准库;`--strict` 拦提交、`--update-baseline` 抬基线(**只许涨**,跌则拒绝写入)、`--only 1,3,5` 跑部分、`--top N` / `--verbose` 出细节、`--no-trace` 跳过 TC 追溯;退出码 0=通过或仅告警 / 1=错误或比例跌破基线 / 2=环境错误) | + --- ## 3. 日常工作流 @@ -561,9 +576,9 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env **为什么必须修**:模拟器与固件**共用同一套 UI 源码**,且它是 UI 回归唯一的像素级证据来源——它落在门禁外,等于验证链的地基不受检。 -**纳入后的实测收益(当天)**:`cppcheck` 立刻揪出 `sim_syslog.c` 一处**真实空指针解引用**(`if (p_epoch == NULL || …) { *p_epoch = 0u; … }`——判空分支里紧接着解引用),另有 2 处 `localtime_s` 未初始化告警、1 处 `px` 可加 `const`;`coding-standard` 报出 61 处(44 处函数左大括号 Allman 风格、8 处缺函数注释、3 处缺文件头注释、2 处 `__` 前缀保护宏、2 处 `sprintf`、2 处 S11 误报),已全部处理。 +**纳入后的实测收益(当天)**:`cppcheck` 立刻揪出 `sim_syslog.c` 一处**真实空指针解引用**(`if (p_epoch == NULL || …) { *p_epoch = 0u; … }`——判空分支里紧接着解引用),另有 2 处 `localtime_s` 未初始化告警、1 处 `px` 可加 `const`;`coding-standard` 报出 61 处(44 处函数左大括号 Allman 风格、8 处缺函数注释、3 处缺文件头注释、2 处 `__` 前缀保护宏、2 处 `sprintf`、2 处 CODE-RULE-11 误报),已全部处理。 -**顺带修掉的规则缺陷**:S11 原实现把**任何含 `/` 的 include** 判违规,误伤 `` 这类系统头;已按 include 形态细化,判定见 §2.8。 +**顺带修掉的规则缺陷**:CODE-RULE-11 原实现把**任何含 `/` 的 include** 判违规,误伤 `` 这类系统头;已按 include 形态细化,判定见 §2.8。 **教训**:`exclude` 只写**真正不该进钩子的路径**。已被 `.gitignore` 覆盖的生成物不需要再挡一遍——写上去只会连带豁免同一目录里的自研代码。 @@ -573,7 +588,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env | 文件 | 位置 | 说明 | |---|---|---| -| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**36 个钩子**,2026-10-15 清点;本文 §2 详解非单测类——N-19 新增的两个单测钩子见 §2.16/§2.17,N-23 双端注册反向校验(含 U4 补强的 UI 页面描述符表接线 ④)见 §2.18,N-27 RAM 预算门禁见 §2.19,B4 IEC104 协议栈单测见 §2.20,B8 频率测量单测见 §2.21,B9 SOE 动作记录单测见 §2.22,N-21 dhara NAND 移植层单测见 §2.23,U4 残余字符集差集检查见 §2.24,其余主机侧单测见 AGENTS.md 门禁条;主机侧单测共 16 个;UI 门禁另有 `font-charset` 与 `ui-golden`) | +| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**37 个钩子**,2026-10-20 清点;本文 §2 详解非单测类——N-19 新增的两个单测钩子见 §2.16/§2.17,N-23 双端注册反向校验(含 U4 补强的 UI 页面描述符表接线 ④)见 §2.18,N-27 RAM 预算门禁见 §2.19,B4 IEC104 协议栈单测见 §2.20,B8 频率测量单测见 §2.21,B9 SOE 动作记录单测见 §2.22,N-21 dhara NAND 移植层单测见 §2.23,U4 残余字符集差集检查见 §2.24,编号门禁八项校验见 §2.25,其余主机侧单测见 AGENTS.md 门禁条;主机侧单测共 16 个;UI 门禁另有 `font-charset` 与 `ui-golden`) | | `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) | | `.cubemx_survival_baseline.json` | 仓库根 | CubeMX 易失定制基线(哈希;`--write` 重建) | | `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 | @@ -585,7 +600,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env | `scripts/check_lv_conf_sync.py` | scripts/ | 两份 `lv_conf.h` 特性子集一致性(比对生效值 + 无效宏名 + 重复定义) | | `scripts/check_test_suite.py` | scripts/ | 测试套件开关门禁(防测试固件静默提交) | | `scripts/check_font_charset.py` | scripts/ | UI 字符集差集检查(复用 `tools/font_gen/` 提取与 cmap 解析规则,防缺字白框) | -| `docs/代码规范/嵌入式C语言代码规范_V1.0.md` | docs/代码规范/ | 代码规范原文(S1~S19 规则来源) | +| `docs/代码规范/嵌入式C语言代码规范_V1.0.md` | docs/代码规范/ | 代码规范原文(CODE-RULE-01~CODE-RULE-19 规则来源) | | `docs/代码规范/cppcheck使用说明.md` | docs/代码规范/ | cppcheck 钩子详解(规则/配置/抑制) | | `docs/文档规范.md` | docs/ | 文档体系规范(链接形态/命名/门禁要求) | | `docs/代码规范/pre-commit使用说明.md` | 本文档 | 部署与使用说明 | diff --git a/docs/环境与部署/并行任务工作树方案.md b/docs/环境与部署/并行任务工作树方案.md index e01e2a09..ee7d2693 100644 --- a/docs/环境与部署/并行任务工作树方案.md +++ b/docs/环境与部署/并行任务工作树方案.md @@ -397,7 +397,7 @@ docs\内存管理\RAM使用分析与优化方案.md:585 [路径引用] docs/内 | 主题 | 去处 | |---|---| -| 提交门禁 35 个钩子 | `docs/代码规范/pre-commit使用说明.md` | +| 提交门禁 37 个钩子 | `docs/代码规范/pre-commit使用说明.md` | | 自动化测试与台架流程 | `docs/测试说明/自动化测试流程.md` | | 文档写作规范(编码/链接/登记) | `docs/文档规范.md` | | 工具路径环境变量(换机) | `docs/环境与部署/本地工具路径环境变量方案.md` | diff --git a/docs/编号规范.md b/docs/编号规范.md index f8d5c404..c5270d6b 100644 --- a/docs/编号规范.md +++ b/docs/编号规范.md @@ -2,9 +2,9 @@ > **适用对象**:本仓库全部 Markdown 文档、C/Python/脚本源码注释、提交信息中出现的**实体编号**(`E-44`、`N-21`、`TC-NET-601`、`FR-STO-1`、`Trap 07` 这类"字母 + 数字"的条目身份标识)。 > **不适用**:C 语言标识符(`g_`/`s_`/`PS_*`/`TEST_SUITE_*`)、元器件位号(`U1`/`C12`/`R3`)、章节号(`§3.2`,归 [`docs/文档规范.md`](文档规范.md) §3.5 管)——见下文「划界」一节。 -> **强制程度**:§2~§5 为**强制**(规划由 `scripts/check_ids.py` 机械校验);§6~§10 为**约定 + 迁移计划**。 -> **相关文档**:[`docs/文档规范.md`](文档规范.md)(链接/章节/门禁总规范)、[`docs/任务规划/README.md`](任务规划/README.md) §0(本规范的登记簿摘要)、[`docs/测试说明/测试文档模板规范.md`](测试说明/测试文档模板规范.md) §6(测试侧编号细则)。 -> **状态**:本文件是**规划稿**——§4 的目标前缀与 §7 的迁移映射在阶段 1 落地前尚未生效,落地进度见 §9。 +> **强制程度**:§2~§5 为**强制**(由 `scripts/check_ids.py` 机械校验,§8 已落地 warn-only);§6~§10 为**约定 + 迁移计划**。 +> **相关文档**:[`docs/文档规范.md`](文档规范.md)(链接/章节/门禁总规范)、[`docs/任务规划/README.md`](任务规划/README.md) §0(本规范的登记簿摘要)、[`docs/测试说明/测试文档模板规范.md`](测试说明/测试文档模板规范.md) §6(测试侧编号细则)、[`docs/代码规范/pre-commit使用说明.md`](代码规范/pre-commit使用说明.md) §2.25(`id-check` 钩子)。 +> **状态**:**§8 门禁已落地**(warn-only,含 `scripts/id_registry.json` / `scripts/id_aliases.json`,实测基线见 §8.1);§4 的目标前缀与 §7 的迁移映射在阶段 1 落地前**尚未生效**,落地进度见 §9。 --- @@ -39,7 +39,7 @@ | **悬空引用(引用了不存在的号)** | `Trap 19` 从未存在却被 `docs/测试说明/CH395F_Test_Guide.md` 引用;`N-5/N-8/N-19/N-30/N-31/N-32` 销项删行后仍被引用(`N-32` = 监听生命周期模块化,被 `docs/网络/` 三篇与代码注释多处引用) | | **同一事物多种字面** | `Trap 07` / `TRAP-07` / `陷阱 1`;`gen 181` / `gen-288` / `gen68`;`TC-NET-704` / `TC704` / `TC-203` | | **规范自身就不自洽** | `docs/测试说明/测试文档模板规范.md` §6.3 同一行并列 `TC-STO-03`(2 位)与 `TC-RTC-001`(3 位);`docs/测试说明/测试文档模板规范.md` §6.2 已废止 `PHY` 模块码,而阶段 ID 至今仍是 `TM-PHY-01` | -| **机器校验只覆盖一个族** | 31 个 pre-commit 钩子中只有 `TC` 有 `scripts/check_test_traceability.py`,且**未挂钩子、warn-only、正则 `TC-[A-Za-z0-9]+-\d+` 匹配不到 `TC-AI-01A`**;`E/N/S/B/C/D/A/U/O/gen/Trap` 全部裸奔 | +| **机器校验只覆盖一个族** | (2026-10-15 现状,2026-10-20 已由 §8.1 收口)当时 31 个 pre-commit 钩子中只有 `TC` 有 `scripts/check_test_traceability.py`,且**未挂钩子、warn-only、正则 `TC-[A-Za-z0-9]+-\d+` 匹配不到大写后缀形态**;`E/N/S/B/C/D/A/U/O/gen/Trap` 全部裸奔 | > **可借鉴的先例**:`§N.M` 章节引用当年 1,578 处也全靠自觉,靠「紧邻写法 + 别名表 + 可判定比例 + 钩子」把可判定比例做到 58% 并转为拦提交([`docs/文档规范.md`](文档规范.md) §3.5)。编号走同一条路径即可收口。 @@ -53,7 +53,7 @@ | **P-2** | **编号不携带语义**:严重度、状态、日期、负责人、波次一律是表格列 | 降级只改列,编号永不变、引用永不断(`P1-x` 的历史教训) | | **P-3** | **一个前缀只有一个属主登记处**;前缀一经分配**永不复用给别类**,号段用尽即扩位(`N-99`→`N-100`),绝不换字母 | 保证"看到前缀就知道去哪张表查" | | **P-4** | **形态唯一**:`<前缀>-<序号>`;前缀 1~3 位大写字母;序号位数按类别固定并左补零;子项只允许一级 `-<小写字母>`。**禁止**裸形(`S3`)、空格形(`Trap 07`)、中文形(`陷阱 1`)、圈号(`①②③④`) | 连字符与补零不得承载语义;字面唯一才能被机器判定 | -| **P-5** | **只增不复用**;每个类别有「下一个可用号」游标(`[计划] scripts/id_registry.json`),新增先取游标 | 并行任务与多文档同时新增时防撞号(《项目审查与优化建议报告》与登记簿并轨时 `N-16` 撞过号,就是没有游标——见 `docs/任务规划/README.md` §0) | +| **P-5** | **只增不复用**;每个类别有「下一个可用号」游标(`scripts/id_registry.json`,§8.1 已落地),新增先取游标 | 并行任务与多文档同时新增时防撞号(《项目审查与优化建议报告》与登记簿并轨时 `N-16` 撞过号,就是没有游标——见 `docs/任务规划/README.md` §0) | | **P-6** | **引用必须机器可判定**:跨文档引用用行内代码,首次出现紧邻属主文档名;文档内编号**必须**带文档名 | 与 `§N.M` 同一条路:能校验的约定才算存在 | | **P-7** | **编号不语义,引用必语义**:编号本体保持无语义并冻结(同 P-2);但**正文引用必须带语义括注**——`TRAP-NET-18(异常释放后排空 RX 残渣)`、`docs/网络/网络自愈架构设计.md` §4.6(应用主动拆会话保监听);**禁止裸 ID 单独承担信息**。语义放括注,事实变了只改括注、编号不变 | 读者不该为看懂一句话去跳三篇文档。可读性由**引用方式**解决,不由**编号命名**解决——命名带语义会重蹈 `P1-x` 覆辙(§1.2)。落地见 §5.6 | | **P-8** | **语义优先**:规划 / 待办类条目**优先用语义标识**(§3.3)——一行短语即标识,不分配字母+数字号;只有**需机器解析**或**作历史证词**的类别才发编号,且引用时强制带语义括注(P-7) | 编号的唯一性维护成本已实测超过收益:`N-31` 销项删行后按"现存最大值 +1"又被取走,与两处历史引用撞号(`docs/任务规划/README.md` §0)。短语自带唯一性(重名一眼可见),且读者不必跳表查号 | @@ -137,10 +137,14 @@ |---|---|---|---| | `FR-[A-Z0-9]+-\d{2}` | 功能需求 | `docs/产品需求/产品需求设计文档.md` §5 | 各域独立递增;**新增域写进该文 §1.2 术语与缩写** | | `NFR-[A-Z0-9]+-\d{2}` | 非功能需求(**新建**,现 §7 全是散文无号) | `docs/产品需求/产品需求设计文档.md` §7 | 从 `NFR-<域>-01` 起 | -| `N-\d{2}` | **登记簿条目**(技术债 / 长期观察 / 硬件改版项;含义由"网络项"重定义) | `docs/任务规划/登记簿.md` | **曾用**最大 `N-33`(`N-30`~`N-33` 均已销项、登记簿行已删,**现存至 `N-29`**)→ 游标 `N-34`(号不回收);原 `S-x`/`T-x` 并入本族(§7.1) | -| `E-\d{2}` | 错误码/返回值传播审计发现 | 无(原属主 `docs/问题记录/错误码传播审计.md` 已于 2026-10-15 删除) | **冻结**:`E-63` 为最大号,**不再新增**;新发现一律 `DEF`;现存引用仅作历史指称(§4.3) | -| `E\d+` | **自愈实验编号**(N34 台架实验,**无连字符**) | `docs/网络/网络自愈架构设计.md` §5.2 | **已封闭**:`E0`~`E15`,**不再新增**;原始实验记录已归档(该文档 §12)。⚠ 与上一行靠**连字符**区分:带连字符 = 审计发现 | -| `TC-[A-Z]{1,4}-\d{3,4}` | 测试用例 | 各测试文档 | 3 位起;分阶段套件序号 = 阶段号×100 + 项号(阶段 ≥10 自然进位 4 位,`TC-NET-1001` 合规);后缀只允许 `-<小写字母>` | +| `N-\d{1,3}[a-z]?` | **登记簿条目**(技术债 / 长期观察 / 硬件改版项;含义由"网络项"重定义) | `docs/任务规划/登记簿.md` | **曾用**最大 `N-33`(`N-30`~`N-33` 均已销项、登记簿行已删,**现存至 `N-29`**)→ 游标 `N-34`(号不回收);原 `S-x`/`T-x` 并入本族(§7.1)。⚠ 实测另有报告侧补零带子项后缀的 `N-03a`/`N-03c` 与 §5.5 的扩位示例 `N-100` → 位数收到 3、允许小写子项后缀 | +| `E-\d{1,2}` | 错误码/返回值传播审计发现 | 无(原属主 `docs/问题记录/错误码传播审计.md` 已于 2026-10-15 删除) | **冻结**:`E-63` 为最大号,**不再新增**;新发现一律 `DEF`;现存引用仅作历史指称(§4.3)。⚠ 属主已删 → 该族**无法判定义存在与否**,门禁只拦越游标(§8.1) | +| `E\d+` | **自愈实验编号**(N34 台架实验,**无连字符**) | `docs/网络/网络自愈架构设计.md` §5.2 | ⚠ 规范原记「已封闭 `E0`~`E15`」,**实测该表已到 `E19`**(E16~E19 是其后追加)→ 游标按实测 `E19` 收录,封闭线待订。⚠ 与上一行靠**连字符**区分:带连字符 = 审计发现 | +| `TC-[A-Z][A-Z0-9]{0,4}-\d{1,4}[a-z]?` | 测试用例 | 各测试文档 | 3 位起;分阶段套件序号 = 阶段号×100 + 项号(阶段 ≥10 自然进位 4 位,`TC-NET-1001` 合规);后缀只允许 `-<小写字母>`。⚠ 模块码 `GD5F`/`I104` **含数字**,规范原写的 `[A-Z]{1,4}` 会整族漏掉 GD5F(实测 285 处)→ 已按实测放宽 | +| `F--` | **net / 驱动侧功能与改进项**(`F-NET-2~17`、`F-DRV-*`、`F-MS-*`) | `docs/网络/网络自愈架构设计.md` | 2026-10-20 补行:本族**早就在被代码注释大量引用**(实测 60+ 处)而总表里没有——正是 §5.1 第 1 步"表里没有的类别先改本规范"的适用对象;`id-check` 落地时按实测收编 | +| `P-` | **本规范的设计原则条目**(`P-1`~`P-8`) | 本文档 §2 | 2026-10-20 补行;⚠ 与档位词 `P0~P3`(§4.4)、审计条目 `P0-x`(§4.3)靠**连字符**区分,三者形态互斥 | +| `CODE-RULE-` | **嵌入式 C 代码规范条款号**(配原文章节号,如 `CODE-RULE-03 (3.4.1)`) | `scripts/check_coding_standard.py`(规则表 = 单一事实源) | 2026-10-20 批 1 新建:原名是裸形 `S1`~`S19`,与登记簿 `S-x`、UI 像素状态、2026-10-02 复审专项条目**四义撞形**(§1.2)→ 单词化改名,旧号进 `id_aliases.json` 的 **scoped** 条目。⚠ `S14` 历史上不存在,空洞不复用 → 游标 19 | +| `MENU-STATE-` | **UI 菜单像素回归状态号**(按键序列状态,共 8 态) | `simulator/tests/check_menu_rows.py` 的 `STATES` 表 | 2026-10-20 批 1 新建:原名裸形 `S0`~`S7`(同上撞形);**冻结在 07**,加状态须先改本族游标 | | `TM-[A-Z]{1,4}-\d{2}` | 测试阶段 | 各测试文档 | 模块码取 §6 统一表(`TM-PHY-01` → `TM-NET-01`) | | `DP-[A-Z]{1,4}-\d{2}` | 设计原则 | 各测试文档 | **加模块码**(现 `DP-01` 跨文档会撞) | | `L\d` / `L\d-\d{1,2}` | 采集与保护分层、层内条目 | `docs/算法/继电保护功能复现规划.md` §3 | 保留(代码侧 141 处引用,是全项目最规范的一族,作样板);`L` 前缀**专属**此族 | @@ -222,7 +226,7 @@ 0. **先问"能不能不加号"**:能用 § 小节引用、表格列、语义标题表达清楚的,就不要分配编号—— 编号是"确需跨文档长期引用"时才付的成本(P-1 / P-3 / P-7)。确需新增才往下走。 1. 查 §4 总表确定类别与属主登记处;**表里没有的类别 → 先改本规范,不得自造前缀**。 -2. 到属主登记处取游标号(`[计划] scripts/id_registry.json` 记录每类下一个可用号),登记处新增一行。 +2. 到属主登记处取游标号(`scripts/id_registry.json` 记每类的游标与冻结状态),登记处新增一行。 3. 严重度 / 状态 / 波次 / 出处写进表格列,**不进编号**。 ### 5.2 引用写法 @@ -273,9 +277,11 @@ 短语**不复用**——重名即视为同一条目,与"号不回收"同理(撞名风险高于撞号,故由门禁兜底)。 - **代码注释**:语义短语可进代码注释(中文注释允许),但必须带属主文档名(同 §5.3); 现状 `App/algo/` 下 `N-18` 等 6+ 处**裸编号引用违反 P-7**,迁移时一并补语义。 -- **门禁**(待落地):① 同一登记处内短语去重;② 引用了登记处不存在的短语 = 悬空,报错; - ③ 打印「语义标识可判定比例」,只许涨不许跌。校验脚本 `[计划] scripts/check_ids.py` - 尚未落地(§8),落地前由人工 + `grep` 执行上述三条。 +- **门禁**(`scripts/check_ids.py`,§8,2026-10-20 已落地 warn-only):① 同一登记处内短语去重; + ② 引用了登记处不存在的短语 = 悬空,报错;③ 打印「语义标识可判定比例」,只许涨不许跌。 + ⚠ **当前落的是"编号"侧的 ②③**(编号族的悬空与可判定比例已机械化);**语义短语**侧的 + 同表去重与短语悬空仍由人工 + `grep` 执行——短语是自由文本,机械去重要先解决同义异写判定 + (`日志静默` vs `49s 日志静默`),那是另一个问题,别把它记成"已由门禁兜住"。 --- @@ -359,7 +365,7 @@ git grep -n '<旧字面量>' # 零残留 ## 8. 门禁:`check_ids.py` 与 `id-check` 钩子 -配置两份:`[计划] scripts/id_registry.json`(前缀 → 属主文档 / 形态正则 / 是否冻结 / 游标)、`[计划] scripts/id_aliases.json`(旧号 → 新号,照搬 `scripts/section_aliases.json` 的成功范式)。 +配置两份:`scripts/id_registry.json`(前缀 → 属主文档 / 形态正则 / 是否冻结 / 游标)、`scripts/id_aliases.json`(旧号 → 新号,照搬 `scripts/section_aliases.json` 的成功范式)。 | # | 校验 | 级别 | |---|---|---| @@ -376,6 +382,58 @@ git grep -n '<旧字面量>' # 零残留 - 上线节奏:**先 warn-only 清 backlog → 清零后加 `--strict` 拦提交**(与 `doc-links` 章节校验完全同一条路径)。 - 顺带收编 `scripts/check_test_traceability.py`:正则补 `TC-[A-Z]{1,4}-\d{3,4}[a-z]?`,覆盖 7 篇「单元测试说明.md」,一并挂进 `id-check`(现为 warn-only 且未挂钩子)。 +### 8.1 落地记录(2026-10-20) + +**已落地并挂上 `id-check` 钩子(warn-only)**。三项机械判据先定死,否则八项校验全是空话: + +| 判据 | 口径 | +|---|---| +| 「定义」存在 | 字面量出现在该族的**属主登记处文档**里(registry 的 `owner_docs` / `owner_docs_glob`)——不依赖表格行首等易漂移形态 | +| 「重复定义」 | 字面量出现在**定义位**(表格首格 / 标题行 / 列表项行首);正文里"提到过"不算定义,否则任何交叉引用都会被误判 | +| 「可判定」 | 字面量落进已登记前缀族**且该族属主登记处已存在**;未登记前缀、属主还是 `[计划]` 的都算不可判定 | + +**首轮基线(本仓库实测,`--update-baseline` 写进 `id_registry.json`,只许涨不许跌)**: + +| 项 | 数值 | +|---|---| +| 扫描文件 | 536 个(`git ls-files` 减去 §8 排除清单) | +| 编号字面量出现 | 8271 处 | +| **可判定比例基线** | **82.6%**(6840 / 8271 = 82.70%,批 1 改名后重打,存进 `id_registry.json` 时**下取整到 0.1pp**——用 `round()` 会让 82.34 进位成 82.3 再反过来把自己判死,实测踩过;比较容差 0.1pp) | +| 校验 1 未登记前缀 | 0 处 | +| 校验 2 形态不合规 | 396 处(**批 1 已从 417 降到 396**:裸形 `S`/`T` 类 71 → 50。剩余大头:真无模块码 `TC-nnn` 181 处 / 5 文件、数字模块码 `TC-104-nnn` 23 处(§6 表里模块码是 `I104`,非数字)、`DP-0n` 22 处、`Task #nn` 33 处、`Trap 空格形`+`陷阱 中文形` 43 处、`gen/round` 36 处、大写后缀 31 处) | +| 校验 3 悬空引用 | 错误 626 / 告警 223(scoped 别名收编旧号后 −16)(错误集中在 `TC-PS-*`/`TC-PE-*`/`TC-AH-*`/`TC-FR-*`/`TC-SOE-*`——**主机侧单测有码无文档**,正是 §7.2 列的"缺文档 = 追溯断链") | +| 校验 4 重复定义 | 0 处(`§4.3` 已声明的同号不同义族对 443 个字面量走 `collides_with` 白名单,不计事故) | +| 校验 5 冻结越游标 | 0 处 | +| 校验 6 未紧邻文档名 | 269 处(报告自有编号 `D-x`/`R-x`/`F-x`/`P0-x` 被裸引,属 §9 阶段 1-③ 待办) | +| 校验 7 孤儿条目 | 88 处(告警) | +| 附带 TC 双向追溯 | 代码侧 556 / 文档侧 290 / 交集 260(差异 326,收编后含主机侧单测一侧,比原板级口径的 33 大一个量级——**原口径漏的正是 7 篇单测说明**) | + +**有牙实测**([示例] 下列字面量是注入用的假编号,不是真实引用):临时注入 `Z-99` / 裸形 `S3` / `TC-NET-9999`(悬空)/ `E-70`(越 `E-63` 游标)/ 裸引 `D-7` / 在第二篇属主文档重复定义 `DP-01` → 六项分别被校验 1/2/3/5/6/4 点名,`--strict` 退出码 1;临时档删除后 `git status` 干净。 + +**显式盲区(不是"检查通过",是记在案上的判定边界)**:裸形同字母多义(§1.2 的 P1 五义 / L1 四义 / U1 四义 / D7 三义)与元器件位号同形(§4.4)→ 裸形族**只在属主登记处文档内**判越游标,文档外降为告警;圈号编号与正常列举无法机械区分,不入判定。这两块的收口属 §9 阶段 1-②/1-③ 的迁移批次,收口后再扩判定范围。 + +### 8.2 落地时发现的规范 ↔ 仓库偏差(registry 按实测收录,规范行待订正) + +| 规范原文 | 实测 | 处置 | +|---|---|---| +| §4.1 `E\d+`「已封闭 `E0`~`E15`」 | `docs/网络/网络自愈架构设计.md` §5.2 实验表已到 **E19** | 游标按实测 E19 收录;封闭线待修订 | +| §4.1 `FR-[A-Z0-9]+-\d{2}` | 现存 `FR-STO-1` 等全是**未补零**的 1 位序号 | 正则放宽到 `\d{1,2}`,补零归 §9 阶段 1 | +| §4.1 `TC-[A-Z]{1,4}-\d{3,4}` | 模块码 `GD5F`/`I104` **含数字**,按原文正则会漏掉 GD5F 全族(实测 285 处) | 放宽为 `[A-Z][A-Z0-9]{0,4}`;序号暂收 2 位(`TC-STO-06` 属 §7.2 孤儿) | +| §4.1/§4.2 总表无 `F--` | `F-NET-2~17`/`F-DRV-*`/`F-MS-*` 定义在 `docs/网络/网络自愈架构设计.md` 且被大量代码注释引用 | 先按实测收编成族(§5.1 第 1 步:表里没有的类别先改本规范) | +| §4.1 `N-\d{2}` | 报告用 `N-03a`/`N-03c` 子项后缀;§5.5 扩位示例 `N-100` | 放宽到 `\d{1,3}[a-z]?` | + + +--- + +### 8.3 迁移批次记录(改名走 `scripts/rename_ids.py`) + +| 批 | 日期 | 族 | 旧 → 新 | 处数 / 文件 | 别名 | +|---|---|---|---|---|---| +| **批 1** | 2026-10-20 | 代码规范条款 | `S1`~`S19` → `CODE-RULE-01`~`19` | 78 处 / 2 文件 | 18 条(scoped:`scripts/check_coding_standard.py`、`docs/代码规范/pre-commit使用说明.md`) | +| **批 1** | 2026-10-20 | UI 像素回归状态 | `S0`~`S7` → `MENU-STATE-00`~`07` | 61 处 / 3 文件 | 8 条(scoped:`simulator/tests/check_menu_rows.py`、`docs/UI设计/UI_自动化测试流程.md`、`simulator/README.md`) | + +⚠ **别名必须带 scope**:同一裸形字面量可同时是多族的旧号(实测 `S3` 三义)。全局别名会把另一族的真悬空洗成「已解析」——等于用工具复制 §1.2 的 `N-31`/`N-32` 撞号事故。 + --- ## 9. 落地阶段与验收 @@ -387,7 +445,7 @@ git grep -n '<旧字面量>' # 零残留 | **1-① 小族** | `RC`/`PAT`/`GEN`/`RUN`/`MS`/`GAP`/`SC`/`OQ`/`RSK`/`DR`/`TRAP`/`ST`/`UL`/`LAT`/`CS` + Mermaid 节点 id + 孤儿处置 | 每批:dry-run 通读 → 落盘 → `git grep` 零残留 → `check_doc_links --all` → `@build` 0/0 → `ci_local.py` | 中 | | **1-② 大族** | `N` 补零 + `S`/`T` 并入;`WI` ← `B`/`C`/`D`/`W1-0`/`S1x`/`A-x`/`R-x`/`F-x`/`U-x`;测试分层 `L0~L3` → `T0~T3`(**最大机械批次**,`L0.5` 一词全库百余处) | 同上,逐族一个 commit | **高** | | **1-③ 缺陷族** | `DEF` ← `P0-x`~`P3-x` 在册项;报告历史编号补齐紧邻文档名;档位词规则落地;别名表与游标定稿 | 同上 | 中 | -| **2 门禁** | `check_ids.py` + `id-check` 钩子(warn → strict);`check_test_traceability.py` 收编 | 全库告警清零后转 strict;可判定比例基线入档 | 中(首轮会暴露大量真悬空引用) | +| **2 门禁** | ✅ **已落地(2026-10-20,warn-only)**:`scripts/check_ids.py` + `id-check` 钩子 + `scripts/id_registry.json` / `scripts/id_aliases.json`;`check_test_traceability.py` 已收编(正则补齐 + 纳入 7 篇「单元测试说明.md」与 `test/host/` 一侧) | 全库告警清零后 entry 加 `--strict` 转拦提交;可判定比例基线 **82.6%** 已入档(批 1 改名后由 82.3% 抬升)(§8.1,写在 `id_registry.json` 里,跌破即失败) | 中(首轮确实暴露出 626 处真悬空 + 416 处形态违规 + 269 处未紧邻文档名,见 §8.1) | 每阶段结束停下等指令再提交(`AGENTS.md` 边界:commit/push 须明确指示)。 @@ -412,4 +470,6 @@ git grep -n '<旧字面量>' # 零残留 |---|---|---| | 2026-10-14 | 新增 P-7 与引用可读性规则 | §2 加 **P-7「编号不语义,引用必语义」**(编号本体保持无语义并冻结,但正文引用必须带语义括注、禁裸 ID 独担信息)→ 六条原则变**七条**;§5.1 加前置步"先问能不能不加号";新增 **§5.6「编号密集文档的对照节(强制)」**(≥3 个编号族必须加「导读:编号与术语」);§4.1 补登记 **`E\d+`**(N34 自愈实验编号族,已封闭)并把 `N-\d{2}` 的在册范围订正为「曾用最大 `N-33`,现存至 `N-29`」;§1.2 悬空引用示例补 `N-32`;§7.1 同步订正。起因:`docs/网络/网络自愈架构设计.md` 合并与导读节实践暴露"裸编号引用不可读",用户裁定以后尽量用语义标识 | | (规划稿) | 建立本文档 | 全库扫描 1,385 个不同编号 / 19,306 处出现后定稿的两层编号体系(全局编号 + 文档内编号)、六条原则、目标前缀总表、三批迁移映射与 `check_ids.py` 门禁设计;用户决策:`E-xx` 冻结保留、严重度档位词 `P0~P3` 不变、文档落 `docs/编号规范.md`、阶段 1 三批全做 | +| 2026-10-20 | **编号改名开禁(用户授权)+ 批 1 落地** | 新增 `scripts/rename_ids.py`(§7.3 规划脚本:映射表驱动、显式 scope、干跑/落盘两态、自动写 `id_aliases.json` 的 scoped 条目、跳过 `[示例]`/`[计划]` 行与变更记录表、按原字节读写保 BOM 与行尾)。**批 1**:代码规范条款 `S1~S19` → `CODE-RULE-nn`(78 处/2 文件)、UI 像素回归状态 `S0~S7` → `MENU-STATE-nn`(61 处/3 文件);`check_ids.py` 支持 **scoped 别名** 并登记两族(66 族);可判定比例 82.3% → **82.7%**、裸形判错 71 → 50、错误级 1312 → 1291。§4.1 补两行、新增 §8.3 迁移批次记录 | +| 2026-10-20 | **§8 门禁落地(warn-only)** | 新增 `scripts/check_ids.py`(八项校验 + 可判定比例基线)、`scripts/id_registry.json`(逐项来自 §4 总表 + 实测游标)、`scripts/id_aliases.json`;挂上 `id-check` 钩子(36→37);顺带收编 `check_test_traceability.py`(正则补齐大写后缀/无模块码形态,比对范围扩到 7 篇「单元测试说明.md」+ `test/host/` 一侧,另加 `--board-only` 回退原口径)。**新增 §8.1 落地记录**(基线 82.3%、backlog 分布、有牙实测、显式盲区)与 **§8.2 规范↔仓库偏差表**(`E19`、`FR` 未补零、`TC` 模块码含数字、`F--` 总表缺行、`N` 子项后缀/扩位);§4.1 相应行按实测订正并补 `F-` / `P-` 两行 | | 2026-10-15 | 新增 P-8 与语义标识规范(§3.3 / §5.7) | §2 加 **P-8「语义优先」**(规划 / 待办类条目优先用语义标识,不发字母+数字号)→ 七条原则变**八条**;§3 主轴由"两层编号"扩为**编号 / 语义标识 / 章节引用**三种;新增 **§3.3「语义标识(新条目默认形态)」**(形态规则、适用分界、引用写法)与 **§5.7「语义标识的登记、冻结与别名」**;§4.2 加修订说明(规划类前缀改语义标识,`TC`/`TRAP`/`FR`/`NFR`/`RC` 等保留编号);§9 加「0.5 语义标识」阶段。起因:用户裁定去除字母数字编号(难理解),先定规范本体 | diff --git a/scripts/README.md b/scripts/README.md index 6a6a7986..c10f4668 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -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)。 diff --git a/scripts/check_coding_standard.py b/scripts/check_coding_standard.py index c3eacd1c..62b6b415 100644 --- a/scripts/check_coding_standard.py +++ b/scripts/check_coding_standard.py @@ -5,24 +5,24 @@ C 语言代码规范检查(《嵌入式C语言代码规范_V1.0》可机械化 由 pre-commit 调用:python scripts/check_coding_standard.py ,发现违规退出码 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+(?`、`#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 diff --git a/scripts/check_ids.py b/scripts/check_ids.py new file mode 100644 index 00000000..fcc7ae06 --- /dev/null +++ b/scripts/check_ids.py @@ -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"(? 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()) diff --git a/scripts/check_test_traceability.py b/scripts/check_test_traceability.py index f75b8c91..0b81f457 100644 --- a/scripts/check_test_traceability.py +++ b/scripts/check_test_traceability.py @@ -43,8 +43,14 @@ REPO_ROOT = Path(__file__).resolve().parent.parent CODE_DIR = REPO_ROOT / "test" / "board" DOC_DIR = REPO_ROOT / "docs" / "测试说明" -# TC 编号:TC--。 允许字母数字(NET/GD5F/STO/RTC…) -TC_RE = re.compile(r"TC-[A-Za-z0-9]+-\d+") +# TC 编号:TC--。 允许字母数字(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) diff --git a/scripts/id_aliases.json b/scripts/id_aliases.json new file mode 100644 index 00000000..aa2c5abe --- /dev/null +++ b/scripts/id_aliases.json @@ -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": {} +} diff --git a/scripts/id_registry.json b/scripts/id_registry.json new file mode 100644 index 00000000..c2f98943 --- /dev/null +++ b/scripts/id_registry.json @@ -0,0 +1,1392 @@ +{ + "schema": 1, + "generated_from": "docs/编号规范.md §4 编号空间总表(§4.1 在册 / §4.2 目标 / §4.3 冻结 / §4.4 划界)+ §6 模块码表 + §7 迁移映射。游标取「规范记载值」与「全库实测最大号」的较大者,实测偏差一律写进 cursor_note,不凭印象编。", + "decidable_ratio_baseline": 82.6, + "spec_docs": [ + "docs/编号规范.md" + ], + "bare_check_letters": [ + "S", + "T", + "N", + "W", + "E", + "M", + "B", + "C", + "D", + "U", + "A", + "O", + "G", + "V", + "R", + "P" + ], + "bare_forbidden": { + "S": "登记簿 S-x(§4.1,§7.1 并入 N 族):连字符形才是规范形态", + "T": "登记簿 T-x(§4.1,§7.1 并入 N 族):连字符形才是规范形态", + "N": "登记簿 N-x(§4.1 在册保留):连字符形才是规范形态" + }, + "excluded_paths": [ + "Lib/", + "Middlewares/", + "Drivers/CMSIS/", + "Drivers/STM32F4xx_HAL_Driver/", + "docs/芯片手册/", + "docs/硬件平台/", + "docs/内存管理/data/", + "docs/images/", + "MDK-ARM/RTE/", + "simulator/SDL2/", + "simulator/build/", + "simulator/tests/ref/", + ".venv/", + ".git/", + "__pycache__/", + ".workbuddy-ai/", + ".vscode/", + ".gitignore", + ".gitattributes", + ".editorconfig", + "requirements.txt", + "ci-artifacts/", + "test/board/third_party/", + "scripts/id_registry.json", + "scripts/id_aliases.json", + "scripts/check_ids.py", + ".dsh/skills/*/scripts/state/", + ".dsh/n31_neg_inject_gen217.log", + ".dsh/n31_neg_inject_gen221_form5.log" + ], + "excluded_suffixes": [ + ".svg", + ".png", + ".jpg", + ".jpeg", + ".gif", + ".bin", + ".map", + ".htm", + ".html", + ".tsv", + ".log", + ".o", + ".axf", + ".hex", + ".ico", + ".ttf", + ".woff", + ".pyc", + ".sct", + ".lin", + ".s", + ".bat", + ".ps1" + ], + "noise_literals": [ + "^UTF-[0-9]+$", + "^RS-[0-9]+$", + "^SHA-[0-9]+$", + "^MD5-[0-9]+$", + "^AES-[0-9]+$", + "^CRC-[0-9]+$", + "^SOIC-[0-9A-Z]+$", + "^SOP-[0-9A-Z]+$", + "^TSSOP-[0-9A-Z]+$", + "^MSOP-[0-9A-Z]+$", + "^QFN-[0-9A-Z]+$", + "^LQFP-[0-9A-Z]+$", + "^TQFP-[0-9A-Z]+$", + "^SOT-[0-9A-Z]+$", + "^WSON-[0-9A-Z]+$", + "^DIP-[0-9A-Z]+$", + "^SMD-[0-9A-Z]+$", + "^MAX-[0-9]+$", + "^MIN-[0-9]+$", + "^LEN-[0-9]+$", + "^SIZE-[0-9]+$", + "^DMA-[0-9]+$", + "^IRQ-[0-9]+$", + "^GPIO-[0-9]+$", + "^USART-[0-9]+$", + "^UART-[0-9]+$", + "^SPI-[0-9]+$", + "^I2C-[0-9]+$", + "^CAN-[0-9]+$", + "^DBG-[0-9]+$", + "^ERR-[0-9]+$", + "^TCP-[0-9]+$", + "^UDP-[0-9]+$", + "^IP-[0-9]+$", + "^PC-[0-9]+$", + "^ST-[0-9]+$", + "^ADC-[0-9]+$", + "^PWM-[0-9]+$", + "^DAC-[0-9]+$", + "^R[0-9]+-[0-9]+$", + "^L[0-9]{1,4}-[0-9]{1,4}$", + "^SDL[0-9]-[0-9]", + "^XH-[0-9A-Z]+$", + "^Y[0-9]-[0-9]+$", + "^A-Z0-9$", + "^X-[0-9]+$", + "^V[0-9A-Z]{2,}-[0-9]+$" + ], + "malformed": [ + { + "name": "空格形 Trap", + "pattern": "\\b[Tt][Rr][Aa][Pp] ?[0-9]{1,2}\\b", + "severity": "error", + "why": "空格形陷阱编号(§2 P-4 禁止),须写 TRAP--(§4.2 / §7.1)" + }, + { + "name": "中文形 陷阱", + "pattern": "陷阱 ?[0-9]{1,2}", + "severity": "error", + "why": "中文形陷阱编号(§2 P-4 禁止),须写 TRAP--(§4.2 / §7.1)" + }, + { + "name": "小写 gen 代次", + "pattern": "\\bgen[ -]?[0-9]{2,3}\\b", + "severity": "error", + "why": "台架代次字面不统一(§7.1),须写 GEN-(§4.2)" + }, + { + "name": "小写 round 轮次", + "pattern": "\\bround ?[0-9]{1,2}\\b", + "severity": "error", + "docs_only": true, + "why": "台架轮次字面(§7.1),须写 RUN--(§4.2);代码里的 round 变量名不受检" + }, + { + "name": "会话临时号 Task #N", + "pattern": "\\bTask ?#[0-9]+\\b", + "severity": "error", + "why": "会话临时号禁止入册(§4.3),改写为可解析引用或就地展开成事实描述(§7.2)" + } + ], + "blind_spots": [ + "裸形同字母多义(§1.2:P1 五义 / L1 四义 / U1 四义 / D7 三义 / O4 四义):裸形只在**属主登记处文档内**判悬空与游标,文档外降为告警——机械全库判定必然一边倒误报,收口属 §9 阶段 1-② 的 L0~L3→T0~T3 与 WI 迁移批次。", + "圈号编号(①②③…):与正常列举无法机械区分,不入判定;§7.1 的 RC-01~05 迁移批次落地后再收。", + "TC 用例的「文档侧/代码侧」双向一一对应由 check_test_traceability.py 判(本门禁只判形态与定义存在性),两脚本同挂 id-check 钩子。" + ], + "families": [ + { + "name": "FR", + "class": "FR", + "title": "功能需求(§4.1 在册保留)", + "pattern": "\\bFR-[A-Z0-9]{1,6}-[0-9]{1,2}\\b", + "owner_docs": [ + "docs/产品需求/产品需求设计文档.md" + ], + "scope": "global", + "frozen": false, + "cursor": null, + "check_dangling": false, + "cursor_note": "各域独立递增(§4.1);实测现存 FR-<域>-1..5(未补零),规范 §4.1 写的是 \\d{2} 与现状不符 → 按现状收录,补零属 §9 阶段 1 未落地项", + "dangling_note": "需求正文以散文描述、未逐条建定义行 → 悬空校验待 §4.1 需求表补号后启用" + }, + { + "name": "NFR", + "class": "NFR", + "title": "非功能需求(§4.1 新建,现 §7 全是散文无号)", + "pattern": "\\bNFR-[A-Z0-9]{1,6}-[0-9]{1,2}\\b", + "owner_docs": [ + "docs/产品需求/产品需求设计文档.md" + ], + "scope": "global", + "frozen": false, + "cursor": null, + "cursor_note": "从 NFR-<域>-01 起(§4.1)", + "check_dangling": false + }, + { + "name": "N", + "class": "N", + "title": "登记簿条目(§4.1 在册保留;S-x/T-x 按 §7.1 并入本族)", + "pattern": "\\bN-[0-9]{1,3}[a-z]?\\b", + "owner_docs": [ + "docs/任务规划/登记簿.md" + ], + "scope": "global", + "frozen": false, + "cursor": "N-34", + "check_dangling": true, + "cursor_note": "规范 §4.1 记游标 N-34;实测登记簿现存至 N-30(N-31 曾于 2026-10-14 被复用撞号,见 docs/任务规划/README.md §0)→ 新条目一律改语义标识(P-8),实需编号才取 N-34", + "adjacent_cues": [ + "登记簿" + ] + }, + { + "name": "S-LDG", + "class": "N", + "title": "登记簿 存储/日志管线 条目(§7.1 续号并入 N,存量冻结)", + "pattern": "\\bS-[0-9]{1,2}\\b", + "owner_docs": [ + "docs/任务规划/登记簿.md" + ], + "scope": "global", + "frozen": true, + "cursor": "S-5", + "check_dangling": true, + "cursor_note": "现存 S-3/S-4/S-5;§7.1 之后不再新增 S 号", + "adjacent_cues": [ + "登记簿" + ] + }, + { + "name": "T-LDG", + "class": "N", + "title": "登记簿 测试与工具链 条目(§7.1 续号并入 N,存量冻结)", + "pattern": "\\bT-[0-9]{1,2}\\b", + "owner_docs": [ + "docs/任务规划/登记簿.md", + "docs/任务规划/测试与架构专项.md" + ], + "scope": "global", + "frozen": true, + "cursor": "T-7", + "check_dangling": true, + "cursor_note": "现存 T-7(复审 T-1~T-5 见 docs/任务规划/测试与架构专项.md 卷首)", + "adjacent_cues": [ + "登记簿", + "专项" + ] + }, + { + "name": "E-AUD", + "class": "E-AUD", + "title": "错误码传播审计发现(§4.1 / §4.3 冻结于 E-63)", + "pattern": "\\bE-[0-9]{1,2}\\b", + "owner_docs": [], + "owner_note": "属主文档 docs/问题记录/错误码传播审计.md 已于 2026-10-15 删除 → 无法判定义存在与否,属主登记处不存在 = 不可判定", + "scope": "global", + "frozen": true, + "cursor": "E-63", + "check_dangling": false, + "cursor_note": "禁止新增(§4.3);新发现一律语义标识(§3.3)", + "dangling_note": "§4.3「引用照旧」:门禁只拦游标之外的新号" + }, + { + "name": "E-EXP", + "class": "E-EXP", + "title": "网络自愈台架实验编号(§4.1 已封闭,无连字符)", + "pattern": "\\bE[0-9]{1,2}\\b", + "owner_docs": [ + "docs/网络/网络自愈架构设计.md" + ], + "scope": "global", + "frozen": true, + "cursor": "E19", + "cursor_owner_only": true, + "check_dangling": true, + "dangling_outside_owner": "warn", + "cursor_note": "规范 §4.1 记「已封闭 E0~E15」,实测 网络自愈架构设计.md §5.2 实验表已到 E19 → 游标按实测 E19 收录,封闭线待规范修订", + "adjacent_cues": [ + "自愈", + "实验" + ] + }, + { + "name": "I-ORPHAN", + "class": "I", + "title": "孤儿编号 I-1/I-2(§7.2:属主已并入 网络自愈架构设计.md §9.3)", + "pattern": "\\bI-[12]\\b", + "owner_docs": [ + "docs/网络/网络自愈架构设计.md" + ], + "scope": "global", + "frozen": true, + "cursor": null, + "check_dangling": false, + "malformed": "§7.2 孤儿编号:原属主《网络Socket监听生命周期设计说明》已并入 docs/网络/网络自愈架构设计.md §9.3,不再补编号定义行 → 就地展开成事实描述", + "note": "登记为族是为了让校验 1 不重复报它(它已由 §7.2 定性),同时保留形态整改提示" + }, + { + "name": "WI", + "class": "WI", + "title": "工作项 / 任务卡(§4.2 规划类 → 语义标识;本族是迁入目标)", + "pattern": "\\bWI-[0-9]{1,3}\\b", + "owner_docs": [], + "owner_note": "§3.3 / P-8:规划类新条目改语义标识 → 属主登记处 = docs/任务规划/ 各文件的语义短语,不建号表", + "scope": "global", + "frozen": true, + "cursor": null, + "check_dangling": false, + "dangling_note": "登记为族只为让规范里出现的 WI-001 等迁入目标形态可判定,并拒绝「新增 WI 号」的误用" + }, + { + "name": "DR", + "class": "DR", + "title": "决策记录(§4.2 规划类 → 语义标识;§7.1 D1~D10 → DR-001~010)", + "pattern": "\\bDR-[0-9]{1,3}\\b", + "owner_docs": [], + "owner_note": "[计划] docs/架构设计/决策记录登记簿.md 未创建,且 P-8 已把决策类改判语义标识", + "scope": "global", + "frozen": true, + "cursor": null, + "check_dangling": false + }, + { + "name": "OQ", + "class": "OQ", + "title": "待确认 / 开放问题(§4.2 规划类 → 语义标识)", + "pattern": "\\bOQ-[0-9]{1,3}\\b", + "owner_docs": [ + "docs/任务规划/基线与待确认.md" + ], + "scope": "global", + "frozen": true, + "cursor": null, + "check_dangling": false + }, + { + "name": "MS-TGT", + "class": "M", + "title": "里程碑目标形态 MS-(§4.2;存量 M1~M3)", + "pattern": "\\bMS-[0-9]{1,2}\\b", + "owner_docs": [ + "docs/任务规划/W1-采集保护收尾.md" + ], + "scope": "global", + "frozen": false, + "cursor": null, + "check_dangling": true, + "dangling_note": "§4.2 游标 MS-01;M1~M3 迁 MS-01~03 属 §7.1 未落地批次" + }, + { + "name": "TRAP-NOMOD", + "class": "TRAP", + "title": "陷阱记录(无模块码形态,§7.1 待补模块码)", + "pattern": "\\bTRAP-[0-9]{2}\\b", + "owner_docs_glob": [ + "docs/问题记录/*_Trap_Records.md" + ], + "scope": "global", + "frozen": true, + "cursor": null, + "check_dangling": false, + "malformed": "缺模块码:须写 TRAP--(§4.2 / §6;§7.1「数字不变,补模块码」)" + }, + { + "name": "TC", + "class": "TC", + "title": "测试用例(§4.1 在册保留,机器解析族)", + "pattern": "\\bTC-[A-Z][A-Z0-9]{0,4}-[0-9]{2,4}[a-z]?\\b", + "owner_docs_glob": [ + "docs/测试说明/*_Test_Guide.md", + "docs/测试说明/*单元测试说明.md", + "docs/测试说明/实机测试实施指南.md", + "docs/测试说明/链路延迟测试说明.md", + "docs/测试说明/自动化测试流程.md", + "docs/测试说明/存储维护与容量恢复.md" + ], + "scope": "global", + "frozen": false, + "cursor": null, + "digit_width": [ + 2, + 4 + ], + "check_dangling": true, + "cursor_note": "§4.1 写的模块码正则是 [A-Z]{1,4},实测 GD5F/I104 含数字 → 正则放宽为 [A-Z][A-Z0-9]{0,4}(单一真源在此,规范行待同步);序号 3 位起是 §4.1 目标,现存 2 位形态属 §7.2 孤儿待处置 → digit_width 暂收 2 位,§9 阶段 1-① 收紧到 3", + "adjacent_cues": [ + "测试", + "用例", + "指南", + "说明" + ] + }, + { + "name": "TC-UPPER", + "class": "TC", + "title": "测试用例(大写后缀形态,§7.1 待统一小写)", + "pattern": "\\bTC-[A-Z][A-Z0-9]{0,4}-[0-9]{2,4}[A-Z](?![A-Za-z])\\b", + "owner_docs_glob": [ + "docs/测试说明/*.md" + ], + "scope": "global", + "frozen": true, + "cursor": null, + "check_dangling": false, + "malformed": "大写后缀非法:§4.1 规定后缀只允许 -<小写字母>(§7.1 统一为小写)" + }, + { + "name": "TC-NOMOD", + "class": "TC", + "title": "测试用例(无模块码形态,§7.2 孤儿:缺文档 = 追溯断链)", + "pattern": "\\bTC-[0-9]{2,4}[a-z]?\\b", + "owner_docs_glob": [ + "docs/测试说明/*_Test_Guide.md", + "docs/测试说明/*单元测试说明.md" + ], + "scope": "global", + "frozen": true, + "cursor": null, + "check_dangling": false, + "malformed": "缺模块码:须写 TC--(§4.1 / §6 模块码表;§7.2 列为孤儿待补文档)" + }, + { + "name": "TM", + "class": "TM", + "title": "测试阶段(§4.1 在册保留,模块码取 §6)", + "pattern": "\\bTM-[A-Z][A-Z0-9]{0,4}-[0-9]{2}\\b", + "owner_docs_glob": [ + "docs/测试说明/*.md" + ], + "scope": "global", + "frozen": false, + "cursor": null, + "digit_width": [ + 2, + 2 + ], + "check_dangling": true, + "cursor_note": "TM-PHY-* 属 §6 废止模块码(PHY → NET,别名见 id_aliases.json),历史文档不回改", + "adjacent_cues": [ + "测试" + ] + }, + { + "name": "DP", + "class": "DP", + "title": "测试设计原则(§4.1 目标形态带模块码)", + "pattern": "\\bDP-[A-Z][A-Z0-9]{0,4}-[0-9]{2}\\b", + "owner_docs_glob": [ + "docs/测试说明/*.md" + ], + "scope": "global", + "frozen": false, + "cursor": null, + "digit_width": [ + 2, + 2 + ], + "check_dangling": true, + "adjacent_cues": [ + "测试", + "原则" + ] + }, + { + "name": "DP-LEGACY", + "class": "DP", + "title": "测试设计原则(无模块码存量,§7.1 待加模块码)", + "pattern": "\\bDP-[0-9]{2}\\b", + "owner_docs_glob": [ + "docs/测试说明/*.md" + ], + "scope": "global", + "frozen": true, + "cursor": "DP-04", + "digit_width": [ + 2, + 2 + ], + "check_dangling": false, + "cursor_note": "现存 DP-01~04(CH395F_Test_Guide.md);§7.1 迁 DP-NET-01~04,历史文档不回改", + "malformed": "缺模块码:§4.1 要求 DP--(现 DP-01 跨文档会撞)" + }, + { + "name": "TRAP", + "class": "TRAP", + "title": "陷阱记录(§4.2 保留原号,历史证词高频锚点)", + "pattern": "\\bTRAP-[A-Z][A-Z0-9]{0,4}-[0-9]{2}\\b", + "owner_docs_glob": [ + "docs/问题记录/*_Trap_Records.md" + ], + "scope": "global", + "frozen": false, + "cursor": null, + "digit_width": [ + 2, + 2 + ], + "check_dangling": true, + "cursor_note": "分模块各自递增:NET 01~21、GD5F 01~06、RTC 01~(§4.2);NET 域 2026-10-13 已全库重排连续", + "adjacent_cues": [ + "Trap", + "trap", + "陷阱", + "Trap_Records" + ] + }, + { + "name": "FEAT", + "class": "FEAT", + "title": "网络层功能/驱动改进项 F--(§4 总表缺这一族)", + "pattern": "\\bF-[A-Z][A-Z0-9]{0,3}-[0-9]{1,2}\\b", + "owner_docs": [ + "docs/网络/网络自愈架构设计.md" + ], + "scope": "global", + "frozen": false, + "cursor": null, + "check_dangling": true, + "cursor_note": "实测 F-NET-2~F-NET-17 / F-DRV-* 定义在 docs/网络/网络自愈架构设计.md 的功能对照表并被大量代码注释引用,但 §4.1/§4.2 总表里没有这一族 → 门禁先按实测收编,规范 §4 补行随本批同步提(§5.1 第 1 步:表里没有的类别先改规范)", + "adjacent_cues": [ + "自愈", + "功能", + "改进" + ] + }, + { + "name": "PRINCIPLE", + "class": "PRINCIPLE", + "title": "编号设计原则 P-1~P-8(§2,定义就在 docs/编号规范.md)", + "pattern": "\\bP-[0-9]\\b", + "owner_docs": [ + "docs/编号规范.md" + ], + "scope": "global", + "frozen": false, + "cursor": "P-8", + "check_dangling": true, + "cursor_note": "八条设计原则(§2,2026-10-15 加 P-8);⚠ 与档位词 P0~P3(§4.4)与审计条目 P0-x(P-AUDIT)不同形,靠连字符区分", + "adjacent_cues": [ + "原则", + "§2" + ] + }, + { + "name": "PAT", + "class": "PAT", + "title": "缺陷模式九类(§4.2.1,定义就在 docs/编号规范.md)", + "pattern": "\\bPAT-[0-9]{2}\\b", + "owner_docs": [ + "docs/编号规范.md" + ], + "scope": "global", + "frozen": true, + "cursor": "PAT-09", + "digit_width": [ + 2, + 2 + ], + "check_dangling": true, + "cursor_note": "封闭九类(§4.2.1);新增类别先改规范" + }, + { + "name": "RC", + "class": "RC", + "title": "net 周期对账类型(§4.2,唯一被代码逻辑引用的圈号链)", + "pattern": "\\bRC-[0-9]{2}\\b", + "owner_docs": [ + "docs/网络/网络自愈架构设计.md" + ], + "scope": "global", + "frozen": true, + "cursor": "RC-05", + "digit_width": [ + 2, + 2 + ], + "check_dangling": true, + "cursor_note": "RC-01~RC-05(§4.2)", + "adjacent_cues": [ + "对账" + ] + }, + { + "name": "GEN", + "class": "GEN", + "title": "台架日志代次(§4.2 目标形态,数字不变仅改字面)", + "pattern": "\\bGEN-[0-9]{1,3}\\b", + "owner_docs": [ + "docs/测试说明/串口助手设计说明.md" + ], + "scope": "global", + "frozen": false, + "cursor": null, + "check_dangling": false, + "dangling_note": "代次是运行时计数、不落登记表 → 无定义行可查;小写 gen 形态由 malformed 拦(§4.4 划界:与 net_get_generation() 是两回事)" + }, + { + "name": "RUN", + "class": "RUN", + "title": "台架轮次(§4.2 目标形态,日期制)", + "pattern": "\\bRUN-[0-9]{8}-[0-9]{1,2}\\b", + "owner_docs": [ + "docs/测试说明/实机测试实施指南.md" + ], + "scope": "global", + "frozen": false, + "cursor": null, + "check_dangling": false + }, + { + "name": "WAVE", + "class": "W", + "title": "波次(§4.1 在册保留;裸形 W 合法)", + "pattern": "\\bW[0-9]+\\b", + "owner_docs": [ + "docs/任务规划/README.md" + ], + "scope": "global", + "frozen": false, + "cursor": "W8", + "cursor_owner_only": true, + "check_dangling": true, + "dangling_outside_owner": "warn", + "allow_missing_defs": [ + "W8", + "W0" + ], + "cursor_note": "游标 W8(§4.1);W2 已清空销项但槽位保留不复用(README §2.0 注)", + "adjacent_cues": [ + "波次", + "规划" + ] + }, + { + "name": "WAVE-TASK", + "class": "W", + "title": "波次子任务卡 W-(§4.1 已废止,并入 WI / 语义标识)", + "pattern": "\\bW[0-9]+-[0-9]+\\b", + "owner_docs": [ + "docs/任务规划/README.md" + ], + "scope": "global", + "frozen": true, + "cursor": null, + "check_dangling": false, + "malformed": "子任务卡 W- 形态已废止(§4.1「子任务卡不再用 W1-0 形态,并入 WI」)→ 改语义标识(§3.3)" + }, + { + "name": "L-PROT", + "class": "L", + "title": "采集与保护分层条目(§4.1 在册保留,代码侧样板族)", + "pattern": "\\bL[0-9](?:\\.[0-9])?-[0-9]{1,2}\\b", + "owner_docs": [ + "docs/算法/继电保护功能复现规划.md" + ], + "scope": "global", + "frozen": false, + "cursor": null, + "check_dangling": true, + "cursor_note": "L 前缀专属本族 + 裸形 L(§4.1);裸形 L0/L0.5/L1/L2/L3 同时被测试分层/UI 四层/CubeMX 三层使用(§1.2「L1 四义」)→ 裸形不入判定,见 unmanaged_bare", + "adjacent_cues": [ + "规划", + "保护", + "分层" + ] + }, + { + "name": "B-WAVE", + "class": "B", + "title": "功能主线排期条目 B0~B10(§4.2 目标 → WI;存量冻结)", + "pattern": "\\bB[0-9]{1,2}\\b", + "owner_docs": [ + "docs/任务规划/README.md", + "docs/任务规划/W1-采集保护收尾.md", + "docs/任务规划/排期详述.md" + ], + "scope": "global", + "frozen": true, + "cursor": "B10", + "cursor_owner_only": true, + "check_dangling": false, + "dangling_outside_owner": "warn", + "allow_missing_defs": [ + "B0" + ], + "cursor_note": "§7.1 → WI-001…(阶段 1-② 未落地);与位号 B1/B7、审计快照 B1~B10 同形 → 文档外只告警", + "adjacent_cues": [ + "主线", + "波次" + ], + "dangling_note": "与元器件位号同形(§4.4),全库悬空判定误报面大 → 只在属主登记处文档内拦越游标(cursor_owner_only)" + }, + { + "name": "C-WAVE", + "class": "C", + "title": "结构债排期条目 C1~C6(§4.2 目标 → WI;存量冻结)", + "pattern": "\\bC[1-9]\\b", + "owner_docs": [ + "docs/任务规划/README.md", + "docs/任务规划/排期详述.md" + ], + "scope": "global", + "frozen": true, + "cursor": "C6", + "cursor_owner_only": true, + "check_dangling": false, + "dangling_outside_owner": "warn", + "cursor_note": "与元器件位号 C1~Cn、UI 约束清单 C5~C9 同形(§4.4)→ 只在属主登记处文档内判定", + "adjacent_cues": [ + "结构债", + "波次" + ], + "dangling_note": "与元器件位号同形(§4.4),全库悬空判定误报面大 → 只在属主登记处文档内拦越游标(cursor_owner_only)" + }, + { + "name": "D-WAVE", + "class": "D", + "title": "验证窗口排期条目 D2~D7(§4.2 目标 → WI;存量冻结)", + "pattern": "\\bD[1-9]\\b", + "owner_docs": [ + "docs/任务规划/README.md", + "docs/任务规划/排期详述.md" + ], + "scope": "global", + "frozen": true, + "cursor": "D7", + "cursor_owner_only": true, + "check_dangling": false, + "dangling_outside_owner": "warn", + "cursor_note": "与位号 D7、设计决策 D1~D10、报告自有 D-x 四方同形(§1.2「D7=验证窗口、D-7=文档门禁、D1~D7=设计决策」)→ 只在属主登记处文档内判定", + "adjacent_cues": [ + "验证", + "窗口" + ], + "dangling_note": "与元器件位号同形(§4.4),全库悬空判定误报面大 → 只在属主登记处文档内拦越游标(cursor_owner_only)" + }, + { + "name": "MS", + "class": "M", + "title": "里程碑 M1~M3(§4.2 目标 MS-;存量冻结)", + "pattern": "\\bM[1-3]\\b", + "owner_docs": [ + "docs/任务规划/W1-采集保护收尾.md", + "docs/任务规划/README.md" + ], + "scope": "global", + "frozen": true, + "cursor": "M3", + "cursor_owner_only": true, + "check_dangling": false, + "dangling_outside_owner": "warn", + "cursor_note": "M1~M3(§7.1 → MS-01~03);正则只收 M1~M3——M4 是 Cortex-M4 内核名,不是里程碑", + "dangling_note": "与元器件位号同形(§4.4),全库悬空判定误报面大 → 只在属主登记处文档内拦越游标(cursor_owner_only)" + }, + { + "name": "DEF", + "class": "DEF", + "title": "缺陷 / 问题发现(§4.2 目标形态 DEF-)", + "pattern": "\\bDEF-[0-9]{1,3}\\b", + "owner_docs": [], + "owner_note": "[计划] docs/问题记录/缺陷登记簿.md 尚未创建 → 属主登记处不存在 = 不可判定", + "scope": "global", + "frozen": false, + "cursor": null, + "check_dangling": false, + "dangling_note": "属主登记处创建后启用(§9 阶段 1-③)" + }, + { + "name": "GAP", + "class": "GAP", + "title": "模拟器差距(§4.2;§7.1 收编 H-x / L-x / 差距 N-x)", + "pattern": "\\bGAP-[0-9]{1,3}\\b", + "owner_docs": [ + "docs/测试说明/模拟器差距清单.md" + ], + "scope": "doc-scoped", + "frozen": false, + "cursor": "GAP-17", + "check_dangling": true, + "adjacent_required": true, + "cursor_note": "GAP-01~GAP-17(§7.1 按文档内出现顺序)", + "adjacent_cues": [ + "差距" + ] + }, + { + "name": "SC", + "class": "SC", + "title": "实机验证场景(§4.2;§7.1 收编实机预检 A1~A8、看门狗 V2/V4/V5)", + "pattern": "\\bSC-[0-9]{1,3}\\b", + "owner_docs": [ + "docs/测试说明/实机测试实施指南.md" + ], + "scope": "doc-scoped", + "frozen": false, + "cursor": null, + "check_dangling": true, + "adjacent_required": true, + "adjacent_cues": [ + "实机", + "场景" + ] + }, + { + "name": "RSK", + "class": "RSK", + "title": "风险 RSK-(§4.2 文档内编号:各文档风险表自 RSK-01 起)", + "pattern": "\\bRSK-[0-9]{1,3}\\b", + "owner_docs_glob": [ + "docs/**/*.md" + ], + "scope": "doc-scoped", + "frozen": false, + "cursor": null, + "check_dangling": false, + "dangling_note": "文档内编号:属主 = 引用它的那篇文档自身,全库悬空校验无意义 → 只判紧邻文档名(§5.2)", + "adjacent_required": true + }, + { + "name": "ST", + "class": "ST", + "title": "专项内部实施阶段 ST-(§4.2 文档内编号;存量 P0~P5)", + "pattern": "\\bST-[0-9]{1,2}\\b", + "owner_docs_glob": [ + "docs/**/*.md" + ], + "scope": "doc-scoped", + "frozen": false, + "cursor": null, + "check_dangling": false, + "adjacent_required": true + }, + { + "name": "UL", + "class": "UL", + "title": "UI 标准化四层 UL-(§4.2 文档内编号;存量报告 UI 四层 L0~L3)", + "pattern": "\\bUL-[0-9]\\b", + "owner_docs": [ + "docs/UI设计/UI_设计架构.md" + ], + "scope": "doc-scoped", + "frozen": false, + "cursor": "UL-3", + "check_dangling": true, + "adjacent_required": true, + "adjacent_cues": [ + "UI", + "架构" + ] + }, + { + "name": "LAT", + "class": "LAT", + "title": "链路延迟分段 LAT-(§4.2 文档内编号;存量 L1~L11)", + "pattern": "\\bLAT-[0-9]{1,2}\\b", + "owner_docs": [ + "docs/测试说明/链路延迟测试说明.md" + ], + "scope": "doc-scoped", + "frozen": false, + "cursor": "LAT-11", + "check_dangling": true, + "adjacent_required": true, + "adjacent_cues": [ + "延迟" + ] + }, + { + "name": "CS", + "class": "CS", + "title": "CubeMX 定制存活三层 CS-1~CS-3(§4.2;存量 AGENTS.md 的 L1/L2/L3)", + "pattern": "\\bCS-[0-9]\\b", + "owner_docs": [ + "docs/子系统设计/看门狗设计说明.md" + ], + "scope": "doc-scoped", + "frozen": true, + "cursor": "CS-3", + "check_dangling": true, + "adjacent_required": true, + "cursor_note": "CS-1~CS-3(§4.2)", + "adjacent_cues": [ + "看门狗", + "存活" + ] + }, + { + "name": "N-RPT", + "class": "N-RPT", + "title": "《项目审查与优化建议报告》自有编号 N-xx(§4.3 冻结历史快照)", + "pattern": "\\bN-[0-9]{1,3}[a-z]?\\b", + "owner_docs": [ + "docs/问题记录/项目审查与优化建议报告.md" + ], + "scope": "doc-scoped", + "frozen": true, + "cursor": null, + "check_dangling": true, + "dangling_outside_owner": "warn", + "adjacent_required": true, + "adjacent_cues": [ + "报告", + "审查" + ], + "collides_with": "N", + "cursor_note": "⚠ 与登记簿 N-x 同名不同义(§4.3 / AGENTS.md 模块约定)→ 引用必须紧邻「报告」或报告文档名" + }, + { + "name": "D-RPT", + "class": "D-RPT", + "title": "报告自有编号 D-x(§4.3 冻结历史快照)", + "pattern": "\\bD-[0-9]\\b", + "owner_docs": [ + "docs/问题记录/项目审查与优化建议报告.md" + ], + "scope": "doc-scoped", + "frozen": true, + "cursor": null, + "check_dangling": true, + "dangling_outside_owner": "warn", + "adjacent_required": true, + "adjacent_cues": [ + "报告", + "审查" + ], + "collides_with": "D" + }, + { + "name": "S-RPT", + "class": "S-RPT", + "title": "报告自有编号 S-x(§4.3 冻结历史快照)", + "pattern": "\\bS-[0-9]\\b", + "owner_docs": [ + "docs/问题记录/项目审查与优化建议报告.md" + ], + "scope": "doc-scoped", + "frozen": true, + "cursor": null, + "check_dangling": true, + "adjacent_required": true, + "adjacent_cues": [ + "报告", + "审查" + ], + "collides_with": "N" + }, + { + "name": "R-RPT", + "class": "R-RPT", + "title": "报告自有编号 R-x(§4.3 冻结;内存优化项)", + "pattern": "\\bR-[0-9]{1,2}\\b", + "owner_docs": [ + "docs/问题记录/项目审查与优化建议报告.md" + ], + "scope": "global", + "frozen": true, + "cursor": "R-9", + "check_dangling": true, + "adjacent_required": true, + "adjacent_cues": [ + "报告", + "审查" + ], + "cursor_note": "实测现存至 R-9(2026-10-15 落地);§1.2 记「R1=风险、R-2=内存优化项」同字母歧义" + }, + { + "name": "F-RPT", + "class": "F-RPT", + "title": "报告自有编号 F-x(§4.3 冻结历史快照)", + "pattern": "\\bF-[0-9]{1,2}\\b", + "owner_docs": [ + "docs/问题记录/项目审查与优化建议报告.md" + ], + "scope": "global", + "frozen": true, + "cursor": null, + "check_dangling": true, + "adjacent_required": true, + "adjacent_cues": [ + "报告", + "审查" + ] + }, + { + "name": "A-AUDIT", + "class": "A-AUDIT", + "title": "CH395F_Trap_Records 2026-10-11 审计快照 A1~A6(§4.3 冻结)", + "pattern": "\\bA[1-6]\\b", + "owner_docs": [ + "docs/问题记录/CH395F_Trap_Records.md" + ], + "scope": "doc-scoped", + "frozen": true, + "cursor": "A6", + "cursor_owner_only": true, + "check_dangling": false, + "dangling_note": "与实机预检 A1~A8(§4.2 → SC-nn)同形 → 只在本档内判存在,不做全库悬空", + "adjacent_required": true, + "adjacent_cues": [ + "审计", + "Trap_Records" + ], + "collides_with": "A-REVIEW" + }, + { + "name": "B-AUDIT", + "class": "B-AUDIT", + "title": "CH395F_Trap_Records 2026-10-11 审计快照 B1~B10(§4.3 冻结)", + "pattern": "\\bB10\\b|\\bB[1-9]\\b", + "owner_docs": [ + "docs/问题记录/CH395F_Trap_Records.md" + ], + "scope": "doc-scoped", + "frozen": true, + "cursor": "B10", + "cursor_owner_only": true, + "check_dangling": false, + "dangling_note": "与主线 B0~B10、位号 B1 三方同形(§1.2「B10 在规划是送检窗口、在陷阱审计是孤儿监听」)→ 只在本档内判", + "adjacent_required": true, + "adjacent_cues": [ + "审计", + "Trap_Records" + ], + "collides_with": "B" + }, + { + "name": "S-REVIEW", + "class": "S-REVIEW", + "title": "2026-10-02 复审条目 S1~S14(§7.1 → WI;编号不回收)", + "pattern": "\\bS1[0-4]\\b|\\bS[1-9]\\b", + "owner_docs": [ + "docs/任务规划/测试与架构专项.md" + ], + "scope": "global", + "frozen": true, + "cursor": "S14", + "cursor_owner_only": true, + "check_dangling": true, + "dangling_outside_owner": "warn", + "cursor_note": "「编号不随条目删除而回收」(专项文档卷首)→ 已销项的号再被引用即悬空;⚠ 与登记簿 S-3/S-4/S-5 同号不同义(§1.2),也与 CI 步骤号 S3、代码规范条款号 S3 同形 → 文档外只告警", + "adjacent_cues": [ + "复审", + "专项" + ] + }, + { + "name": "A-REVIEW", + "class": "A-REVIEW", + "title": "2026-10-02 复审条目 A-1~A-7(§7.1 → WI)", + "pattern": "\\bA-[0-9]\\b", + "owner_docs": [ + "docs/任务规划/测试与架构专项.md" + ], + "scope": "global", + "frozen": true, + "cursor": "A-7", + "check_dangling": true, + "cursor_note": "现存 A-4/A-5 行,其余已销项(编号不回收)", + "adjacent_cues": [ + "复审", + "专项" + ] + }, + { + "name": "T-REVIEW", + "class": "T-REVIEW", + "title": "2026-10-02 复审条目 T-1~T-5(专项文档卷首,编号不回收)", + "pattern": "\\bT-[1-5]\\b", + "owner_docs": [ + "docs/任务规划/测试与架构专项.md" + ], + "scope": "global", + "frozen": true, + "cursor": "T-5", + "check_dangling": true, + "adjacent_cues": [ + "复审", + "专项" + ], + "collides_with": "N" + }, + { + "name": "P-AUDIT", + "class": "P", + "title": "2026-09 代码审计缺陷条目 P0-x~P3-x(§4.3 冻结;在册项 §7.1 → DEF)", + "pattern": "\\bP[0-3]-[0-9]{1,2}\\b", + "owner_docs": [ + "docs/任务规划/README.md", + "docs/任务规划/测试与架构专项.md", + "docs/任务规划/登记簿.md", + "docs/任务规划/排期详述.md" + ], + "scope": "global", + "frozen": true, + "cursor": null, + "check_dangling": true, + "dangling_outside_owner": "warn", + "adjacent_required": true, + "adjacent_cues": [ + "审计", + "报告", + "P1 收尾", + "严重度" + ], + "cursor_note": "⚠ 档位词 P0~P3 不带 -<数字> 后缀(§4.4);本族是历史审计条目,形态按 §4.3 冻结沿用,新发现改语义标识(§3.3);报告侧 P-x 属历史基线(§4.3),文档外按告警处理" + }, + { + "name": "O-DOC", + "class": "O", + "title": "各设计文档「本期不做 / 遗留可选项」O1~O4(§3.2 默认不造编号)", + "pattern": "\\bO[1-9]\\b", + "owner_docs_glob": [ + "docs/**/*.md" + ], + "scope": "doc-scoped", + "frozen": true, + "cursor": null, + "check_dangling": false, + "dangling_note": "§3.2:这类清单默认改 § 小节引用;确需逐条引用才用文档内编号 OOS-。登记为族只为防校验 1 误报", + "adjacent_required": true, + "adjacent_cues": [ + "遗留", + "不做", + "可选" + ] + }, + { + "name": "H-GAP", + "class": "H-GAP", + "title": "模拟器差距清单 H-1~H-7(§7.1 → GAP-01~07)", + "pattern": "\\bH-[0-9]\\b", + "owner_docs": [ + "docs/测试说明/模拟器差距清单.md" + ], + "scope": "doc-scoped", + "frozen": true, + "cursor": "H-7", + "check_dangling": true, + "adjacent_required": true, + "adjacent_cues": [ + "差距" + ] + }, + { + "name": "L-GAP", + "class": "L-GAP", + "title": "模拟器差距清单 L-1~L-4(§7.1 → GAP-08~11)", + "pattern": "\\bL-[0-9]\\b", + "owner_docs": [ + "docs/测试说明/模拟器差距清单.md" + ], + "scope": "doc-scoped", + "frozen": true, + "cursor": "L-4", + "check_dangling": true, + "adjacent_required": true, + "adjacent_cues": [ + "差距" + ] + }, + { + "name": "N-GAP", + "class": "N-GAP", + "title": "模拟器差距清单 N-1~N-6(§7.1 → GAP-12~17;与登记簿 N-x 同名不同义)", + "pattern": "\\bN-[0-9]\\b", + "owner_docs": [ + "docs/测试说明/模拟器差距清单.md" + ], + "scope": "doc-scoped", + "frozen": true, + "cursor": "N-6", + "cursor_owner_only": true, + "check_dangling": false, + "dangling_note": "1 位数 N-x 与登记簿同号(§1.2 病根)→ 本族不判悬空,只在属主文档内拦越游标;迁移属 §9 阶段 1-①", + "collides_with": "N" + }, + { + "name": "U-RPT", + "class": "U", + "title": "报告 UI 标准化簇 U1~U5(§4.3 冻结历史快照)", + "pattern": "\\bU[1-5]\\b", + "owner_docs": [ + "docs/问题记录/项目审查与优化建议报告.md", + "docs/任务规划/README.md", + "docs/任务规划/登记簿.md" + ], + "scope": "global", + "frozen": true, + "cursor": "U5", + "cursor_owner_only": true, + "check_dangling": false, + "dangling_note": "与 UI 状态机 U1~U15、位号 U17 三方同形(§4.4 位号目录已排除)→ 只拦越游标", + "cursor_note": "U1~U5 见报告 §6.5;W6 波次按此推进(docs/任务规划/README.md §2.0)" + }, + { + "name": "U-STATE", + "class": "U-STATE", + "title": "Socket 状态机组合 U1~U15(§1.1 各模块设计文档自造;文档内编号)", + "pattern": "\\bU1[0-5]\\b|\\bU[1-9]\\b", + "owner_docs": [ + "docs/网络/网络自愈架构设计.md", + "docs/网络/Net层阻塞模型架构设计.md" + ], + "scope": "doc-scoped", + "frozen": true, + "cursor": "U15", + "cursor_owner_only": true, + "check_dangling": false, + "dangling_note": "裸形与报告 U1~U5、位号 U17 同形(§1.2「U1 四义」)→ 只在属主文档内拦越游标", + "adjacent_required": true, + "adjacent_cues": [ + "状态机", + "组合", + "自愈" + ], + "collides_with": "U" + }, + { + "name": "D-DOC", + "class": "D-DOC", + "title": "网络自愈设计决策 D1~D10(§4.2 目标 DR-;§7.1 D1~D7 → DR-001~007)", + "pattern": "\\bD10\\b|\\bD[1-9]\\b", + "owner_docs": [ + "docs/网络/网络自愈架构设计.md" + ], + "scope": "doc-scoped", + "frozen": true, + "cursor": "D10", + "cursor_owner_only": true, + "check_dangling": false, + "dangling_note": "与验证窗口 D2~D7、位号 D7、报告 D-x 四方同形(§1.2)→ 只在属主文档内拦越游标", + "adjacent_required": true, + "adjacent_cues": [ + "决策", + "自愈" + ], + "collides_with": "D" + }, + { + "name": "V-WD", + "class": "V", + "title": "看门狗验证用例 V2/V4/V5(§7.1 → SC-)", + "pattern": "\\bV[2-9]\\b", + "owner_docs": [ + "docs/子系统设计/看门狗设计说明.md", + "docs/测试说明/实机测试实施指南.md" + ], + "scope": "doc-scoped", + "frozen": true, + "cursor": null, + "cursor_owner_only": true, + "check_dangling": false, + "dangling_note": "与固件版本号 V0.1/V1.0、实机预检项同形 → 只在属主文档内判", + "adjacent_required": true, + "adjacent_cues": [ + "看门狗", + "验证", + "实机" + ] + }, + { + "name": "RISK", + "class": "RISK", + "title": "专项风险裸形 R1~R8(§4.2 目标 RSK-;文档内编号)", + "pattern": "\\bR[1-8]\\b", + "owner_docs": [ + "docs/算法/继电保护功能复现规划.md", + "docs/子系统设计/看门狗设计说明.md" + ], + "scope": "doc-scoped", + "frozen": true, + "cursor": "R8", + "cursor_owner_only": true, + "check_dangling": false, + "dangling_note": "与报告 R-x(内存优化项,连字符形)同字母不同形(§1.2)→ 只在属主文档内拦越游标", + "adjacent_required": true, + "adjacent_cues": [ + "风险", + "规划", + "看门狗" + ] + }, + { + "name": "T-TEST", + "class": "T-TEST", + "title": "测试分层五档 T0/T0.5/T1/T2/T3(§4.2 目标形态;现存写 L0~L3)", + "pattern": "\\bT[0-3](?:\\.5)?\\b", + "owner_docs": [ + "docs/测试说明/测试开发标准流程.md" + ], + "scope": "global", + "frozen": false, + "cursor": null, + "cursor_owner_only": true, + "check_dangling": false, + "dangling_note": "§7.1 迁移批未落地(现存字面是 L0~L3)→ 本族是迁入目标,登记只为让新写法可判定;裸形 T1~T3 与 UART 引脚功能名同形,不做全库判定" + }, + { + "name": "G-GATE", + "class": "G", + "title": "门禁档位 G0~G3(§4.2,与测试分层两套不合并)", + "pattern": "\\bG[0-3](?:\\.5)?\\b", + "owner_docs": [ + "docs/测试说明/测试开发标准流程.md" + ], + "scope": "global", + "frozen": true, + "cursor": null, + "cursor_owner_only": true, + "check_dangling": false + }, + { + "name": "L-LAYER", + "class": "L-LAYER", + "title": "测试分层 / 算法层现存裸形 L0~L3(§4.2 目标 → T0~T3;§1.2 记四义)", + "pattern": "\\bL[0-3](?:\\.5)?\\b", + "owner_docs": [ + "docs/测试说明/测试开发标准流程.md", + "App/algo/README.md" + ], + "scope": "global", + "frozen": true, + "cursor": null, + "check_dangling": false, + "dangling_note": "阶段 1-②(L0~L3 → T0~T3,全库最大机械批次)未落地 → 本族只登记不判定(§9)" + }, + { + "name": "CODE-RULE", + "class": "CODE-RULE", + "title": "嵌入式 C 代码规范条款号(V1.0;配原文章节号,如 CODE-RULE-03 (3.4.1))", + "pattern": "\\bCODE-RULE-[0-9]{2}\\b", + "digit_width": [ + 2, + 2 + ], + "owner_docs": [ + "scripts/check_coding_standard.py" + ], + "scope": "global", + "frozen": false, + "cursor": "CODE-RULE-19", + "cursor_owner_only": true, + "owner_note": "属主登记处 = 门禁脚本的规则表本身(单一事实源);docs/代码规范/pre-commit使用说明.md 与规范原文是引用方", + "cursor_note": "2026-10-20 批 1 由 scripts/rename_ids.py 从裸形 S1~S19 单词化(旧号见 id_aliases.json 的 scoped 条目)。S14 历史上不存在(脚本无该条),空洞不复用 → 游标 19" + }, + { + "name": "MENU-STATE", + "class": "MENU-STATE", + "title": "UI 菜单像素回归状态号(按键序列状态,共 8 态)", + "pattern": "\\bMENU-STATE-[0-9]{2}\\b", + "digit_width": [ + 2, + 2 + ], + "owner_docs": [ + "simulator/tests/check_menu_rows.py" + ], + "scope": "global", + "frozen": true, + "cursor": "MENU-STATE-07", + "cursor_owner_only": true, + "owner_note": "属主登记处 = check_menu_rows.py 的 STATES 表(键序 → 状态);UI_自动化测试流程.md 与 simulator/README.md 是引用方", + "cursor_note": "2026-10-20 批 1 从裸形 S0~S7 单词化(原名与登记簿 S-x、代码规范条款、2026-10-02 复审专项条目四义撞形,见 §1.2)。8 态封顶:加状态先改游标" + } + ], + "unmanaged_bare": { + "note": "以下裸形族在 §9 阶段 1-② 迁移落地前**不做全库判定**——它们彼此、与元器件位号(§4.4)、与档位词大面积同形(§1.2 的五义/四义清单)。这些族的判定范围已收窄到「属主登记处文档内」(cursor_owner_only),文档外降为告警;显式登记 = 记录盲区,不是「检查通过」。", + "patterns": [ + "\\bL[0-9](?:\\.[0-9])?\\b", + "\\bP[0-3]\\b", + "\\bU[0-9]{1,2}\\b", + "\\bH[0-9]\\b", + "\\bI[0-9]\\b", + "\\bJ[0-9]\\b", + "\\bQ[0-9]{1,2}\\b", + "\\bF[0-9]{1,2}\\b", + "\\bR[0-9]\\b", + "\\bV[0-9]\\b" + ] + }, + "owner_docs_exclude": [ + "docs/测试说明/测试文档模板规范.md" + ] +} diff --git a/scripts/rename_ids.py b/scripts/rename_ids.py new file mode 100644 index 00000000..8619b198 --- /dev/null +++ b/scripts/rename_ids.py @@ -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(?P1?[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()) diff --git a/scripts/rename_maps/batch1.json b/scripts/rename_maps/batch1.json new file mode 100644 index 00000000..1d2c378d --- /dev/null +++ b/scripts/rename_maps/batch1.json @@ -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(?P1[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[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)" + } + ] +} diff --git a/simulator/README.md b/simulator/README.md index a82cef02..98d094bc 100644 --- a/simulator/README.md +++ b/simulator/README.md @@ -219,7 +219,7 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI设计/UI_自动化 | 脚本 | 原理 | 失败输出 | |------|------|----------| -| `check_menu_rows.py` | 键序注入(u/d/e/s)跑 8 个菜单状态,断言选中行白带、非选行黑底白字、框体边框、往返状态逐像素一致;S0 池水位须等于脚本内 `MENU_POOL_BASELINE` | 未过断言清单 + 保留临时截图 | +| `check_menu_rows.py` | 键序注入(u/d/e/s)跑 8 个菜单状态,断言选中行白带、非选行黑底白字、框体边框、往返状态逐像素一致;MENU-STATE-00 池水位须等于脚本内 `MENU_POOL_BASELINE` | 未过断言清单 + 保留临时截图 | | `check_content_pages.py` | ① 文本断言(解析字体字形,在截图内精确匹配期望串,不依赖 `ref/`)② 11 内容页无头截图与 `tests/ref/shot_*.bin` **逐字节**比对;另校验开页-关页往返池水位 = `POOL_AFTER_ROUNDTRIP` | 文本未命中项 / 差异像素数 + 双方 ink runs 对照 | | `check_remo_sink.py` | 启动**窗口**模拟器(非无头,`CREATE_NEW_PROCESS_GROUP` 防管道阻塞),经 TCP 7003 发 LCDMEM 取 1bpp 帧;断言首帧=LCDMEM(无 INIT 污染)/位图 3200 字节/非平凡;复用 `i1_to_png` 出目检 PNG;与无头参考(`lvgl_sim --screenshot`)逐位比对 <2% 判 MATCH。守护"INIT 污染 / 快照卡死"两个历史缺陷 | 各判据失败原因 + `sim_err.txt` 尾部 + 保留 PNG(默认不删,`--clean` 才删) | diff --git a/simulator/tests/check_menu_rows.py b/simulator/tests/check_menu_rows.py index 5d14a1e9..cc118137 100644 --- a/simulator/tests/check_menu_rows.py +++ b/simulator/tests/check_menu_rows.py @@ -8,16 +8,16 @@ check_menu_rows.py — 自绘菜单 8 状态像素回归(160x160 I1,MSB-firs 前置:simulator 已构建(build/Debug/lvgl_sim.exe 存在);纯标准库,任意 python3。 状态(键序注入,u=上 d=下 e=Enter s=Esc): - S0 "" 根框 7 行 sel=0 - S1 "d" sel=1 - S2 "dd" sel=2 - S3 "ddu" 回退 → 须与 S1 全像素一致 - S4 "de" 开 L2(实时数据 3 子项),根框不变 - S5 "ded" L2 sel=1 - S6 "des" 返回 → 须与 S1 全像素一致(销毁/重建一致性) - S7 "ddes" 根框 sel=2、L2 已毁 → 须与 S2 全像素一致 + MENU-STATE-00 "" 根框 7 行 sel=0 + MENU-STATE-01 "d" sel=1 + MENU-STATE-02 "dd" sel=2 + MENU-STATE-03 "ddu" 回退 → 须与 MENU-STATE-01 全像素一致 + MENU-STATE-04 "de" 开 L2(实时数据 3 子项),根框不变 + MENU-STATE-05 "ded" L2 sel=1 + MENU-STATE-06 "des" 返回 → 须与 MENU-STATE-01 全像素一致(销毁/重建一致性) + MENU-STATE-07 "ddes" 根框 sel=2、L2 已毁 → 须与 MENU-STATE-02 全像素一致 断言:选中行白带(白占比>0.50)+ 带内黑字;非选行黑底白字(0.03<占比<0.45); - 框体 1px 白边框;一致性状态逐字节相同;S0 池水位 = MENU_POOL_BASELINE。 + 框体 1px 白边框;一致性状态逐字节相同;MENU-STATE-00 池水位 = MENU_POOL_BASELINE。 退出码:0 全 PASS;1 有断言失败;2 运行/环境错误(exe 缺失等)。 """ @@ -33,14 +33,14 @@ from pathlib import Path EXE = Path(__file__).resolve().parents[1] / "build" / "Debug" / "lvgl_sim.exe" STATES = [ - ("S0", ""), - ("S1", "d"), - ("S2", "dd"), - ("S3", "ddu"), - ("S4", "de"), - ("S5", "ded"), - ("S6", "des"), - ("S7", "ddes"), + ("MENU-STATE-00", ""), + ("MENU-STATE-01", "d"), + ("MENU-STATE-02", "dd"), + ("MENU-STATE-03", "ddu"), + ("MENU-STATE-04", "de"), + ("MENU-STATE-05", "ded"), + ("MENU-STATE-06", "des"), + ("MENU-STATE-07", "ddes"), ] W = H = 160 @@ -135,17 +135,17 @@ def main(): r.stderr.decode(errors="replace")[:200])) return 2 paths[name] = out - if name == "S0": + if name == "MENU-STATE-00": s0_out = r.stdout - # S0(空键序)= 菜单基线:池水位门禁 + # MENU-STATE-00(空键序)= 菜单基线:池水位门禁 m = re.search(r"pool after keys: used=(\d+)", s0_out.decode(errors="replace")) if not m: - print("FAIL: S0 输出未找到 'pool after keys'") + print("FAIL: MENU-STATE-00 输出未找到 'pool after keys'") return 2 used = int(m.group(1)) check(used == MENU_POOL_BASELINE, - "S0 池水位 %d != 基线 %d(菜单结构/样式变更?确认合理后更新常量)" + "MENU-STATE-00 池水位 %d != 基线 %d(菜单结构/样式变更?确认合理后更新常量)" % (used, MENU_POOL_BASELINE)) shots = {n: load(p) for n, p in paths.items()} @@ -169,46 +169,46 @@ def main(): check(px[y0][x] == 1 and px[y0 + h - 1][x] == 1, "%s 上下边框 x=%d 非白" % (tag, x)) - # --- S0:根框 7 行,sel=0 --- - p = S["S0"] + # --- MENU-STATE-00:根框 7 行,sel=0 --- + p = S["MENU-STATE-00"] border_check(p, ROOT_X0, ROOT_Y0, ROOT_W, ROOT_H, "S0根框") for i in range(7): row_check(p, *root_row_strip(i), band=(i == 0), tag="S0行%d" % i) - # --- S1/S2:sel 移位 --- - for name, sel in (("S1", 1), ("S2", 2)): + # --- MENU-STATE-01/MENU-STATE-02:sel 移位 --- + for name, sel in (("MENU-STATE-01", 1), ("MENU-STATE-02", 2)): p = S[name] border_check(p, ROOT_X0, ROOT_Y0, ROOT_W, ROOT_H, name + "根框") for i in range(7): row_check(p, *root_row_strip(i), band=(i == sel), tag=name + "行%d" % i) - # --- S4:L2 开于 sel=1 行右侧(3 子项、无箭头),根框区与 S1 相同 --- - p = S["S4"] + # --- MENU-STATE-04:L2 开于 sel=1 行右侧(3 子项、无箭头),根框区与 MENU-STATE-01 相同 --- + p = S["MENU-STATE-04"] lx0, ly0, lw, lh = l2_geom(1, 3) - border_check(p, lx0, ly0, lw, lh, "S4-L2框") + border_check(p, lx0, ly0, lw, lh, "MENU-STATE-04-L2框") for i in range(3): - row_check(p, *l2_row_strip(lx0, ly0, i, lw), band=(i == 0), tag="S4-L2行%d" % i) - # 根框区(x 0..65)须与 S1 逐像素一致 + row_check(p, *l2_row_strip(lx0, ly0, i, lw), band=(i == 0), tag="MENU-STATE-04-L2行%d" % i) + # 根框区(x 0..65)须与 MENU-STATE-01 逐像素一致 for y in range(H): for x in range(0, ROOT_X0 + ROOT_W): - check(S["S1"][y][x] == p[y][x], "S4 根框区(x=%d,y=%d) 与 S1 不一致" % (x, y)) + check(S["MENU-STATE-01"][y][x] == p[y][x], "MENU-STATE-04 根框区(x=%d,y=%d) 与 MENU-STATE-01 不一致" % (x, y)) # L2 框下方区域须全黑(无残留对象) f = frac(p, lx0 + 1, lx0 + lw - 2, ly0 + lh + 1, min(ly0 + lh + 14, H - 1)) - check(f == 0.0, "S4 L2 框下方有像素(占比 %.3f)" % f) + check(f == 0.0, "MENU-STATE-04 L2 框下方有像素(占比 %.3f)" % f) - # --- S5:L2 sel=1,根框区仍与 S1 同 --- - p = S["S5"] + # --- MENU-STATE-05:L2 sel=1,根框区仍与 MENU-STATE-01 同 --- + p = S["MENU-STATE-05"] lx0, ly0, lw, lh = l2_geom(1, 3) for i in range(3): - row_check(p, *l2_row_strip(lx0, ly0, i, lw), band=(i == 1), tag="S5-L2行%d" % i) + row_check(p, *l2_row_strip(lx0, ly0, i, lw), band=(i == 1), tag="MENU-STATE-05-L2行%d" % i) for y in range(H): for x in range(0, ROOT_X0 + ROOT_W): - check(S["S1"][y][x] == p[y][x], "S5 根框区(x=%d,y=%d) 与 S1 不一致" % (x, y)) + check(S["MENU-STATE-01"][y][x] == p[y][x], "MENU-STATE-05 根框区(x=%d,y=%d) 与 MENU-STATE-01 不一致" % (x, y)) # --- 一致性状态:逐字节全像素比对 --- - for a, b, tag in (("S3", "S1", "ddu==d"), - ("S6", "S1", "des==d(往返)"), - ("S7", "S2", "ddes==dd(往返)")): + for a, b, tag in (("MENU-STATE-03", "MENU-STATE-01", "ddu==d"), + ("MENU-STATE-06", "MENU-STATE-01", "des==d(往返)"), + ("MENU-STATE-07", "MENU-STATE-02", "ddes==dd(往返)")): if raw(paths[a]) == raw(paths[b]): print("[%s] PASS 全像素一致 (%s)" % (a, tag)) else: