96a3d15efc
门禁解释器解耦
44 条 language: system 钩子此前把解释器写成仓库检出的绝对路径,换机、
换克隆目录都要重写。改为 entry 只写 scripts/gate_python.py + 脚本名,
由启动器按候选链解析:$DTU_GATE_PYTHON > <仓库>/.venv >
<仓库>/.gate/python > PATH。启动器用 execve 替换自身,并把选中的
解释器经 DTU_GATE_PYTHON 传给子进程,保证脚本内再调 Python 时一致。
实测 pre-commit 4.6.2 不展开 ${VAR};language: script 的 shebang 只做
纯 PATH 改写,会把决定权交给 PATH 首个 python,故不采用。
ci_local.py 预检识别启动器并报出解析结果。
并行工作树统一
task-worktree 新增 bench-gate(强制台架验收)与 merge(编排式合并),
删除 new/abort,done 标 DEPRECATED;台架通过记录改存
.agents/worktrees/.bench-pass-log。keil_build.py 的 plugin_tree_owner
识别 .agents/worktrees/<name> 并返回 pw:<name>。
check_doc_links / gen_refgraph / check_layer_deps 的扫描排除面补上
.agents;并行任务工作树方案重写为单一模型(7 节),取消工作树数量上限,
删历史实测记录节,两处 §7.3 悬空引用改为按语义引用。
文档与约定
本地工具路径环境变量方案 §5 重写为四节:谁跑在哪个 Python 上、写新钩子、
预检对号入座、动环境五步。文档规范的变更记录例外名单由 5 篇减为 4 篇
(本篇实际没有该节)。AGENTS.md 补委派验收口径三条。
验证:pre_commit run --all-files 48 钩子全 Passed;ci_local.py
--preflight-only 报 44 条经启动器 -> 仓库 .venv;keil-build 0 errors
0 warnings;check_doc_links --sections-strict 全通过。
156 lines
6.9 KiB
Python
156 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。
|
||
# 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", ".dsh-worktrees", ".agents")];
|
||
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())
|