Files
STM32F4-Base/scripts/check_registration.py
T
wandering1 48bc50e794 feat(遥测快照): 卡8 meas 双区快照模块 + adc_task 200ms 唯一发布点(道D M5)
卡 8(App/algo/meas.*,新增;布局权威 docs/任务规划/数据可信.md §4.5):
- meas_snap_t = §4.5 布局(sizeof 72 B;字段偏移由 TC-MEAS-002 钉死——卡 9/
  UI 按整体拷贝消费,破布局=破协议;谐波 k-bin 扩展(docs/算法/继电保护功能
  复现规划.md L2-8)只许尾部追加,留口注释已写进 meas.h)
- 并发 = phasor 双区 + 单字节索引翻转(L2-7 同型,无锁);seq 只增供判"换帧";
  meas_get 失败路径清零填充(-2 未就绪时调用方可安全读)
- 值口径 = A-lite 裁决(lead 2026-10-25 快裁,推翻票面全链):基波有效值瞬时
  透传,复用 phasor_calc 纯核 → 与 phasor 使用区同拍同值(TC-MEAS-005 逐位
  相等钉死);不做 4 点平均/方和根(§4.5"200 ms 是上送节拍不是平均窗")
- 品质注入:CLIPPED 位图唯一源 = adc_cal_flags()(「数据可信」§4.2 三态表,
  编排方注入、本层不链 adc_cal);取窗不足路 = IV|NT 且整帧 valid=0 但照常
  发布(品质节拍性刷新);TEMP = 最新一点透传 + NT(「数据可信」决策 5);
  MEAS_QDS_* 低字节 = 104 QDS 线上值(本层不 include 协议层头——分层方向,
  卡 9 直接截低字节映射);OV 留位不填(判据待运行参数化,卡 10 域)
- adc_task.c 编排:新帧拍 freq_run() 后按 **tick 经过**分频(非拍计数,idle/
  skip 拍会拉长名义节拍)调 meas_tick()——全仓调用点数 = 1(「数据可信」§8
  风险表"快照双写"行评审判据);meas_init 挂 adc_task_init(不动 app_main.c);
  心跳新增 meas: 行(seq/v/ua·un·ia_mv/f/fv)作 M5 台架观测面
- 死区 2 点确认状态机本卡不建(卡 9 迁移 104 语义时随建,lead 裁③);104 层
  现有 deadband 常量未动;频率点绑定归卡 11
- 实测采样类判据(读数 vs 注入源)= 待台架实测,本卡零台架动作

测试与注册:新套件 test/host/meas(226/226,链真源 meas+phasor+sample_ring
免桩;覆盖 seq/双区无撕裂/布局/品质汇总/回绕/守卫);id_registry 建族
TC-HOST-MEAS(游标 011);meas-unit-test 钩子 + SIM_EXEMPT 一条 + uvprojx
仅增 meas.c 单条挂载(diff 已核零 UV4 抖动);App/algo/README.md §1/§2/
§3.9/§4 同步(设计说明大节与 AGENTS 口径留 lead 收口,同批 1 先例)

gate:@build 0 错 0 警(Code=216352 RO=21476 RW=1232 ZI=132128);RAM 增量
RW+ZI = +152 B(快照双区恰 +144 B = 卡面 DoD;余 s_meas_ms 等编排状态),
IRAM1 57.8%→约 57.9%;Flash(Code+RO+RW)+696 B;adcTask 静态最坏链 1408 B
/ margin 1152 B(45%)PASS(stack_report 实测,无抬栈);BOM/CR/U+FFFD/
字面量 ASCII 逐文件自查通过(未触碰带 BOM
文件)。pre-commit run --all-files 实跑:其余钩子全绿(含 doc-links/id-check/
覆盖率总闸/双端注册/ui-golden/全部主机单测套),仅 mem-budget + mem-numbers
两条红 = 台账受管区 vs .map 漂移(Flash/RAM 台账回填属 lead 单写者区,批 1
同口径):
gate: only mem-budget/mem-numbers pending lead ledger backfill
2026-10-25 10:02:10 +08:00

388 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""check_registration.py - 双端注册反向校验门禁(纯标准库)。
背景(登记簿 N-23,出处审查报告 N-06 残余):
本工程 Keil 工程与 PC 模拟器都用**显式文件列表**(无通配扫描)。新 `.c`
必须手动注册两处才会被编译:
① `MDK-ARM/STM32F407-Demo.uvprojx` 的 `<FilePath>` 条目(MCU 固件构建)
② `simulator/CMakeLists.txt` 的显式源列表(PC 模拟器构建)
历史上 `App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c` **两处都没注册**
= 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效。
本脚本做什么(四类缺口):
扫描 `App/`、`Drivers/BSP/`、`Src/` 下全部 `.c`(排除第三方),对照两份注册表:
① 存在但未进 uvprojx -> 固件不编译(死代码 / 漏注册)
② 存在但未进 CMakeLists -> 模拟器不编译(**仅当该文件本应进模拟器时**才是缺口;
MCU-only 文件不进模拟器是**正确**的,见下方豁免)
③ 注册了但文件不存在(反向)-> 幽灵注册(改名/删除后残留的注册条目)
另对 UI 页面模块(U4 门禁补强,2026-10-15,报告 §6.5):
④ `Drivers/BSP/UI/ui_page_*.c` 定义了 `ui_page_<name>_desc`,但该符号在 UI 目录内
除自身 .c/.h 外无任何引用 -> 页面未接入
`ui_content_page.c` 的 `s_defs[]` 描述符表:两端**编译
链接全过**(desc 无人引用,链接器整段剥离),菜单永远
打不开这一页 = 又一形态的"写了却从不生效"死代码。
①②③ 对此完全照不出(2026-10-15 实测),故补本类;
即"新增 UI 页面必须过 registration-check"的机械口径。
豁免机制(关键):
模拟器只建模 **UI + 网络配置共享核心**;其余模块(MCU 驱动 / 固件任务 / 纯算法 /
CubeMX 生成层)**不进模拟器是设计事实**,模拟器另有 `_sim` 替身或根本无对应。
豁免用**显式清单 + 每行理由注释**(命名模式 `*_mcu.c` + 个别点名),
**不做整目录一刀切**(N-23 教训正是"目录级盲区")。唯一例外是 `Src/`:
CubeMX 生成的外设初始化/中断/main,按构造即 MCU 专属、不可能有模拟器替身,
故整目录豁免(任务口径明确认可"Src/ 全部")。
已知缺口过渡(存量不拦、只对新缺口 FAIL):
`KNOWN_GAPS` 登记存量缺口(N-23 存量,Lead 处置中):命中 -> 打印 `[WARN]` 不拦;
新出现的缺口 -> 打印 `[GAP]` 并 FAIL。`--strict` 关闭白名单(存量也 FAIL),供审计。
退出码:
0 = 一致(无新缺口;存量缺口已白名单 WARN)
1 = 有缺口(存在未白名单的新缺口或幽灵注册)
2 = 脚本环境错误(注册表文件缺失 / 无法读取)
用法(仓库根目录):
& '.venv\\Scripts\\python.exe' scripts/check_registration.py
& '.venv\\Scripts\\python.exe' scripts/check_registration.py --strict
设计依据:docs/任务规划/登记簿.md N-23;.dsh/skills/new-source-file
创建日期:2026-10-12
"""
import argparse
import fnmatch
import os
import re
import sys
# --- 仓库根(scripts/ 的上一级)---
REPO_ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
UVPROJX = os.path.join(REPO_ROOT, "MDK-ARM", "STM32F407-Demo.uvprojx")
CMAKELISTS = os.path.join(REPO_ROOT, "simulator", "CMakeLists.txt")
SIM_DIR = os.path.join(REPO_ROOT, "simulator")
# --- 扫描范围与第三方排除 ---
SCAN_ROOTS = ["App", "Drivers/BSP", "Src"]
# 第三方目录(即便落在扫描根下也跳过)
THIRD_PARTY = [
"Drivers/STM32F4xx_HAL_Driver",
"Drivers/CMSIS",
"Middlewares",
"Lib",
]
# --- 模拟器豁免清单(category ②:不进模拟器是**正确**的)---
# 每项 = (fnmatch 模式(对仓库相对 posix 路径匹配), 理由)。
# 原则:命名模式 + 个别点名,不整目录一刀切;唯一目录级豁免 Src/(CubeMX 生成,MCU 专属)。
SIM_EXEMPT = [
# CubeMX 生成的外设初始化 / 中断向量 / main / freertos:按构造即 MCU 专属,
# 模拟器无对应替身(任务口径明确认可"Src/ 全部")。唯一目录级豁免。
("Src/*", "CubeMX 生成的外设初始化/中断/main,MCU 专属,模拟器无替身"),
# MCU 平台后端:命名约定 *_mcu.c,模拟器另有同名 *_sim.c 替身(在 simulator/sim/)。
("*_mcu.c", "MCU 平台后端,模拟器另有 *_sim.c 替身"),
# --- App/:固件任务 / MCU 依赖 / 纯算法(模拟器无消费路径)---
("App/algo/adc_health.c", "L1 有效性校验纯算法,模拟器无消费路径"),
("App/algo/adc_input.c", "依赖 HAL/TPAFE 取数,MCU 专属"),
("App/algo/phasor.c", "L2 相量纯算法,模拟器无消费路径"),
("App/algo/prot_elm.c", "L3 保护元件纯算法,模拟器无消费路径"),
("App/algo/prot_set.c", "L5 保护定值纯算法,模拟器无消费路径"),
("App/algo/freq.c", "L2 派生测频纯算法(输入=phasor 使用区),模拟器无消费路径"),
("App/cfg/prot_set_store.c", "定值持久化 MCU 胶水(依赖 prot_set 纯算法),模拟器无消费路径"),
("App/log/soe_rec.c", "SOE 记录编解码纯逻辑(B9),L0.5 主机单测覆盖,模拟器无消费路径"),
("App/log/soe_store.c", "SOE 落盘 MCU 胶水(队列+FatFs 单写者,B9),模拟器无消费路径"),
("App/algo/sample_ring.c", "L2 环形缓冲纯算法,模拟器无消费路径"),
("App/algo/adc_cal.c", "L1.5 标定折算纯算法 + cfg 段注册(消费者 = MCU 侧 adcTask 每拍;模拟器不跑采样任务),模拟器无消费路径"),
("App/algo/meas.c", "L2 遥测快照发布纯算法(唯一发布者 = MCU 侧 adcTask 200 ms 分频;104/UI 侧消费属卡 9/UI 波次另行接线,模拟器无消费路径;L0.5 单测 test/host/meas 覆盖),模拟器无消费路径"),
("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"),
("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"),
("App/log/console_log.c", "MCU 控制台输出,模拟器无消费路径"),
("App/sync/time_sync.c", "对时任务(net+RTOS),MCU 专属"),
("App/task/adc_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
("App/task/default_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
("App/task/iec104_serv.c", "固件 FreeRTOS 任务(104 上行,B4),模拟器不跑固件任务"),
("App/task/protect_task.c", "固件 FreeRTOS 任务(保护判据节拍,B5),模拟器不跑固件任务"),
("App/task/net_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
("App/task/rs485_task.c", "固件 RS-485 任务,MCU 专属(另:uvprojx 缺口见 KNOWN_GAPS)"),
("App/task/ui_task.c", "固件 UI 任务,模拟器另有 main.c 驱动 UI"),
("App/protocol/iec104/iec104_asdu.c", "104 ASDU 编解码纯逻辑,L0.5 主机单测覆盖,模拟器无消费路径"),
("App/protocol/iec104/iec104_link.c", "104 链路状态机纯逻辑(I/O 注入),L0.5 覆盖,模拟器无消费路径"),
("App/protocol/iec104/iec104_pts.c", "104 点表/数据源适配(phasor/prot 依赖),模拟器无消费路径"),
("App/util/sha256.c", "纯工具(FTP 鉴权在 MCU 端),模拟器无消费路径"),
("App/util/sys_clock.c", "RTC/SD2506 依赖墙钟,MCU 专属"),
("App/util/task_prof.c", "任务性能探针,MCU 专属"),
("App/util/task_watchdog.c", "看门狗判定(RTOS 依赖),MCU 专属"),
# --- Drivers/BSP/:MCU 芯片/网络驱动与 FTP 服务(模拟器无对应)---
("Drivers/BSP/CH395F/ch395f.c", "MCU SPI 网卡驱动"),
("Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c", "MCU SPI NAND 驱动"),
("Drivers/BSP/GD5F2GQ5UE/nand_ftl.c", "dhara FTL 适配,MCU 存储栈"),
("Drivers/BSP/NET/lftpd/lftpd.c", "MCU FTP 服务器"),
("Drivers/BSP/NET/lftpd/lftpd_auth.c", "MCU FTP 鉴权"),
("Drivers/BSP/NET/lftpd/lftpd_inet.c", "MCU FTP 网络层"),
("Drivers/BSP/NET/lftpd/lftpd_io.c", "MCU FTP 文件 IO"),
("Drivers/BSP/NET/lftpd/lftpd_string.c", "MCU FTP 字符串工具"),
("Drivers/BSP/NET/net_select.c", "MCU socket 选择层"),
("Drivers/BSP/NET/net_socket.c", "MCU socket 后端,模拟器另有 net_socket_sim.c"),
("Drivers/BSP/NET/net_listener.c", "MCU 监听器生命周期(依赖 ch395f_* SPI 驱动与 net_socket.c 静态状态),模拟器无对应(Winsock 后端无孤儿态/重监听概念)"),
("Drivers/BSP/NET/net_events.c", "MCU 芯片事件处理 + 100ms 周期对账(依赖 ch395f_* SPI 驱动与驱动状态缓存),模拟器无对应(Winsock 后端无\"事件未收到/静默回收\"概念,无失同步可收敛)"),
("Drivers/BSP/NET/net_pending.c", "MCU 挂起 recv 模型(依赖 ch395f_* SPI 读出与 FreeRTOS 通知),模拟器另有 net_socket_sim.c 按外部语义模拟"),
("Drivers/BSP/RS485/rs485.c", "MCU RS-485 驱动"),
("Drivers/BSP/SD2506/sd2506.c", "MCU RTC 驱动"),
("Drivers/BSP/TPAFE5160/tpafe5160.c", "MCU 计量芯片驱动"),
("Drivers/BSP/UI/ui_port.c", "MCU UI 端口层(显示/触摸硬件),模拟器另有 sim/sim_port.c"),
]
# --- 已知缺口白名单(存量,N-23,Lead 已处置)---
# 命中 -> WARN 不拦;新缺口 -> GAP FAIL。仅针对"存在但未注册"方向。
KNOWN_GAPS = {
"App/task/rs485_task.c": "E-59 门禁刻意未接线(B2 消费者就绪时随任务立项注册)",
}
def to_repo_rel(path):
"""把绝对/相对路径归一为仓库相对 posix 路径。"""
p = os.path.normpath(path)
rel = os.path.relpath(p, REPO_ROOT)
return rel.replace("\\", "/")
def is_scan_scope(repo_rel):
"""是否在扫描范围内(App/、Drivers/BSP/、Src/ 下的 .c,排除第三方)。"""
if not repo_rel.endswith(".c"):
return False
for tp in THIRD_PARTY:
if repo_rel == tp or repo_rel.startswith(tp + "/"):
return False
for root in SCAN_ROOTS:
if repo_rel.startswith(root + "/"):
return True
return False
def scan_sources():
"""扫描扫描根下全部 .c,返回仓库相对 posix 路径集合。"""
found = set()
for root in SCAN_ROOTS:
abs_root = os.path.join(REPO_ROOT, root.replace("/", os.sep))
if not os.path.isdir(abs_root):
continue
for dirpath, _dirnames, filenames in os.walk(abs_root):
for fn in filenames:
if fn.endswith(".c"):
abs_p = os.path.join(dirpath, fn)
rel = to_repo_rel(abs_p)
if is_scan_scope(rel):
found.add(rel)
return found
def parse_uvprojx():
"""解析 uvprojx 的 <FilePath>,返回仓库相对 posix 路径集合(含 .c/.s)。"""
with open(UVPROJX, "r", encoding="utf-8", errors="replace") as f:
text = f.read()
mdk_dir = os.path.dirname(UVPROJX)
entries = set()
for m in re.finditer(r"<FilePath>(.*?)</FilePath>", text):
raw = m.group(1).strip()
if not raw:
continue
# uvprojx 路径相对 MDK-ARM/ 目录,反斜杠或正斜杠混用
joined = os.path.join(mdk_dir, raw.replace("\\", os.sep))
entries.add(to_repo_rel(joined))
return entries
def _expand_var(s, varmap, depth=0):
"""递归展开 ${VAR}。"""
if depth > 12:
return s
def repl(m):
name = m.group(1)
val = varmap.get(name, m.group(0))
return _expand_var(val, varmap, depth + 1)
return re.sub(r"\$\{(\w+)\}", repl, s)
def parse_cmake():
"""解析 CMakeLists 的 *_SOURCES 列表(先展开变量),返回仓库相对 posix 路径集合。"""
with open(CMAKELISTS, "r", encoding="utf-8", errors="replace") as f:
text = f.read()
# 1) 单值 set(NAME value):目录变量(不含 *_SOURCES)
varmap = {"CMAKE_CURRENT_SOURCE_DIR": SIM_DIR}
for m in re.finditer(r'set\s*\(\s*([A-Za-z_]\w*)\s+("?)([^"\r\n]*?)\2\s*\)', text):
name = m.group(1)
val = m.group(3).strip()
if name.endswith("_SOURCES"):
continue
varmap[name] = val
# 2) 多值 set(NAME ... ) 中名字以 _SOURCES 结尾的块
sources = set()
for m in re.finditer(r"set\s*\(\s*([A-Za-z_]\w*_SOURCES)\b(.*?)\)", text, re.DOTALL):
body = m.group(2)
# 逐行剥注释
lines = []
for line in body.splitlines():
idx = line.find("#")
if idx >= 0:
line = line[:idx]
lines.append(line)
body = " ".join(lines)
for tok in body.split():
expanded = _expand_var(tok, varmap)
if not expanded:
continue
# 展开后若仍是绝对/带盘符路径直接用,否则相对 simulator/ 目录
if os.path.isabs(expanded) or re.match(r"^[A-Za-z]:[\\/]", expanded):
cand = expanded
else:
cand = os.path.join(SIM_DIR, expanded)
sources.add(to_repo_rel(cand))
return sources
# --- UI 页面模块 → 描述符表接线(④,U4 门禁补强)---
UI_DIR_REL = "Drivers/BSP/UI"
def check_ui_page_wiring():
"""每个定义 `ui_page_<name>_desc` 的 `ui_page_*.c`,其 desc 必须被 UI 目录内其他文件引用。
UI 页面的"注册"有**两层**:构建注册(uvprojx + CMakeLists,①② 类)与框架注册
(`ui_content_page.h` 枚举 + `ui_content_page.c` 的 `s_defs[]` 表)。漏了后者的页面
两端照常编译链接(desc 无引用 → 链接器剥离),①②③ 全绿但菜单打不开这一页。
判定:页面模块 = `ui_page_*.c` 中定义 `<stem>_desc =` 的文件(不定义 desc 的
`ui_page_*.c` 不是页面模块——未来 U3 生成的 `ui_page_registry.c` 承载表引用,
本规则依旧成立且不会把它误判为页面);引用面 = `Drivers/BSP/UI/` 下全部 .c/.h,
排除该页自身 .c/.h(自身头文件里的 extern 声明不算接线)。
"""
ui_dir = os.path.join(REPO_ROOT, UI_DIR_REL.replace("/", os.sep))
if not os.path.isdir(ui_dir):
return [], 0
# 1) 页面模块清单:ui_page_*.c 中定义 `<stem>_desc =` 的
pages = {} # sym -> repo-rel .c
for fn in sorted(os.listdir(ui_dir)):
if not (fn.startswith("ui_page_") and fn.endswith(".c")):
continue
try:
text = open(os.path.join(ui_dir, fn), encoding="utf-8-sig", errors="replace").read()
except OSError:
continue
sym = fn[:-2] + "_desc"
if re.search(r"\b%s\b\s*=" % re.escape(sym), text):
pages[sym] = "%s/%s" % (UI_DIR_REL, fn)
if not pages:
return [], 0
# 2) 引用面扫描:UI 目录全部 .c/.h,排除各页自身 .c/.h
referenced = {sym: False for sym in pages}
for dirpath, _dirnames, filenames in os.walk(ui_dir):
for fname in filenames:
if not fname.endswith((".c", ".h")):
continue
rel_p = to_repo_rel(os.path.join(dirpath, fname))
try:
text = open(os.path.join(dirpath, fname), encoding="utf-8-sig", errors="replace").read()
except OSError:
continue
for sym, own_c in pages.items():
if rel_p in (own_c, own_c[:-2] + ".h"):
continue
if re.search(r"\b%s\b" % re.escape(sym), text):
referenced[sym] = True
gaps = []
for sym, rel_c in sorted(pages.items()):
if not referenced[sym]:
gaps.append("[GAP] %s | 页面未接入描述符表(%s 在 %s/ 内除自身外无引用 -> "
"编译得过但菜单打不开,死代码;新增 UI 页面必须过本项)"
% (rel_c, sym, UI_DIR_REL))
return gaps, len(pages)
def main():
ap = argparse.ArgumentParser(description="双端注册反向校验(uvprojx + CMakeLists)")
ap.add_argument("--strict", action="store_true",
help="关闭 KNOWN_GAPS 白名单:存量缺口也按 FAIL 处理(供审计)")
args = ap.parse_args()
# 环境校验
for path, label in ((UVPROJX, "uvprojx"), (CMAKELISTS, "CMakeLists.txt")):
if not os.path.isfile(path):
print("[ENV-ERROR] 注册表缺失: %s (%s)" % (label, path))
return 2
try:
scanned = scan_sources()
uv = parse_uvprojx()
cmake = parse_cmake()
except OSError as e:
print("[ENV-ERROR] 读取失败: %s" % e)
return 2
uv_c = {p for p in uv if is_scan_scope(p)}
cmake_c = {p for p in cmake if is_scan_scope(p)}
gaps = [] # 新缺口 -> FAIL
warns = [] # 存量白名单 -> WARN
# ① 存在但未进 uvprojx(无豁免:扫描范围内每个 .c 都应编入固件)
for f in sorted(scanned):
if f not in uv:
if (not args.strict) and f in KNOWN_GAPS:
warns.append("[WARN] %s | 缺 uvprojx(存量:%s)" % (f, KNOWN_GAPS[f]))
else:
gaps.append("[GAP] %s | 缺 uvprojx" % f)
# ② 存在但未进 CMakeLists(MCU-only 豁免)
for f in sorted(scanned):
if f in cmake_c:
continue
if any(fnmatch.fnmatch(f, pat) for pat, _ in SIM_EXEMPT):
continue
if (not args.strict) and f in KNOWN_GAPS:
warns.append("[WARN] %s | 缺 CMakeLists(存量:%s)" % (f, KNOWN_GAPS[f]))
else:
gaps.append("[GAP] %s | 缺 CMakeLists" % f)
# ③ 幽灵注册:注册了但文件不存在(反向)
for f in sorted(uv_c):
if not os.path.isfile(os.path.join(REPO_ROOT, f.replace("/", os.sep))):
gaps.append("[GAP] %s | 幽灵注册(uvprojx)" % f)
for f in sorted(cmake):
if not os.path.isfile(os.path.join(REPO_ROOT, f.replace("/", os.sep))):
gaps.append("[GAP] %s | 幽灵注册(CMakeLists)" % f)
# ④ UI 页面模块未接入描述符表(框架注册,U4;①②③ 照不出的盲区)
ui_gaps, ui_pages = check_ui_page_wiring()
gaps.extend(ui_gaps)
# 输出
print("== 双端注册反向校验 ==")
print("扫描 .c: %d | uvprojx(扫描范围): %d | CMakeLists(扫描范围): %d | UI 页面模块: %d"
% (len(scanned), len(uv_c), len(cmake_c), ui_pages))
for w in warns:
print(w)
for g in gaps:
print(g)
if gaps:
print("结果: FAIL(%d 个新缺口/幽灵注册/页面未接线,%d 个存量 WARN)" % (len(gaps), len(warns)))
return 1
if warns:
print("结果: PASS(无新缺口;%d 个存量缺口已白名单 WARN,N-23 Lead 处置中)" % len(warns))
return 0
print("结果: PASS(双端注册一致)")
return 0
if __name__ == "__main__":
sys.exit(main())