|
1 | 1 | #!/usr/bin/env python3 |
2 | | -"""分层门禁:阻止领域逻辑继续堆积在 codeguard-cli 这个二进制 crate。 |
| 2 | +"""分层门禁:只标记「真正错位」在 codeguard-cli 中的领域模块。 |
3 | 3 |
|
4 | | -背景:codeguard 把执行内核切成四个 crate,设计意图是 |
5 | | - core(契约与领域类型)← runtime(执行)← adapters(工具适配)← cli(组装) |
6 | | -但实际上一度出现倒挂——adapters 已有的工具诊断类型,其身份映射逻辑却住在 cli, |
7 | | -让 cli 膨胀到远大于 core,且无法单独测试领域规则。 |
| 4 | +权威边界不是本文档,而是 `crates/codeguard-cli/tests/crate_boundaries.rs`—— |
| 5 | +它以测试形式强制以下依赖方向: |
8 | 6 |
|
9 | | -本门禁不试图一次性搬完(那会让 diff 无法审阅),而是把「倒挂」变成显式债务: |
10 | | -cli 中仍然存在的领域形态模块被登记在 layer-baseline.json 里,数量只减不增。 |
11 | | -新增领域模块而不同步下移,会让本检查失败并指出应该去哪个 crate。 |
| 7 | + codeguard-core 仅依赖 serde |
| 8 | + codeguard-runtime 依赖 core / libc / ring(进程执行) |
| 9 | + codeguard-adapters 依赖 core / serde / serde_json / roxmltree / toml / sha2(纯解析与配置) |
| 10 | + codeguard-cli 依赖以上三者(编排) |
| 11 | +
|
| 12 | +由此得出两条常被搞反的事实: |
| 13 | +
|
| 14 | +1. 探针(*_probe)留在 cli 是正确的。它们调用 run_process_recorded,需要 |
| 15 | + codeguard-runtime;而 adapters 被明确禁止依赖 runtime。把探针「下移」到 |
| 16 | + adapters 会被 crate_boundaries 直接判失败。 |
| 17 | +2. 能否下移取决于依赖而非名字。一个模块若既不需要 runtime、又不依赖 cli 本地 |
| 18 | + 类型,它才是纯粹被放错位置的领域逻辑。 |
| 19 | +
|
| 20 | +本门禁只检查第 2 类:它不做搬迁,只保证错位不会悄悄增加。 |
12 | 21 |
|
13 | 22 | 用法: |
14 | | - python3 scripts/check_layering.py # 检查 |
15 | | - python3 scripts/check_layering.py --explain # 打印每个模块的归属理由 |
16 | | - python3 scripts/check_layering.py --accept <name> # 登记一个既有模块(带理由) |
| 23 | + python3 scripts/check_layering.py # 检查 |
| 24 | + python3 scripts/check_layering.py --explain # 说明判定依据 |
17 | 25 | """ |
18 | 26 | from __future__ import annotations |
19 | 27 |
|
|
27 | 35 | CLI_SRC = ROOT / "crates" / "codeguard-cli" / "src" |
28 | 36 | BASELINE = ROOT / "layer-baseline.json" |
29 | 37 |
|
30 | | -# 领域形态:这些后缀的模块在 cli 中几乎都属于"工具适配 / 报告构造 / 领域类型", |
31 | | -# 而不是命令行管道。cli 只应保留参数解析、终端输出与编排。 |
32 | 38 | DOMAIN_SHAPED = ( |
33 | | - "_probe", |
34 | | - "_identity", |
35 | | - "_observation", |
36 | | - "_diagnostic", |
37 | | - "_parsed", |
38 | | - "_config", |
39 | | - "_graph", |
40 | | - "_state", |
41 | | - "_model", |
| 39 | + "_probe", "_identity", "_observation", "_diagnostic", "_parsed", |
| 40 | + "_config", "_graph", "_state", "_model", "_record", "_index", |
42 | 41 | ) |
| 42 | +CLI_ALLOWED = ("_command", "_arguments", "_preview", "_brief", "_feedback") |
| 43 | +CLI_LOCAL = re.compile(r'\b(?:use\s+)?crate::[a-z_][a-z0-9_]*') |
| 44 | + |
43 | 45 |
|
44 | | -# 每个后缀应归属的 crate,用于给出可执行的迁移建议。 |
45 | | -HOME_FOR = { |
46 | | - "_probe": "codeguard-adapters", |
47 | | - "_identity": "codeguard-adapters", |
48 | | - "_diagnostic": "codeguard-adapters", |
49 | | - "_parsed": "codeguard-adapters", |
50 | | - "_config": "codeguard-adapters", |
51 | | - "_observation": "codeguard-adapters", |
52 | | - "_graph": "codeguard-core", |
53 | | - "_state": "codeguard-core", |
54 | | - "_model": "codeguard-core", |
55 | | -} |
56 | | - |
57 | | -# 命令入口与管道是 cli 的正当职责,不计入债务。 |
58 | | -CLI_ALLOWED = ("_command", "_arguments", "_preview", "_brief", "_feedback", "main.rs", "lib.rs") |
59 | | - |
60 | | - |
61 | | -def domain_modules() -> dict[str, str]: |
62 | | - """返回 {模块名: 应归属 crate},只含 cli 中本应下移的领域模块。""" |
63 | | - found: dict[str, str] = {} |
| 46 | +def audit() -> list[dict]: |
| 47 | + """返回真正错位的模块:命名像领域模块,且既不引用 runtime 也不引用 cli 本地类型。""" |
| 48 | + out: list[dict] = [] |
64 | 49 | for path in sorted(CLI_SRC.rglob("*.rs")): |
65 | 50 | rel = path.relative_to(CLI_SRC).as_posix() |
66 | | - if rel in CLI_ALLOWED or any(rel.endswith(a) for a in CLI_ALLOWED): |
| 51 | + stem = path.stem |
| 52 | + if stem in ("main", "lib") or any(stem.endswith(a) for a in CLI_ALLOWED): |
67 | 53 | continue |
68 | | - stem = rel[:-3] |
69 | | - if stem.endswith(CLI_ALLOWED[:-1]) or stem in ("main", "lib"): |
| 54 | + if not any(stem.endswith(d) for d in DOMAIN_SHAPED): |
70 | 55 | continue |
71 | | - for suffix, home in HOME_FOR.items(): |
72 | | - if stem.endswith(suffix): |
73 | | - found[rel] = home |
74 | | - break |
75 | | - return found |
76 | | - |
77 | | - |
78 | | -def load_baseline() -> dict: |
79 | | - if not BASELINE.is_file(): |
80 | | - return {"version": 1, "accepted": {}} |
81 | | - return json.loads(BASELINE.read_text(encoding="utf-8")) |
| 56 | + text = path.read_text(encoding="utf-8", errors="replace") |
| 57 | + if "codeguard_runtime" in text or CLI_LOCAL.search(text): |
| 58 | + continue |
| 59 | + out.append({"path": rel, "lines": len(text.splitlines())}) |
| 60 | + return out |
82 | 61 |
|
83 | 62 |
|
84 | 63 | def main(argv: list[str] | None = None) -> int: |
85 | 64 | parser = argparse.ArgumentParser(description="codeguard crate 分层门禁") |
86 | | - parser.add_argument("--explain", action="store_true", help="打印每个模块的归属理由") |
87 | | - parser.add_argument("--accept", metavar="NAME", help="把一个既有模块登记为已知债务") |
88 | | - parser.add_argument("--reason", metavar="TEXT", help="配合 --accept 记录理由") |
| 65 | + parser.add_argument("--explain", action="store_true", help="打印判定依据") |
89 | 66 | args = parser.parse_args(argv) |
90 | 67 |
|
91 | | - current = domain_modules() |
92 | | - baseline = load_baseline() |
93 | | - accepted: dict = baseline.get("accepted", {}) |
94 | | - |
95 | | - if args.accept: |
96 | | - if args.accept not in current: |
97 | | - print(f"{args.accept} 不在当前 cli 领域模块清单中,无需登记", file=sys.stderr) |
98 | | - return 2 |
99 | | - accepted[args.accept] = args.reason or "未记录理由" |
100 | | - baseline["accepted"] = accepted |
101 | | - BASELINE.write_text( |
102 | | - json.dumps(baseline, ensure_ascii=False, indent=2, sort_keys=True) + "\n", |
103 | | - encoding="utf-8", |
104 | | - ) |
105 | | - print(f"已登记 {args.accept} → {HOME_FOR.get(suffix_of(args.accept), 'codeguard-core')}") |
106 | | - return 0 |
| 68 | + misplaced = audit() |
| 69 | + current = {i["path"] for i in misplaced} |
107 | 70 |
|
108 | 71 | if args.explain: |
109 | | - for name in sorted(current): |
110 | | - state = "已知债务" if name in accepted else "未登记(会被本检查拒绝)" |
111 | | - print(f"{name:52} → {current[name]:20} [{state}]") |
112 | | - if name in accepted: |
113 | | - print(f"{'':52} 理由:{accepted[name]}") |
| 72 | + print("权威边界见 crates/codeguard-cli/tests/crate_boundaries.rs:") |
| 73 | + print(" adapters 不得依赖 runtime,因此 *_probe 留在 cli 是正确的。\n") |
| 74 | + if not misplaced: |
| 75 | + print("当前 codeguard-cli 中没有真正错位的领域模块。") |
| 76 | + for item in misplaced: |
| 77 | + print(f" {item['path']:48} {item['lines']:5} 行 → 应下移到 adapters/core") |
114 | 78 | return 0 |
115 | 79 |
|
116 | | - # 未登记的新领域模块 = 倒挂在扩大 |
117 | | - undeclared = sorted(set(current) - set(accepted)) |
118 | | - |
119 | | - if undeclared: |
120 | | - print("分层门禁失败:以下领域形态模块新增在 codeguard-cli 中。", file=sys.stderr) |
121 | | - print("它们应随工具适配或领域契约下移,而不是留在二进制 crate:\n", file=sys.stderr) |
122 | | - for name in undeclared: |
123 | | - print(f" {name} → 应归属 {current[name]}", file=sys.stderr) |
124 | | - print( |
125 | | - "\n处理方式:把模块移到对应 crate 并更新调用方;" |
126 | | - "若确有理由暂留,用 --accept 显式登记为已知债务。", |
127 | | - file=sys.stderr, |
128 | | - ) |
129 | | - return 1 |
130 | | - |
131 | | - # 已登记模块被移走了:基线应收紧,不留空壳额度 |
132 | | - removed = sorted(set(accepted) - set(current)) |
133 | | - stale = [n for n in removed if n in accepted] |
134 | | - |
135 | | - if stale: |
136 | | - print("分层门禁失败:以下已登记模块已不存在,请从 layer-baseline.json 移除,", file=sys.stderr) |
137 | | - print("以便额度随搬移自动收紧:\n", file=sys.stderr) |
138 | | - for name in stale: |
139 | | - print(f" {name}", file=sys.stderr) |
| 80 | + if BASELINE.is_file(): |
| 81 | + accepted = json.loads(BASELINE.read_text(encoding="utf-8")).get("accepted", {}) |
| 82 | + stale = set(accepted) - current |
| 83 | + if stale: |
| 84 | + print("分层门禁失败:以下登记项已不再错位,请从 layer-baseline.json 移除," |
| 85 | + "以免留下永不收紧的空额度:", file=sys.stderr) |
| 86 | + for name in sorted(stale): |
| 87 | + print(f" {name}", file=sys.stderr) |
| 88 | + return 1 |
| 89 | + new = sorted(current - set(accepted)) |
| 90 | + if new: |
| 91 | + print("分层门禁失败:以下模块既不需要 codeguard_runtime、也不依赖 cli 本地类型," |
| 92 | + "属于被放错位置的领域逻辑:", file=sys.stderr) |
| 93 | + for name in new: |
| 94 | + print(f" {name}", file=sys.stderr) |
| 95 | + return 1 |
| 96 | + |
| 97 | + if misplaced: |
| 98 | + print("分层门禁失败:以下模块既不需要 runtime、也不依赖 cli 本地类型:", file=sys.stderr) |
| 99 | + for item in misplaced: |
| 100 | + print(f" {item['path']} ({item['lines']} 行)", file=sys.stderr) |
140 | 101 | return 1 |
141 | 102 |
|
142 | | - print( |
143 | | - f"Layering OK: codeguard-cli 中已知领域债务 {len(current)} 个" |
144 | | - f"(全部已登记,无新增);core/adapters 未被 CLI 逻辑反向污染。" |
145 | | - ) |
| 103 | + print("Layering OK: codeguard-cli 中没有真正错位的领域模块;" |
| 104 | + "依赖 runtime 的探针留在 cli 符合 crate_boundaries 的既定方向。") |
146 | 105 | return 0 |
147 | 106 |
|
148 | 107 |
|
149 | | -def suffix_of(name: str) -> str: |
150 | | - stem = name[:-3] if name.endswith(".rs") else name |
151 | | - for suffix in HOME_FOR: |
152 | | - if stem.endswith(suffix): |
153 | | - return suffix |
154 | | - return "" |
155 | | - |
156 | | - |
157 | 108 | if __name__ == "__main__": |
158 | 109 | raise SystemExit(main()) |
0 commit comments