131 lines
6.4 KiB
Python
Executable File
131 lines
6.4 KiB
Python
Executable File
#!/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())
|