128 lines
5.2 KiB
Python
Executable File
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())
|