diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9c65cfd..ca3cb74c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -590,6 +590,36 @@ repos: language: system files: ^(Drivers/BSP/UI/|App/task/ui_task\.(c|h)$|ui_cn_chars\.txt$|tools/font_gen/|scripts/check_font_charset\.py$) pass_filenames: false + # 12a) BOM 漂移门禁(2026-10-25 道E task-20) + # 背景:AGENTS 铁律第 2 条——编辑工具会**静默剥掉**文件原有 BOM,ARMCC 于是按 CP936 + # 解码带中文的源文件,报 #870-D / #8 / #274 一类**假语法错**(报错位置与内容都不指向 + # 真因),而 MSVC(模拟器侧)与既有钩子全照不出来。2026-10-25 一天内道E 踩 4 次、 + # 道B 批 1 踩 1 次,此前全靠人工逐文件比 HEAD 兜底——人工审计只防得住记得做的人。 + # 判定:HEAD 有 EF BB BF 而工作区没有 → FAIL。两条刻意不报(新增文件无 BOM、HEAD 无 + # BOM 而工作区加了 BOM)与一条刻意不做(不自动补:整文件重写正是本仓更大的坑)都 + # 写在脚本头部。files 与 AGENTS 名单、脚本内 BOM_LIST **三处互指**,脚本每次跑核一次, + # 单独改任何一处都会在别人的提交里被拦下——名单散在三处而无人同步等于没有名单。 + - id: bom-guard + name: BOM 漂移检查(带 BOM 文件被剥掉即拦) + entry: python scripts/check_bom_drift.py + language: system + files: ^(Drivers/BSP/UI/|App/task/ui_task\.c$|App/cfg/cfg_file\.c$|Lib/lv_conf\.h$) + + # 12b) 定值查看行表一致性(2026-10-25 道E task-20 追加批;脚本随 task-15 批 3 落) + # 核 Drivers/BSP/UI/ui_w_setview.c 的四张视图表与 App/algo/prot_set 的枚举序、 + # g_prot_set_meta 上下限是否仍是同一份口径:id 恰好覆盖 0..39 各一次、数值行 + # min/max 等于 meta 换算、kind 与 meta.is_ctl 一致、单位纯 ASCII、小数位查表与行表 + # 一致。为什么必须机械化:BSP 侧看不到 App 头,行表里的 id 是裸数字 + 行尾注名, + # 这条对齐在编译期**物理上无法自证**;而页面上只有当前可见的 8 行会被像素断言看到, + # 错位要翻到第 2、3 页才可能被人发现(发现了也已被基线钉死)。 + # ⚠ 跨分支先后:脚本文件随 task-15 分支合入主干;在此之前 files 永不命中 → 钩子自然 + # skip(skip 不是失败),合并后自动生效,不需要任何人再动这条配置。 + - id: setview-check + name: 定值查看行表与 prot_set 枚举/上下限一致性 + entry: python tools/setview_check/check_setview_limits.py + language: system + files: ^(tools/setview_check/|Drivers/BSP/UI/ui_w_setview\.(c|h)$) + pass_filenames: false # 12) UI 像素回归门禁(2026-10-20 加,W6 U4 收尾第三件 / 报告 §6.5 U4、§6.1 golden 行) # 背景:golden 两脚本(check_menu_rows.py / check_content_pages.py)早已存在且一直在 diff --git a/AGENTS.md b/AGENTS.md index 867b2e07..39f42828 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,7 +68,7 @@ ## 铁律:源码编辑与编码 - 改代码**只用 Edit 工具**(精确子串替换 / replaceAll);**绝不用 `Get-Content`/`Set-Content`/`cmd` 重写含中文的文件**——整文件重写按系统默认编码重解码 → `U+FFFD` 乱码且不可逆 -- **Edit 会静默剥掉文件原有的 UTF-8 BOM**:带 BOM 的文件(`Drivers/BSP/UI/*`、`App/task/ui_task.c`、`App/cfg/cfg_file.c`、`Lib/lv_conf.h`)被 Edit 后丢 BOM → ARMCC 改按 CP936 解码 → 中文字面量报 `#870-D: invalid multibyte character sequence`(**只有重编译该文件才暴露,历史"0 警告"不代表安全**)。改完须核对 BOM 并按原样补回(只前缀 3 字节 `EF BB BF`,勿整文件重写) +- **Edit 会静默剥掉文件原有的 UTF-8 BOM**:带 BOM 的文件(`Drivers/BSP/UI/*`、`App/task/ui_task.c`、`App/cfg/cfg_file.c`、`Lib/lv_conf.h`)被 Edit 后丢 BOM → ARMCC 改按 CP936 解码 → 中文字面量报 `#870-D: invalid multibyte character sequence`(**只有重编译该文件才暴露,历史"0 警告"不代表安全**)。改完须核对 BOM 并按原样补回(只前缀 3 字节 `EF BB BF`,勿整文件重写)。**已机械化:`bom-guard` 钩子**(`scripts/check_bom_drift.py`:HEAD 有 BOM 而工作区没有即拦;上面这份名单与钩子 `files` 正则、脚本内 `BOM_LIST` **三处互指**,脚本每次跑都核一次,单独改一处会在别人的提交里被拦下) - **字符串字面量必须纯 ASCII**:无 BOM 文件里奇数个汉字会吞掉结束引号(偶数侥幸通过)→ 中文只写注释 - 怀疑乱码先统计 `EF BF BD` 字节数(勿信终端/Read 渲染);**不要急著 `git checkout`**(会丢弃全部未提交工作)——用 Edit 只修坏行 - 行尾由 `.gitattributes` 写死(`* text=auto eol=lf`;`*.bat`/`*.cmd` 强制 CRLF):**不依赖 `core.autocrlf`、不要把工作区文件改回 CRLF** → 权威口径 `docs/文档规范.md` §2 @@ -90,7 +90,7 @@ ## 提交门禁(pre-commit) -- 任一失败即拦截,共 **46 个钩子**(**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check`(**编号门禁**,`docs/编号规范.md` §8 八项校验;**已关闸为【增量拦截】**:只拦 `scripts/id_known_backlog.json` 台账之外的新错误级问题,存量按指纹豁免、台账只减不增;另有一根恒生效的线——可判定比例跌破 `id_registry.json` 基线即失败)+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 22 个(`-unit-test` 后缀,2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六个)+ `test-coverage`(**覆盖率总闸**:跑全部 39 套并守基线回退;缺 mscoverage 组件时按用户裁定 **FAIL**、不 skip,2026-10-24)+ `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)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 3 条基线只减不增(2026-10-25 记录 reader 注入削掉 UI 记录 3 条边,6→3),报告 S-1)+ `bsp-err`(**BSP 错误码与超时命名约定**:公开 API 不得拿 `HAL_StatusTypeDef`/`bool` 当错误通道、`*_TIMEOUT` 宏不得取负值、非 void 须写「返回值:」、**返回错误码的函数不得裸丢弃返回值**(须接住或显式 `(void)`;实测编译器与 cppcheck 都不报这类,只有这条会响);约定正文 `Drivers/BSP/bsp_err.h`,存量基线只减不增(2026-10-23 首清点 19 条 → 已还 18;`consume-result` 上线又暴露 37 条存量丢弃 → **现 38 条**),报告 S-5) +- 任一失败即拦截,共 **48 个钩子**(新增 `bom-guard` / `setview-check` 两条,2026-10-25 道E task-20,详解篇目待补;**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check`(**编号门禁**,`docs/编号规范.md` §8 八项校验;**已关闸为【增量拦截】**:只拦 `scripts/id_known_backlog.json` 台账之外的新错误级问题,存量按指纹豁免、台账只减不增;另有一根恒生效的线——可判定比例跌破 `id_registry.json` 基线即失败)+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 22 个(`-unit-test` 后缀,2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六个)+ `test-coverage`(**覆盖率总闸**:跑全部 39 套并守基线回退;缺 mscoverage 组件时按用户裁定 **FAIL**、不 skip,2026-10-24)+ `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)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 3 条基线只减不增(2026-10-25 记录 reader 注入削掉 UI 记录 3 条边,6→3),报告 S-1)+ `bsp-err`(**BSP 错误码与超时命名约定**:公开 API 不得拿 `HAL_StatusTypeDef`/`bool` 当错误通道、`*_TIMEOUT` 宏不得取负值、非 void 须写「返回值:」、**返回错误码的函数不得裸丢弃返回值**(须接住或显式 `(void)`;实测编译器与 cppcheck 都不报这类,只有这条会响);约定正文 `Drivers/BSP/bsp_err.h`,存量基线只减不增(2026-10-23 首清点 19 条 → 已还 18;`consume-result` 上线又暴露 37 条存量丢弃 → **现 38 条**),报告 S-5) - **范围 = 项目代码**:第三方(`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/` @@ -173,7 +173,7 @@ | ADC 采样节拍 / 通道映射 / V0.1 硬件缺陷 | `docs/子系统设计/ADC采样节拍设计说明.md`、`docs/硬件平台/ADC通道映射说明.md` | | 存储栈(FatFs / dhara / lftpd / 两条 FatFs 陷阱) | `docs/架构设计/STM32F4-Base存储架构说明.md` | | RAM / 栈预算 / 容量台账 | `docs/内存管理/RAM使用分析与优化方案.md`、`内存内容分配策略.md` | -| **每个提交门禁一篇详解**(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径) | `docs/门禁说明/README.md`(46 个钩子 → 23 篇索引) | +| **每个提交门禁一篇详解**(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径) | `docs/门禁说明/README.md`(48 个钩子 → 23 篇索引;bom-guard / setview-check 两篇待补) | | 实体编号怎么写 / 前缀总表 / `id-check` 八项校验 | `docs/编号规范.md`(§4 总表、§8 门禁与实测基线) | | 提交门禁通用机制(阶段 / stash 陷阱 / 触发面 / 增量拦截三段式) | `docs/代码规范/pre-commit使用说明.md` §2 | | 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` | diff --git a/scripts/check_bom_drift.py b/scripts/check_bom_drift.py new file mode 100644 index 00000000..d402895e --- /dev/null +++ b/scripts/check_bom_drift.py @@ -0,0 +1,292 @@ +# -*- coding: utf-8 -*- +""" +模块名称:BOM 漂移检查(提交门禁 bom-guard 的实现) +模块功能:对给定文件集合(默认 = pre-commit 传进来的本次所改文件)逐个比较 HEAD 版本与工作区 + 版本的前 3 字节:HEAD 有 UTF-8 BOM(EF BB BF)而工作区没有 → FAIL 并列出补救动作。 +为什么要这条:AGENTS 铁律第 2 条记载的实测坑——编辑工具会**静默剥掉**文件原有 BOM,ARMCC + 于是按 CP936 解码带中文的源文件,报出 #870-D / #8 / #274 一类**假语法错**(错的位置 + 和内容都不指向真因);MSVC(模拟器侧)与任何"只跑编译"的门禁都不覆盖这一步,历史 + "0 警告"不代表安全。2026-10-25 一天之内道E 一个任务踩了 4 次、道B 批 1 踩过 1 次, + 此前全靠人工逐文件比 HEAD 兜底——人工审计只有"记得做的人"才会做,本脚本把它变成 + 提交时的硬闸。 +刻意不做(写清楚比补上更容易维持): + ① 不报"HEAD 无 BOM、工作区加了 BOM":那是另一个方向的改动,可能是有意的(把文件 + 纳入 BOM 名单),报红只会逼人 --no-verify,而绕过门禁的习惯比漏检更贵; + ② 不自动补 BOM:正确修复是"在最前面加 3 个字节",用脚本整文件重写反而可能把中文 + 按错误编码重打一遍(本仓记录的更大的坑),所以只报文件与动作; + ③ 不报已删除的文件:删除不是 BOM 漂移,且删除自有 review 兜。 +名单互指(三处必须一致,本脚本每次运行都核一次,不一致即 FAIL): + AGENTS.md 铁律第 2 条列出的带 BOM 文件名单、.pre-commit-config.yaml 里 bom-guard + 钩子的 files 正则、下面的 BOM_LIST。任何一处单独改动都会在别人的提交里被拦下—— + 这正是目的:名单散在三处而无人同步,等于没有名单。 +用法: .venv/Scripts/python.exe scripts/check_bom_drift.py [文件名...] [--all] [--selftest] + 不带文件名也不带 --all 时(pre-commit 的常规形态)只核传进来的文件;--all 核全仓 + 跟踪文件(人工审计/排查用,比 HEAD 全量比一遍慢,但不依赖"改没改过");--selftest + 在临时 git 仓库里自证四条判定(含票面要求的两条边界)。 +退出码: 0 = 通过;1 = 有 BOM 漂移或名单不一致;2 = 用法或环境错误(不在 git 仓库内等) +创建日期:2026-10-25(道E task-20) +""" + +import argparse +import os +import subprocess +import sys +import tempfile + +BOM = bytes((0xEF, 0xBB, 0xBF)) + +# 名单项:(路径, 匹配方式),prefix = 目录内全部文件,eq = 恰好这一个文件 +BOM_LIST = [ + ("Drivers/BSP/UI/", "prefix"), + ("App/task/ui_task.c", "eq"), + ("App/cfg/cfg_file.c", "eq"), + ("Lib/lv_conf.h", "eq"), +] + +AGENTS_FILE = "AGENTS.md" +CONFIG_FILE = ".pre-commit-config.yaml" + +# Windows 控制台默认按 CP936 写 stdout:本脚本要打印中文,先把输出锁到 UTF-8。 +# 不锁的后果不是难看,是**崩在半路**——门禁脚本崩在半路等于一条都没检查(同一课已在 +# tools/setview_check 脚本上记过一次,这里不再重犯)。 +try: + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + sys.stderr.reconfigure(encoding="utf-8", errors="replace") +except Exception: + pass + + +def in_list(path): + """path 是否在带 BOM 名单内(git 与控制台都用正斜杠,不做转换)。""" + for name, kind in BOM_LIST: + if kind == "prefix": + if path.startswith(name): + return True + elif path == name: + return True + return False + + +def git_run(args): + try: + r = subprocess.run(["git"] + args, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL) + except OSError: + return None + if r.returncode != 0: + return None + return r.stdout + + +def head_prefix(path): + """HEAD 版本前 3 字节;不在 HEAD(新增文件)返回 None → 不误报。""" + data = git_run(["show", "HEAD:" + path]) + if data is None: + return None + return data[:3] + + +def work_prefix(path): + """工作区版本前 3 字节;读不到(已删除等)返回 None → 不报。""" + try: + with open(path, "rb") as f: + return f.read(3) + except OSError: + return None + + +def check_paths(paths): + """返回 (违规列表, 核过条数);违规项 = 路径。""" + bad = [] + n = 0 + for p in paths: + if not in_list(p): + continue + hp = head_prefix(p) + if hp is None: + continue # 新增文件:HEAD 无 BOM 可谈,不误报(票面边界①) + n += 1 + if hp != BOM: + continue # HEAD 本来就没 BOM:不报(票面边界②) + wp = work_prefix(p) + if wp is None: + continue # 已删除:不是本门禁的事 + if wp != BOM: + bad.append(p) + return bad, n + + +def tracked_files(): + data = git_run(["ls-files", "-z"]) + if data is None: + return [] + return [x for x in data.decode("utf-8", "replace").split(chr(0)) if x] + + +def list_crosscheck(): + """核 AGENTS 名单 / 钩子 files 正则 / BOM_LIST 三处是否互指一致。""" + problems = [] + names = [n for n, _ in BOM_LIST] + try: + agents = open(AGENTS_FILE, encoding="utf-8").read() + except OSError: + agents = None + problems.append("读不到 " + AGENTS_FILE + "(不在仓库根执行?)") + try: + cfg = open(CONFIG_FILE, encoding="utf-8").read() + except OSError: + cfg = None + problems.append("读不到 " + CONFIG_FILE) + if agents is not None: + seg = "" + for line in agents.split(chr(10)): + if "BOM" in line: + seg += line + for n in names: + key = n.rstrip("/") + if key not in seg: + problems.append("AGENTS.md 的 BOM 条目里没有列到 " + n) + if cfg is not None: + seg = "" + hit = False + for line in cfg.split(chr(10)): + if "id: bom-guard" in line: + hit = True + elif hit and "files:" in line: + seg = line + break + if not seg: + problems.append("在 " + CONFIG_FILE + " 里找不到 bom-guard 钩子的 files 行") + else: + # 正则里的反斜杠与锚点是**写法**,不是名单内容:比之前先脱掉,否则 + # App/task/ui_task\.c$ 会被判成"没覆盖 App/task/ui_task.c"。这条检查要核的是 + # 三处的意图是否一致,不是字节是否相同 + flat = seg.replace(chr(92), "").replace("^", "").replace("$", "") + for n in names: + key = n.rstrip("/") + if key not in flat: + problems.append("bom-guard 的 files 正则没覆盖 " + n) + return problems + + +def report(bad): + print("[FAIL] 以下文件的 BOM 被剥掉了(HEAD 有 EF BB BF,工作区没有):") + for p in bad: + print(" " + p) + print(" 补救:只把 3 个字节 EF BB BF 补回文件最前面;**不要**整文件重写——") + print(" 用 PowerShell/Set-Content 之类整文件重写会按系统默认编码重打中文,") + print(" 那会把可修的丢 BOM 变成不可修的 U+FFFD(AGENTS 铁律第 1/2 条)。") + print(" 为什么必须修:ARMCC 无 BOM 时按 CP936 解码带中文的源文件,报的是 #870-D/#8/#274") + print(" 这类**假语法错**,模拟器(MSVC)0 警告照不出来,只有重编该文件才暴露。") + + +def selftest(): + """在临时 git 仓库里跑四条判定(含票面要求的两条边界),任一不符返回 1。""" + ok = True + tmp = tempfile.mkdtemp(prefix="bomdrift_") + old = os.getcwd() + try: + os.chdir(tmp) + for a in (["init", "-q"], ["config", "user.email", "t@t"], + ["config", "user.name", "t"]): + if git_run(a) is None: + print("[SELFTEST FATAL] 临时仓库 git 初始化失败") + return 1 + os.makedirs("Drivers/BSP/UI") + os.makedirs("App/task") + os.makedirs("App/cfg") + os.makedirs("Lib") + bomf = "Drivers/BSP/UI/t.c" + with open(bomf, "wb") as f: + f.write(BOM + b"/* x */" + chr(0x4e2d).encode("utf-8")) + with open("App/task/ui_task.c", "wb") as f: + f.write(b"/* plain, no BOM in HEAD */") + git_run(["add", "-A"]) + if git_run(["commit", "-q", "-m", "seed"]) is None: + print("[SELFTEST FATAL] 临时仓库提交失败") + return 1 + + bad, n = check_paths([bomf]) + if n != 1 or bad: + print("[SELFTEST FAIL] 未改动的带 BOM 文件被判成违规(应 0 违规、核过 1 个)") + ok = False + + with open(bomf, "rb") as f: + body = f.read() + with open(bomf, "wb") as f: + f.write(body[3:]) + bad, n = check_paths([bomf]) + if bad != [bomf]: + print("[SELFTEST FAIL] 剥掉 BOM 后没报(主用例失守 = 门禁等于没有)") + ok = False + with open(bomf, "wb") as f: + f.write(body) + + # 边界①:HEAD 里根本没有的新文件(哪怕在名单目录内)不得误报 + with open("Drivers/BSP/UI/new.c", "wb") as f: + f.write(b"/* brand new, no BOM */") + bad, n = check_paths(["Drivers/BSP/UI/new.c"]) + if bad or n != 0: + print("[SELFTEST FAIL] 新增无 BOM 文件被误报") + ok = False + os.remove("Drivers/BSP/UI/new.c") + + # 边界②:HEAD 无 BOM、工作区加了 BOM 不得报(另一个方向的改动,可能有意) + with open("App/task/ui_task.c", "wb") as f: + f.write(BOM + b"/* now with BOM */") + bad, n = check_paths(["App/task/ui_task.c"]) + if bad: + print("[SELFTEST FAIL] HEAD 无 BOM 而工作区加了 BOM 被误报") + ok = False + # 名单外文件即使 BOM 丢了也不报(门禁不越权管名单外) + with open("Lib/lv_conf.h", "wb") as f: + f.write(b"x") + bad, n = check_paths(["App/cfg/cfg_file.c"]) + if bad or n != 0: + print("[SELFTEST FAIL] 名单内但不在 HEAD 的文件被误报") + ok = False + bad, n = check_paths(["README.md"]) + if n != 0: + print("[SELFTEST FAIL] 名单外文件参与了检查") + ok = False + finally: + os.chdir(old) + print("[SELFTEST] " + ("4 条判定全过(含两条票面要求的边界)" if ok else "有判定未过")) + return 0 if ok else 1 + + +def main(): + ap = argparse.ArgumentParser(description="BOM 漂移检查(bom-guard 钩子实现)") + ap.add_argument("files", nargs="*", help="待核文件(仓库根相对路径)") + ap.add_argument("--all", action="store_true", help="核 git 跟踪的全部文件") + ap.add_argument("--selftest", action="store_true", help="临时仓库自证判定") + args = ap.parse_args() + + if args.selftest: + return selftest() + + if git_run(["rev-parse", "--git-dir"]) is None: + print("[FATAL] 不在 git 仓库里(或 git 不可用),本检查无法比对 HEAD") + return 2 + + paths = tracked_files() if args.all else args.files + if not paths and not args.all: + return 0 + + bad, n = check_paths(paths) + problems = list_crosscheck() + if bad: + report(bad) + if problems: + print("[FAIL] BOM 名单三处互指不一致(名单散着改 = 名单失效):") + for m in problems: + print(" " + m) + print(" 要一起改的三处:AGENTS.md 铁律第 2 条、.pre-commit-config.yaml 的 bom-guard") + print(" files 正则、scripts/check_bom_drift.py 的 BOM_LIST。") + if not bad and not problems: + print("BOM 漂移检查:核过 %d 个名单内文件,无漂移;名单三处互指一致" % n) + return 1 if (bad or problems) else 0 + + +if __name__ == "__main__": + sys.exit(main())