#!/usr/bin/env python3 """Dependency-free Phase 0 repository and specification checks.""" from __future__ import annotations import csv import re import sys import tomllib from urllib.parse import unquote from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SPEC = ROOT / "docs" / "spec" def fail(message: str) -> None: print(f"FAIL: {message}", file=sys.stderr) raise SystemExit(1) def check_toml() -> None: manifests = [ROOT / "Cargo.toml", *sorted((ROOT / "crates").glob("*/Cargo.toml"))] for manifest in manifests: with manifest.open("rb") as stream: tomllib.load(stream) toolchain = tomllib.loads((ROOT / "rust-toolchain.toml").read_text()) if toolchain["toolchain"]["channel"] != "1.97.1": fail("Rust toolchain is not pinned to 1.97.1") def check_spec() -> set[str]: expected = {f"0{number}_" for number in range(8)} files = sorted(SPEC.glob("0[0-7]_*.md")) prefixes = {path.name[:3] for path in files} if prefixes != expected: fail(f"active spec set is incomplete: {sorted(prefixes)}") combined = "\n".join(path.read_text() for path in files) requirements = re.findall(r"^\| (R-\d{3})\s+\|", combined, re.MULTILINE) decisions = re.findall(r"^\| (D-\d{3})\s+\|", combined, re.MULTILINE) for label, identifiers in (("requirement", requirements), ("decision", decisions)): duplicates = sorted({item for item in identifiers if identifiers.count(item) > 1}) if duplicates: fail(f"duplicate {label} IDs: {duplicates}") versions = re.findall(r"^\| \*\*Version\*\*\s+\| ([0-9.]+)", combined, re.MULTILINE) if versions != ["0.18"]: fail(f"active specification version markers are stale or ambiguous: {versions}") return set(requirements) def check_traceability(required: set[str]) -> None: path = ROOT / "docs" / "phase-0" / "traceability.csv" with path.open(newline="") as stream: rows = list(csv.DictReader(stream)) traced = {row["requirement_id"] for row in rows} missing = sorted(required - traced) if missing: fail(f"requirements missing from traceability matrix: {missing}") def check_markdown_links() -> None: broken: list[str] = [] for document in ROOT.rglob("*.md"): if any(part in {".git", "target"} for part in document.relative_to(ROOT).parts): continue for destination in re.findall(r"\[[^]]*\]\(([^)]+)\)", document.read_text()): target = destination.split("#", 1)[0] if not target or "://" in target or target.startswith("mailto:"): continue resolved = (document.parent / unquote(target)).resolve() if not resolved.exists(): broken.append(f"{document.relative_to(ROOT)} -> {destination}") if broken: fail(f"broken Markdown links: {broken}") def check_placeholders() -> None: allowed_roots = {"crates", "docs", "scripts", ".github"} for child in ROOT.iterdir(): if child.is_dir() and not child.name.startswith(".") and child.name not in allowed_roots: fail(f"unexpected top-level directory: {child.name}") manifest = (ROOT / "docs" / "phase-0" / "rename-manifest.md").read_text() for token in ("kiln", "KILN", "Project Kiln"): if token not in manifest: fail(f"rename manifest does not track {token!r}") def check_verification_entrypoints() -> None: makefile = (ROOT / "Makefile").read_text() required = ( "python3 scripts/verify_repo.py", "python3 scripts/audit_workspace.py", "python3 scripts/scan_placeholders.py", "python3 scripts/verify_rust_toolchain.py", "cargo fmt --all --check", "cargo test --workspace --all-targets --locked", "cargo clippy --workspace --all-targets --locked -- -D warnings", "cargo deny check advisories bans licenses sources", ) missing = [command for command in required if command not in makefile] if missing: fail(f"Makefile is missing verification gates: {missing}") def main() -> None: check_toml() requirements = check_spec() check_traceability(requirements) check_markdown_links() check_placeholders() check_verification_entrypoints() print(f"PASS: Phase 0 static baseline ({len(requirements)} requirements traced)") if __name__ == "__main__": main()