21c68521ff
诉求最初是升级 dsh-task-worktree:实测已装 0.4.6 = npm dist-tags.latest,无需升级; 真正的缺口是本仓 skill 看不见插件建的树(.dsh-worktrees/),故按用户追加指令把两套 系统接起来。回收方向刻意不打通:本 skill 永不删插件的树。 脚本 task_worktree.py:新增 plugin 子命令 list/adopt/unadopt,事实源是 git worktree list(不是插件 manifest,免得被绕过);adopt 只补缺失的 .venv 与 simulator/SDL2 联接,已有联接跳过、实体目录绝不碰;锁归属用 pw:<name> 前缀;new 顺带提示另有 N 棵 插件树、不占活跃上限 3。 一并修一处真 bug:lock release --bench-pass 在任务无台账时先 EnvError 退出、锁释放 不掉,导致每个任务的 done 收口连坐失败;现在改为警告后仍然释放。 keil-build:default_lock_task() 增加 .dsh-worktrees/ 反推分支,插件树内的构建归属 pw:<name> 而不是兜底的 build-cli(主树行为未变,本次 @build 实测仍走 build-cli)。 .gitignore 预先忽略 .dsh-worktrees/,让插件的 ensureIgnored() 找到同一字面量、不再 改写这份受控文件。check_doc_links / check_layer_deps / gen_refgraph 三处扫描面同步 排除:仓里再放一份仓会让 doc-links 假红、并让生成的配图字节漂移到 generated-fresh。 文档:并行任务工作树方案新增 §3.4 两套系统边界对照;SKILL.md 加 §F 共存一节,同时把 §B 从 53 行压到 29 行(净 +39,用户此前在飞的改写已 rebase 保留、未被覆盖)。 验证:@build 0 错误 0 警告;scripts/ci_local.py 48 根提交期钩子与 gitlint 全绿; check_doc_links --all --sections-strict 通过(2596 章节引用、无法判定 0);id-check-new 通过;改动文件字节体检无 BOM、CRLF 与 U+FFFD。桥接行为在临时 scratch 仓端到端跑过 (adopt 建两个 reparse、unadopt 只摘联接、pw 锁取放、缺台账也能释放、越界 name 退 2)。
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")];
|
||
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())
|