Blacksite/scripts/codex/architecture_audit.py

128 lines
5.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Enforce Blacksite's module-size debt ratchet."""
from __future__ import annotations
import argparse
import tempfile
import tomllib
from pathlib import Path
def nonblank_lines(path: Path) -> int:
return sum(1 for line in path.read_text(encoding="utf-8").splitlines() if line.strip())
def module_limit(relative: str, policy: dict) -> int:
if relative in policy.get("shells", {}):
return int(policy["limits"]["ui_shell"])
if relative.startswith("crates/editor/src/ui/"):
return int(policy["limits"]["ui_module"])
return int(policy["limits"]["production_rust_module"])
def validate_exception(entry: dict) -> list[str]:
required = ("path", "issue", "rationale", "maximum", "extraction_target", "expiry_milestone")
missing = [name for name in required if not entry.get(name)]
errors = [f"invalid exception for {entry.get('path', '<unknown>')}: missing {name}" for name in missing]
if entry.get("issue") and not str(entry["issue"]).startswith("#"):
errors.append(f"invalid exception for {entry['path']}: issue must be a tracker reference")
return errors
def audit(root: Path, policy: dict) -> tuple[list[str], list[str]]:
errors: list[str] = []
notes: list[str] = []
baselines = {str(path): int(value) for path, value in policy.get("baselines", {}).items()}
exceptions = {entry.get("path"): entry for entry in policy.get("exceptions", [])}
for entry in policy.get("exceptions", []):
errors.extend(validate_exception(entry))
for path in sorted((root / "crates").glob("**/*.rs")):
relative = path.relative_to(root).as_posix()
count = nonblank_lines(path)
limit = module_limit(relative, policy)
baseline = baselines.get(relative)
exception = exceptions.get(relative)
if baseline is not None and count > baseline:
errors.append(f"{relative}: {count} nonblank lines exceeds frozen baseline {baseline}")
continue
if baseline is not None and count < baseline:
notes.append(f"{relative}: shrank from baseline {baseline} to {count}")
if count <= limit:
continue
if baseline is not None:
continue
if exception is not None and count <= int(exception["maximum"]):
notes.append(f"{relative}: temporary {exception['issue']} exception ({count}/{exception['maximum']})")
continue
errors.append(f"{relative}: {count} nonblank lines exceeds module budget {limit}")
return errors, notes
def load_policy(path: Path) -> dict:
with path.open("rb") as handle:
policy = tomllib.load(handle)
if policy.get("version") != 1:
raise SystemExit(f"unsupported architecture policy version in {path}")
return policy
def self_test() -> int:
with tempfile.TemporaryDirectory(prefix="blacksite-architecture-audit-") as directory:
root = Path(directory)
(root / "crates/editor/src/ui").mkdir(parents=True)
policy = {
"limits": {"ui_shell": 2, "ui_module": 3, "production_rust_module": 4},
"shells": {"crates/editor/src/ui/shell.rs": 2},
"baselines": {"crates/editor/src/ui/legacy.rs": 5},
"exceptions": [],
}
(root / "crates/editor/src/ui/new.rs").write_text("a\nb\nc\nd\n", encoding="utf-8")
errors, _ = audit(root, policy)
assert any("new.rs" in error for error in errors)
(root / "crates/editor/src/ui/new.rs").write_text("a\nb\n", encoding="utf-8")
(root / "crates/editor/src/ui/legacy.rs").write_text("a\nb\nc\nd\ne\nf\n", encoding="utf-8")
errors, _ = audit(root, policy)
assert any("frozen baseline" in error for error in errors)
(root / "crates/editor/src/ui/legacy.rs").write_text("a\nb\nc\nd\n", encoding="utf-8")
errors, notes = audit(root, policy)
assert not errors and any("shrank" in note for note in notes)
policy["exceptions"] = [{
"path": "crates/editor/src/ui/new.rs", "issue": "#1", "rationale": "test",
"maximum": 4, "extraction_target": "test module", "expiry_milestone": "M2",
}]
(root / "crates/editor/src/ui/new.rs").write_text("a\nb\nc\nd\n", encoding="utf-8")
errors, _ = audit(root, policy)
assert not errors
policy["exceptions"][0]["issue"] = "missing-prefix"
errors, _ = audit(root, policy)
assert any("tracker reference" in error for error in errors)
print("PASS architecture-audit self-test")
return 0
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("command", choices=("check", "self-test"), nargs="?", default="check")
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[2])
arguments = parser.parse_args()
if arguments.command == "self-test":
return self_test()
root = arguments.root.resolve()
policy = load_policy(root / ".codex/architecture.toml")
errors, notes = audit(root, policy)
for note in notes:
print(f"NOTE {note}")
if errors:
for error in errors:
print(f"FAIL {error}")
return 1
print("PASS architecture-audit")
return 0
if __name__ == "__main__":
raise SystemExit(main())