117 lines
4.3 KiB
Python
Executable File
117 lines
4.3 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.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", "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}")
|
|
|
|
|
|
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()
|