Files
Project-Kiln/scripts/verify_repo.py
T

127 lines
4.8 KiB
Python
Executable File

#!/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.22"]:
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", "target", ".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}")
if "check: verify audit" not in makefile:
fail("Makefile must expose the complete local check entrypoint")
hosted = [
*ROOT.glob(".github/workflows/*.yml"),
*ROOT.glob(".github/workflows/*.yaml"),
*ROOT.glob(".gitea/workflows/*.yml"),
*ROOT.glob(".gitea/workflows/*.yaml"),
]
if hosted:
fail(f"hosted workflows conflict with the local-only verification policy: {hosted}")
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()