c4edf352fe
B-6 分层收紧(UI_数据供给设计说明.md §7.2 契约原样落地):
- ui_w_records:删 #include "sys_log.h",改 ui_rec_reader_t 只读函数表 +
ui_w_records_bind() 模块级注入(sys_log 读 API 本为全局,函数表共享、
实例读盘缓存仍各自独享——切页陈旧行防御不变);log_id 换不透明编号
UI_W_REC_LOG_RUN/SELFTEST;缓冲容量宏本地化 UI_W_REC_TS/DESC_LEN
(20/29,与 sys_log 契约下限同宽;读失败退化空行防御保留)
- ui_page_runlog / ui_page_selftest:去 sys_log include,实例改不透明编号
- 两端薄包装适配层(编号→syslog_id_t 显式映射,错误码 0/-1/-2 直通不改写):
App/task/ui_task.c 与 simulator/main.c(无头+窗口两个 init 点),均在
ui_menu_init 之前 ui_w_records_bind;未绑定窗口组件按空态防御不读野
- scripts/check_layer_deps.py:BASELINE 6→3(--prune 实测恰 3 条 UI 记录
边消失后写回;剩 NETCFG×2 + lftpd_io 与 UI 无关)——道B 独占收紧口径
- 文档同步:门禁说明 layer-deps.md §1/§6/§9 与 README 计数 6→3;
UI_数据供给设计说明.md §7.2 加落地注记、§14"计划削减"改"已兑现"
- 首提被 id-check 拦下:注释/文档里自造"批卡编号"(B-6/道B-3)命中
"未登记前缀"增量拦截——已全数改回 task-6/道B 普通指称(编号不回收、
禁自造,教训入本提交);id-check 复跑 RC=0(S10 告警为存量非本改动)
验证:
- 模拟器 0 警告;菜单 8 状态 + 内容页 11 页黄金参考逐字节 PASS
(记录页数据流经 reader 后像素与基线一致,无基线更新提交)
- cppcheck/coding-standard/bsp-err/font-charset/no-mojibake 单跑全绿;
layer-deps 复跑:6 处反向 include = 豁免 3 / 基线内 3 / 新增 0,
--prune 显示"基线无需收紧(3 条存量全部仍成立)"
- doc-links --all 的 35 处告警全为本仓检出路径含空格误扫 SDL2 自带 md
(环境问题、非本改动引入;提交门禁按暂存文件名过滤不受影响)
- @build 0 Error 0 Warning(终树复跑 BUILD_OK);uvprojx 无 UV4 抖动、
无新文件不涉注册;其余 38 钩全绿(ui-golden/font-charset/coding-standard/
bsp-err/id-check/cppcheck…单跑复验 RC=0)
增量(A/B 同树 .map,基线 = 合入 c1b425d 后 HEAD):
- Flash:Code +104 B、RO +8 B(两端薄包装代码 + reader 函数表 const)
- RAM:RW +4 B(s_reader 指针)/ ZI +4 B(两页实例 struct 枚举改 uint8
本应 -8 B,链接段对齐后净 +4 B);RW+ZI 合计 +8 B
gate: only mem-budget/mem-numbers pending lead ledger backfill
- pre-commit run --all-files 实测仅这两条失败(台账/RAM 文档 vs .map 漂移,
根因即上列 Code +104/RO +8/RW +4/ZI +4),其余 38 钩全绿;按 Lead 裁决
(task-4 口径"以后照此办理",含 Flash/RAM 漂移同例外)--no-verify 提交,
合并后请主树回填台账(scripts/ram_baseline.py --write 由 Lead 侧执行)
- 手工自查通过:5 个 BOM 文件 BOM=EF BB BF 齐全、docs/py/main.c 保持无 BOM,
全部 10 文件 CR=0、U+FFFD=0
- 台账增量供回填:Flash Code +104 B、RO +8 B;RAM RW +4 B、ZI +4 B
155 lines
6.9 KiB
Python
155 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""分层方向门禁:Drivers/BSP/** 不得 include App/** 的头(反向依赖增量拦截)。
|
||
|
||
背景(2026-10-22 加)
|
||
----------------
|
||
AGENTS.md 与 docs/架构设计/项目架构总览.md 都写着"六层单向依赖:业务 → UI/基础服务
|
||
→ 中间件 → BSP 驱动 → HAL/RTOS → 硬件",但**此前没有任何机械检查**:Drivers/BSP/ 里
|
||
可以随手 include "cfg_file.h" 而无人报警。后果是可测量的口径漂移——2026-09 审查报告
|
||
记"NETCFG 一处",2026-10-22 实测已长到 8 个文件 9 处。边界的静默腐蚀只能靠门禁止住:
|
||
先把现状钉成基线,然后**只拦新增**。
|
||
|
||
与 scripts/check_ids.py 的"增量拦截"同构:
|
||
· 新出现的 (引用方, App 头) 组合 → FAIL(拦提交)
|
||
· 基线里的组合已消失 → WARN,并提示收紧(基线只减不增,绝没有 --write 往里加)
|
||
· 横切基础设施头走 ALLOW 永久豁免:它们是"任何层都要用的叶子能力",不是上层服务
|
||
|
||
用法
|
||
----
|
||
python scripts/check_layer_deps.py # 门禁模式(pre-commit / CI)
|
||
python scripts/check_layer_deps.py --verbose # 列出全部反向边与判定
|
||
python scripts/check_layer_deps.py --prune # 只报告可以删掉的基线条目
|
||
|
||
退出码:0 = 通过;1 = 发现新增反向依赖;2 = 用法/环境错误
|
||
"""
|
||
|
||
import argparse
|
||
import os
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
|
||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
BSP_PREFIX = "Drivers/BSP/"
|
||
APP_PREFIX = "App/"
|
||
SRC_EXT = (".c", ".h")
|
||
|
||
# 横切基础设施:任何层都可用,允许被 BSP include。
|
||
# 新增条目必须先回答"它为什么不是上层服务"——否则就是给自己开后门。
|
||
ALLOW = {
|
||
"App/log/console_log.h", # 日志后端:BSP 的 dbg_log.h 本来就建在它上面
|
||
"App/util/sys_clock.h", # 时间源:日志时间戳的唯一提供者
|
||
"App/util/sha256.h", # 纯算法工具:无状态、不依赖任何上层服务
|
||
}
|
||
|
||
# 存量基线(2026-10-22 清点 6 条,只减不增)。每条 = (引用方, 被引用的 App 头)。
|
||
# 修法是"注入/传参",不是把上层服务搬进 BSP——详见报告 §0.4 的 S-1 行。
|
||
# 2026-10-25 道B(task/UI-rec-reader)还清 UI 记录 3 条:ui_w_records 改注入式
|
||
# ui_rec_reader_t(契约 UI_数据供给设计说明.md §7.2),runlog/selftest/records.h
|
||
# 不再 include sys_log.h,绑定在 App/task/ui_task.c 与 simulator/main.c。
|
||
BASELINE = {
|
||
("Drivers/BSP/NETCFG/net_cfg.c", "App/cfg/cfg_file.h"), # 配置引擎属主 = defaultTask
|
||
("Drivers/BSP/NETCFG/ftp_cfg.c", "App/cfg/cfg_file.h"), # 同上([ftp] 段注册)
|
||
("Drivers/BSP/NET/lftpd/lftpd_io.c", "App/fs/vol_lock.h"), # 卷锁在 App/fs
|
||
}
|
||
|
||
INC_RE = re.compile(r'^[ \t]*#[ \t]*include[ \t]*"([^"]+)"', re.M)
|
||
|
||
|
||
def tracked_files():
|
||
"""以 git ls-files 为准(不扫构建产物);git 不可用时退回 os.walk。"""
|
||
try:
|
||
out = subprocess.check_output(["git", "ls-files", "-z"], cwd=REPO)
|
||
return [p.decode("utf-8") for p in out.split(b"\0") if p]
|
||
except Exception: # noqa: BLE001
|
||
files = []
|
||
for root, dirs, names in os.walk(REPO):
|
||
dirs[:] = [d for d in dirs if d not in (".git", ".venv", "build")];
|
||
for n in names:
|
||
files.append(os.path.relpath(os.path.join(root, n), REPO).replace(os.sep, "/"))
|
||
return files
|
||
|
||
|
||
def app_header_index(files):
|
||
idx = {}
|
||
for f in files:
|
||
if f.startswith(APP_PREFIX) and f.endswith(".h"):
|
||
idx.setdefault(os.path.basename(f), []).append(f)
|
||
return idx
|
||
|
||
|
||
def scan(files, idx):
|
||
edges = set()
|
||
for f in files:
|
||
if not f.startswith(BSP_PREFIX) or not f.endswith(SRC_EXT):
|
||
continue
|
||
try:
|
||
with open(os.path.join(REPO, f), encoding="utf-8-sig") as fh:
|
||
text = fh.read()
|
||
except (OSError, UnicodeDecodeError) as exc:
|
||
print("[WARN] 读取失败,跳过:%s(%s)" % (f, exc))
|
||
continue
|
||
for m in INC_RE.finditer(text):
|
||
literal = m.group(1)
|
||
owners = idx.get(os.path.basename(literal))
|
||
if not owners:
|
||
continue
|
||
norm = os.path.normpath(literal).replace(os.sep, "/")
|
||
exact = [o for o in owners if norm == o or o.endswith("/" + norm)]
|
||
for o in (exact or owners):
|
||
edges.add((f, o))
|
||
return sorted(edges)
|
||
|
||
|
||
def main(argv=None):
|
||
ap = argparse.ArgumentParser(description="BSP 不得反向 include App 的头(增量拦截)")
|
||
ap.add_argument("--verbose", action="store_true", help="列出全部反向边与判定")
|
||
ap.add_argument("--prune", action="store_true", help="只报告已消失、可从基线删除的条目")
|
||
args = ap.parse_args(argv)
|
||
|
||
files = tracked_files()
|
||
edges = scan(files, app_header_index(files))
|
||
live = set(e for e in edges if e[1] not in ALLOW)
|
||
allowed = [e for e in edges if e[1] in ALLOW]
|
||
new = sorted(live - BASELINE)
|
||
gone = sorted(BASELINE - live)
|
||
|
||
if args.verbose:
|
||
print("== 反向 include 全量边(含豁免)==")
|
||
for src, hdr in edges:
|
||
tag = "豁免" if hdr in ALLOW else ("基线" if (src, hdr) in BASELINE else "新增")
|
||
print(" [%s] %s -> %s" % (tag, src, hdr))
|
||
|
||
if args.prune:
|
||
if not gone:
|
||
print("[OK] 基线无需收紧(%d 条存量全部仍成立)" % len(BASELINE))
|
||
return 0
|
||
print("以下基线条目已不再成立,请从 BASELINE 删除(基线只减不增):")
|
||
for src, hdr in gone:
|
||
print(' ("%s", "%s"),' % (src, hdr))
|
||
return 0
|
||
|
||
print("分层方向:BSP 反向 include App 共 %d 处(豁免 %d / 基线内 %d / 新增 %d)"
|
||
% (len(edges), len(allowed), len(live & BASELINE), len(new)))
|
||
if gone:
|
||
print("[WARN] %d 条基线已消失,建议收紧(跑 --prune 看明细)" % len(gone))
|
||
if new:
|
||
print("")
|
||
print("[FAIL] 新增反向依赖 %d 处——BSP 只依赖相邻下层,不许 include App 层的头:" % len(new))
|
||
for src, hdr in new:
|
||
print(" %s: include \"%s\"" % (src, hdr))
|
||
print("")
|
||
print(" 正确做法(三选一):")
|
||
print(" ① 把上层能力注入给 BSP(函数指针/回调,参照 net_cfg_set_save_req_cb 的先例);")
|
||
print(" ② 由上层把数据取好再传进来(参数/结构体),BSP 不自己去拿;")
|
||
print(" ③ 若它其实是叶子工具(无状态、不依赖上层服务),可加进 ALLOW 并注明理由。")
|
||
print(" 顺手修掉存量也欢迎:跑 --prune 看哪些基线可以删。")
|
||
return 1
|
||
print("[OK] 无新增反向依赖(存量基线只减不增)")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|