From f0f2d7a21ef84d1338aaeb84658863b739af8dbb Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Tue, 29 Sep 2026 01:55:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(layering):=20=E7=BA=A0=E6=AD=A3=E5=88=86?= =?UTF-8?q?=E5=B1=82=E9=97=A8=E7=A6=81=E7=9A=84=E9=94=99=E4=BD=8D=E5=88=A4?= =?UTF-8?q?=E6=8D=AE=EF=BC=8C=E6=92=A4=E9=94=80=E9=94=99=E8=AF=AF=E7=9A=84?= =?UTF-8?q?=2013=20=E9=A1=B9=E5=80=BA=E5=8A=A1=E5=9F=BA=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #1 的门禁按「命名像领域模块就应下移到 adapters」判定,这是错的。 本仓 `crates/codeguard-cli/tests/crate_boundaries.rs` 早已用测试强制: codeguard-adapters 仅可依赖 core / serde / serde_json / roxmltree / toml / sha2 codeguard-runtime 承担进程执行 codeguard-cli 编排,可依赖以上三者 据此,`*_probe` 探针调用 `run_process_recorded`、需要 runtime,而 adapters 被 明确禁止依赖 runtime——**探针留在 cli 是正确的,把它们"下移"会被 crate_boundaries 直接判失败**。PR #1 的基线把 9 个探针登记为债务,等于在 引导后来者走一条注定被本仓测试拒绝的路。 实测 13 项登记项的真实依赖: - 10 项依赖 codeguard_runtime → 依边界不得进 adapters - module_graph / cargo_module_graph 虽不依赖 runtime,但依赖 cli 本地的 crate::discovery,不能单独下移 → 真正错位者为 0。 本 PR 改用「依赖」而非「命名」判定:只有既不需要 runtime、也不引用 cli 本地 类型的领域模块才算错位,并删除 layer-baseline.json(纠正后无已知债务)。 双向负向验证: - 新增自足领域模型 → 失败(正确捕获) - 新增依赖 runtime 的探针 → 放行(不再误报) crates/ 零改动;测试 997 通过 / 0 失败。 --- layer-baseline.json | 19 ---- scripts/check_layering.py | 183 ++++++++++++++------------------------ 2 files changed, 67 insertions(+), 135 deletions(-) delete mode 100644 layer-baseline.json diff --git a/layer-baseline.json b/layer-baseline.json deleted file mode 100644 index 2e220be0..00000000 --- a/layer-baseline.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "accepted": { - "cargo_module_graph.rs": "Cargo 模块图领域模型;目标 core", - "checkstyle_probe.rs": "包裹 checkstyle 原生调用;目标 adapters,随 checkstyle 适配迁移", - "eslint_probe.rs": "包裹 eslint 原生调用与配置解析;目标 adapters,随 eslint 适配迁移", - "maven_dependency_probe.rs": "包裹 OWASP dependency-check;目标 adapters", - "maven_javadoc_probe.rs": "包裹 Maven Javadoc 生命周期;目标 adapters", - "maven_probe.rs": "包裹 Maven 原生调用;目标 adapters,随 Maven 适配迁移", - "module_graph.rs": "模块图领域模型;目标 core,与 core 既有 task_graph 同类", - "npm_audit_probe.rs": "包裹 npm audit 原生调用;目标 adapters", - "npm_input_state.rs": "npm 输入状态领域模型;目标 core", - "owasp_maven_probe.rs": "包裹 OWASP Maven 报告解析;目标 adapters", - "pmd6_probe.rs": "包裹 PMD6 原生调用;目标 adapters,随 PMD 适配迁移", - "ruff_probe.rs": "包裹 ruff 原生调用与配置观察;目标 adapters,随 ruff 适配迁移", - "tool_identity.rs": "工具身份与版本绑定;目标 adapters,core 已有 native_checker_binding 可对齐" - }, - "note": "codeguard-cli 中的已知分层债务。只减不增:模块下移后必须同步移除本文件登记。", - "version": 1 -} diff --git a/scripts/check_layering.py b/scripts/check_layering.py index d342dd56..f32aaf40 100644 --- a/scripts/check_layering.py +++ b/scripts/check_layering.py @@ -1,19 +1,27 @@ #!/usr/bin/env python3 -"""分层门禁:阻止领域逻辑继续堆积在 codeguard-cli 这个二进制 crate。 +"""分层门禁:只标记「真正错位」在 codeguard-cli 中的领域模块。 -背景:codeguard 把执行内核切成四个 crate,设计意图是 - core(契约与领域类型)← runtime(执行)← adapters(工具适配)← cli(组装) -但实际上一度出现倒挂——adapters 已有的工具诊断类型,其身份映射逻辑却住在 cli, -让 cli 膨胀到远大于 core,且无法单独测试领域规则。 +权威边界不是本文档,而是 `crates/codeguard-cli/tests/crate_boundaries.rs`—— +它以测试形式强制以下依赖方向: -本门禁不试图一次性搬完(那会让 diff 无法审阅),而是把「倒挂」变成显式债务: -cli 中仍然存在的领域形态模块被登记在 layer-baseline.json 里,数量只减不增。 -新增领域模块而不同步下移,会让本检查失败并指出应该去哪个 crate。 + codeguard-core 仅依赖 serde + codeguard-runtime 依赖 core / libc / ring(进程执行) + codeguard-adapters 依赖 core / serde / serde_json / roxmltree / toml / sha2(纯解析与配置) + codeguard-cli 依赖以上三者(编排) + +由此得出两条常被搞反的事实: + +1. 探针(*_probe)留在 cli 是正确的。它们调用 run_process_recorded,需要 + codeguard-runtime;而 adapters 被明确禁止依赖 runtime。把探针「下移」到 + adapters 会被 crate_boundaries 直接判失败。 +2. 能否下移取决于依赖而非名字。一个模块若既不需要 runtime、又不依赖 cli 本地 + 类型,它才是纯粹被放错位置的领域逻辑。 + +本门禁只检查第 2 类:它不做搬迁,只保证错位不会悄悄增加。 用法: - python3 scripts/check_layering.py # 检查 - python3 scripts/check_layering.py --explain # 打印每个模块的归属理由 - python3 scripts/check_layering.py --accept # 登记一个既有模块(带理由) + python3 scripts/check_layering.py # 检查 + python3 scripts/check_layering.py --explain # 说明判定依据 """ from __future__ import annotations @@ -27,132 +35,75 @@ CLI_SRC = ROOT / "crates" / "codeguard-cli" / "src" BASELINE = ROOT / "layer-baseline.json" -# 领域形态:这些后缀的模块在 cli 中几乎都属于"工具适配 / 报告构造 / 领域类型", -# 而不是命令行管道。cli 只应保留参数解析、终端输出与编排。 DOMAIN_SHAPED = ( - "_probe", - "_identity", - "_observation", - "_diagnostic", - "_parsed", - "_config", - "_graph", - "_state", - "_model", + "_probe", "_identity", "_observation", "_diagnostic", "_parsed", + "_config", "_graph", "_state", "_model", "_record", "_index", ) +CLI_ALLOWED = ("_command", "_arguments", "_preview", "_brief", "_feedback") +CLI_LOCAL = re.compile(r'\b(?:use\s+)?crate::[a-z_][a-z0-9_]*') + -# 每个后缀应归属的 crate,用于给出可执行的迁移建议。 -HOME_FOR = { - "_probe": "codeguard-adapters", - "_identity": "codeguard-adapters", - "_diagnostic": "codeguard-adapters", - "_parsed": "codeguard-adapters", - "_config": "codeguard-adapters", - "_observation": "codeguard-adapters", - "_graph": "codeguard-core", - "_state": "codeguard-core", - "_model": "codeguard-core", -} - -# 命令入口与管道是 cli 的正当职责,不计入债务。 -CLI_ALLOWED = ("_command", "_arguments", "_preview", "_brief", "_feedback", "main.rs", "lib.rs") - - -def domain_modules() -> dict[str, str]: - """返回 {模块名: 应归属 crate},只含 cli 中本应下移的领域模块。""" - found: dict[str, str] = {} +def audit() -> list[dict]: + """返回真正错位的模块:命名像领域模块,且既不引用 runtime 也不引用 cli 本地类型。""" + out: list[dict] = [] for path in sorted(CLI_SRC.rglob("*.rs")): rel = path.relative_to(CLI_SRC).as_posix() - if rel in CLI_ALLOWED or any(rel.endswith(a) for a in CLI_ALLOWED): + stem = path.stem + if stem in ("main", "lib") or any(stem.endswith(a) for a in CLI_ALLOWED): continue - stem = rel[:-3] - if stem.endswith(CLI_ALLOWED[:-1]) or stem in ("main", "lib"): + if not any(stem.endswith(d) for d in DOMAIN_SHAPED): continue - for suffix, home in HOME_FOR.items(): - if stem.endswith(suffix): - found[rel] = home - break - return found - - -def load_baseline() -> dict: - if not BASELINE.is_file(): - return {"version": 1, "accepted": {}} - return json.loads(BASELINE.read_text(encoding="utf-8")) + text = path.read_text(encoding="utf-8", errors="replace") + if "codeguard_runtime" in text or CLI_LOCAL.search(text): + continue + out.append({"path": rel, "lines": len(text.splitlines())}) + return out def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="codeguard crate 分层门禁") - parser.add_argument("--explain", action="store_true", help="打印每个模块的归属理由") - parser.add_argument("--accept", metavar="NAME", help="把一个既有模块登记为已知债务") - parser.add_argument("--reason", metavar="TEXT", help="配合 --accept 记录理由") + parser.add_argument("--explain", action="store_true", help="打印判定依据") args = parser.parse_args(argv) - current = domain_modules() - baseline = load_baseline() - accepted: dict = baseline.get("accepted", {}) - - if args.accept: - if args.accept not in current: - print(f"{args.accept} 不在当前 cli 领域模块清单中,无需登记", file=sys.stderr) - return 2 - accepted[args.accept] = args.reason or "未记录理由" - baseline["accepted"] = accepted - BASELINE.write_text( - json.dumps(baseline, ensure_ascii=False, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - print(f"已登记 {args.accept} → {HOME_FOR.get(suffix_of(args.accept), 'codeguard-core')}") - return 0 + misplaced = audit() + current = {i["path"] for i in misplaced} if args.explain: - for name in sorted(current): - state = "已知债务" if name in accepted else "未登记(会被本检查拒绝)" - print(f"{name:52} → {current[name]:20} [{state}]") - if name in accepted: - print(f"{'':52} 理由:{accepted[name]}") + print("权威边界见 crates/codeguard-cli/tests/crate_boundaries.rs:") + print(" adapters 不得依赖 runtime,因此 *_probe 留在 cli 是正确的。\n") + if not misplaced: + print("当前 codeguard-cli 中没有真正错位的领域模块。") + for item in misplaced: + print(f" {item['path']:48} {item['lines']:5} 行 → 应下移到 adapters/core") return 0 - # 未登记的新领域模块 = 倒挂在扩大 - undeclared = sorted(set(current) - set(accepted)) - - if undeclared: - print("分层门禁失败:以下领域形态模块新增在 codeguard-cli 中。", file=sys.stderr) - print("它们应随工具适配或领域契约下移,而不是留在二进制 crate:\n", file=sys.stderr) - for name in undeclared: - print(f" {name} → 应归属 {current[name]}", file=sys.stderr) - print( - "\n处理方式:把模块移到对应 crate 并更新调用方;" - "若确有理由暂留,用 --accept 显式登记为已知债务。", - file=sys.stderr, - ) - return 1 - - # 已登记模块被移走了:基线应收紧,不留空壳额度 - removed = sorted(set(accepted) - set(current)) - stale = [n for n in removed if n in accepted] - - if stale: - print("分层门禁失败:以下已登记模块已不存在,请从 layer-baseline.json 移除,", file=sys.stderr) - print("以便额度随搬移自动收紧:\n", file=sys.stderr) - for name in stale: - print(f" {name}", file=sys.stderr) + if BASELINE.is_file(): + accepted = json.loads(BASELINE.read_text(encoding="utf-8")).get("accepted", {}) + stale = set(accepted) - current + if stale: + print("分层门禁失败:以下登记项已不再错位,请从 layer-baseline.json 移除," + "以免留下永不收紧的空额度:", file=sys.stderr) + for name in sorted(stale): + print(f" {name}", file=sys.stderr) + return 1 + new = sorted(current - set(accepted)) + if new: + print("分层门禁失败:以下模块既不需要 codeguard_runtime、也不依赖 cli 本地类型," + "属于被放错位置的领域逻辑:", file=sys.stderr) + for name in new: + print(f" {name}", file=sys.stderr) + return 1 + + if misplaced: + print("分层门禁失败:以下模块既不需要 runtime、也不依赖 cli 本地类型:", file=sys.stderr) + for item in misplaced: + print(f" {item['path']} ({item['lines']} 行)", file=sys.stderr) return 1 - print( - f"Layering OK: codeguard-cli 中已知领域债务 {len(current)} 个" - f"(全部已登记,无新增);core/adapters 未被 CLI 逻辑反向污染。" - ) + print("Layering OK: codeguard-cli 中没有真正错位的领域模块;" + "依赖 runtime 的探针留在 cli 符合 crate_boundaries 的既定方向。") return 0 -def suffix_of(name: str) -> str: - stem = name[:-3] if name.endswith(".rs") else name - for suffix in HOME_FOR: - if stem.endswith(suffix): - return suffix - return "" - - if __name__ == "__main__": raise SystemExit(main())