#!/usr/bin/env python3 # file: scripts/audit_ksp_workspace_rules.py # version: 1 """Audit mechanically verifiable KSP-specific Rust/dependency boundaries.""" from __future__ import annotations import argparse import dataclasses import pathlib import re import sys import tomllib @dataclasses.dataclass(frozen=True) class Violation: """One KSP-specific workspace violation.""" code: str path: str line: int message: str def line_number(text: str, needle: str) -> int: """Return one-based line number for a substring.""" offset = text.find(needle) return 1 if offset < 0 else text[:offset].count("\n") + 1 def audit_manifests(root: pathlib.Path) -> list[Violation]: """Audit centralized third-party dependency ownership.""" violations: list[Violation] = [] workspace_manifest = root / "Cargo.toml" workspace = tomllib.loads(workspace_manifest.read_text(encoding="utf-8")) workspace_dependencies = set(workspace.get("workspace", {}).get("dependencies", {})) for manifest in sorted((root / "crates").glob("*/Cargo.toml")): text = manifest.read_text(encoding="utf-8") data = tomllib.loads(text) relative = manifest.relative_to(root).as_posix() for section in ("dependencies", "dev-dependencies", "build-dependencies"): values = data.get(section, {}) for name, value in values.items(): if isinstance(value, str): violations.append(Violation("KSP-DEP-101", relative, line_number(text, name), f"third-party `{name}` must be owned by [workspace.dependencies] and consumed with workspace = true")) continue if not isinstance(value, dict): continue if "path" in value: continue if name not in workspace_dependencies: violations.append(Violation("KSP-DEP-102", relative, line_number(text, name), f"`{name}` is missing from root [workspace.dependencies]")) if value.get("workspace") is not True: violations.append(Violation("KSP-DEP-103", relative, line_number(text, name), f"`{name}` must use workspace = true in member manifest")) if "version" in value: violations.append(Violation("KSP-DEP-104", relative, line_number(text, name), f"member dependency `{name}` must not own a version")) return violations def audit_source_boundaries(root: pathlib.Path) -> list[Violation]: """Audit central KSP ownership boundaries visible directly in Rust sources.""" violations: list[Violation] = [] for path in sorted((root / "crates").glob("*/src/**/*.rs")): relative = path.relative_to(root).as_posix() crate_name = path.relative_to(root / "crates").parts[0] for idx, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): if crate_name != "ksp-logging-lib" and re.search(r"\btracing::", line) and "tauri_plugin_tracing" not in line: violations.append(Violation("KSP-TRACE-101", relative, idx, "behavioral crates must use ksp-logging-lib rather than tracing directly")) if crate_name != "ksp-logging-lib" and "ksp_logging_lib::tracing::" in line: violations.append(Violation("KSP-TRACE-104", relative, idx, "the hidden Logging tracing bridge is reserved for exported KSP macros")) if crate_name != "ksp-config-lib" and re.search(r"\bstd::env::(?:var|var_os|vars|vars_os|set_var|remove_var)\b", line): violations.append(Violation("KSP-CONFIG-101", relative, idx, "only ksp-config-lib may access KSP process environment directly")) if crate_name != "ksp-core-lib" and "solana_pubkey::" in line: violations.append(Violation("KSP-SOL-101", relative, idx, "consume Pubkey through ksp-core-lib rather than solana-pubkey directly")) if crate_name != "ksp-wallet-lib" and "solana_keypair::" in line: violations.append(Violation("KSP-SOL-102", relative, idx, "solana-keypair is Wallet-owned and must not leak into other crates")) return violations def audit_tracing_targets(root: pathlib.Path) -> list[Violation]: """Require crate-root canonical tracing targets in behavioral libraries.""" violations: list[Violation] = [] for manifest in sorted((root / "crates").glob("*/Cargo.toml")): crate = manifest.parent name = tomllib.loads(manifest.read_text(encoding="utf-8")).get("package", {}).get("name") if not isinstance(name, str) or name == "ksp-core-lib": continue source_files = sorted((crate / "src").rglob("*.rs")) if not any("TRACING_TARGET" in path.read_text(encoding="utf-8") for path in source_files): continue constants = crate / "src/constants.rs" if constants.is_file(): expected = f'pub(crate) const TRACING_TARGET: &str = "{name}";' if expected not in constants.read_text(encoding="utf-8"): violations.append(Violation("KSP-TRACE-102", constants.relative_to(root).as_posix(), 1, f"expected canonical `{expected}`")) root_file = crate / "src/lib.rs" if not root_file.is_file(): root_file = crate / "src/main.rs" if root_file.is_file() and constants.is_file() and "pub(crate) use self::constants::TRACING_TARGET;" not in root_file.read_text(encoding="utf-8"): violations.append(Violation("KSP-TRACE-103", root_file.relative_to(root).as_posix(), 1, "TRACING_TARGET must be re-exported at crate root")) return violations def main() -> int: """Run KSP-specific workspace audits.""" parser = argparse.ArgumentParser() parser.add_argument("--root", default=".") parser.add_argument("--report-only", action="store_true") arguments = parser.parse_args() root = pathlib.Path(arguments.root).resolve() violations = audit_manifests(root) + audit_source_boundaries(root) + audit_tracing_targets(root) violations.sort(key=lambda item: (item.code, item.path, item.line, item.message)) if not violations: sys.stdout.write("KSP workspace Rust rule audit: clean\n") return 0 sys.stdout.write(f"KSP workspace Rust rule audit: {len(violations)} violation(s)\n") for item in violations: sys.stdout.write(f"{item.code} {item.path}:{item.line}: {item.message}\n") return 0 if arguments.report_only else 1 if __name__ == "__main__": raise SystemExit(main())