v0.2.5-pre.010.fix.001
This commit is contained in:
130
scripts/audit_ksp_workspace_rules.py
Executable file
130
scripts/audit_ksp_workspace_rules.py
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user