#!/usr/bin/env python3 """Check current-state documentation references against source and the filesystem.""" from __future__ import annotations import argparse import re import sys import tempfile from pathlib import Path DOCS = ("README.md", "DESIGN.md", "AGENTS.md", "src/oauth/mod.rs") STALE_REFERENCES = ("cards.css", "001_projection_schema.sql") REQUIRED_ARCHITECTURE_TERMS = ( "SQLite", "appview projection", "Hydrant", "Fjall", "pending_writes", "pending_deletes", ) STYLE_DECLARATION = re.compile(r'asset!\("/assets/styling/([^"/]+\.css)"\)') REQUIRED_SOURCE_FILES = ("src/main.rs", "migrations/0001_initial_schema.sql") def fail(message: str) -> None: raise ValueError(message) def selected_documents(root: Path) -> list[tuple[Path, str]]: documents: list[tuple[Path, str]] = [] for relative in DOCS: path = root / relative if not path.is_file(): fail(f"missing documentation source: {relative}") documents.append((path, path.read_text(encoding="utf-8"))) return documents def check_paths(root: Path, documents: list[tuple[Path, str]]) -> None: source = root / "src/main.rs" if not source.is_file(): fail("missing source of truth: src/main.rs") declarations = STYLE_DECLARATION.findall(source.read_text(encoding="utf-8")) if not declarations: fail("src/main.rs has no stylesheet declarations") for stylesheet in declarations: if not (root / "assets/styling" / stylesheet).is_file(): fail(f"stylesheet declared by src/main.rs does not exist: {stylesheet}") for relative in REQUIRED_SOURCE_FILES: if not (root / relative).is_file(): fail(f"required current path does not exist: {relative}") for path, text in documents: if "assets/styling/" in text: for reference in re.findall(r"assets/styling/([A-Za-z0-9_.-]+\.css)", text): if not (root / "assets/styling" / reference).is_file(): fail(f"{path.relative_to(root)} references missing stylesheet: {reference}") for reference in re.findall(r"migrations/[A-Za-z0-9_.-]+\.sql", text): if not (root / reference).is_file(): fail(f"{path.relative_to(root)} references missing migration: {reference}") def check_current_maps(root: Path, documents: list[tuple[Path, str]]) -> None: text_by_name = {path.name: text for path, text in documents} architecture_text = "\n".join(text_by_name[name] for name in ("README.md", "AGENTS.md")) for term in REQUIRED_ARCHITECTURE_TERMS: if term not in architecture_text: fail(f"current architecture maps omit required term: {term}") source = (root / "src/main.rs").read_text(encoding="utf-8") expected_cascade = STYLE_DECLARATION.findall(source) agents = text_by_name["AGENTS.md"] cascade_line = next((line for line in agents.splitlines() if "layered cascade" in line), "") if not cascade_line: fail("AGENTS.md does not describe the stylesheet cascade") positions = [cascade_line.find(stylesheet) for stylesheet in expected_cascade] if any(position < 0 for position in positions) or positions != sorted(positions): fail("AGENTS.md does not describe the stylesheet cascade in src/main.rs order") def check_stale_references(documents: list[tuple[Path, str]]) -> None: for path, text in documents: for stale in STALE_REFERENCES: if stale in text: fail(f"{path.relative_to(path.parents[1]) if path.name == 'mod.rs' else path.name} contains stale reference: {stale}") def validate(root: Path) -> None: documents = selected_documents(root) check_paths(root, documents) check_current_maps(root, documents) check_stale_references(documents) def write_fixture(root: Path) -> None: (root / "src/oauth").mkdir(parents=True, exist_ok=True) (root / "assets/styling").mkdir(parents=True, exist_ok=True) (root / "migrations").mkdir(exist_ok=True) (root / "src/main.rs").write_text( "\n".join(f'const STYLE: Asset = asset!("/assets/styling/{name}");' for name in ( "theme.css", "base.css", "primitives.css", "shell.css", "home.css", "thing-detail.css", "profile.css", "viewer.css", "publish.css", "placeholders.css", "about.css", )), encoding="utf-8", ) for name in ( "theme.css", "base.css", "primitives.css", "shell.css", "home.css", "thing-detail.css", "profile.css", "viewer.css", "publish.css", "placeholders.css", "about.css", ): (root / "assets/styling" / name).write_text("", encoding="utf-8") (root / "migrations/0001_initial_schema.sql").write_text("", encoding="utf-8") (root / "README.md").write_text( "SQLite appview projection; Hydrant retains raw events in Fjall. " "pending_writes and pending_deletes define the pending-operation boundary.\n", encoding="utf-8", ) (root / "DESIGN.md").write_text("Use assets/styling/theme.css and primitives.css.\n", encoding="utf-8") (root / "AGENTS.md").write_text( "SQLite appview projection; Hydrant and Fjall; pending_writes and pending_deletes.\n" "assets/styling/ follows a layered cascade: " "theme.css base.css primitives.css shell.css home.css thing-detail.css profile.css " "viewer.css publish.css placeholders.css about.css\n", encoding="utf-8", ) (root / "src/oauth/mod.rs").write_text( "See migrations/0001_initial_schema.sql.\n", encoding="utf-8" ) def self_test() -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) write_fixture(root) validate(root) (root / "assets/styling/cards.css").write_text("", encoding="utf-8") (root / "DESIGN.md").write_text( "Use assets/styling/cards.css.\n", encoding="utf-8" ) try: validate(root) except ValueError: pass else: fail("negative stale-stylesheet fixture unexpectedly passed") write_fixture(root) (root / "migrations/001_projection_schema.sql").write_text("", encoding="utf-8") (root / "src/oauth/mod.rs").write_text( "See migrations/001_projection_schema.sql.\n", encoding="utf-8" ) try: validate(root) except ValueError: pass else: fail("negative stale-migration fixture unexpectedly passed") write_fixture(root) (root / "assets/styling/about.css").unlink() try: validate(root) except ValueError: pass else: fail("negative missing-stylesheet fixture unexpectedly passed") print("check-doc-references self-test passed") def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--self-test", action="store_true") parser.add_argument("root", nargs="?", type=Path, default=Path(__file__).resolve().parents[1]) args = parser.parse_args() try: if args.self_test: self_test() else: validate(args.root.resolve()) print(f"check-doc-references passed: {args.root.resolve()}") except (OSError, ValueError) as exc: print(f"check-doc-references failed: {exc}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())