"""Validate documentation links and canonical baseline claims.""" from __future__ import annotations import argparse import json import re import subprocess import sys from pathlib import Path from urllib.parse import unquote REPO_ROOT = Path(__file__).resolve().parents[1] DOCS_ROOT = REPO_ROOT / "docs" ROADMAP_DOCS = ( DOCS_ROOT / "08-roadmap" / "TODO.md", DOCS_ROOT / "08-roadmap" / "ROADMAP.md", ) MARKDOWN_LINK = re.compile(r"(? tuple[Path, ...]: """Return tracked documentation files in stable order.""" return tuple(sorted(DOCS_ROOT.rglob("*.md"))) def check_markdown_links(files: tuple[Path, ...], root: Path = REPO_ROOT) -> list[str]: """Return one error for every missing local Markdown link target.""" root = root.resolve() errors: list[str] = [] for source in files: source = source.resolve() text = source.read_text(encoding="utf-8") for match in MARKDOWN_LINK.finditer(text): target = match.group(1).strip().strip("<>") target = target.split("#", 1)[0].strip() if not target or target.startswith( ("http://", "https://", "mailto:", "data:") ): continue candidate = (source.parent / unquote(target)).resolve() try: candidate.relative_to(root) except ValueError: errors.append(f"{source}: link escapes repository: {target}") continue if not candidate.exists(): errors.append(f"{source}: missing link target: {target}") return errors def check_baseline_version() -> list[str]: """Ensure the canonical roadmap baseline matches ``pyproject.toml``.""" pyproject = (REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8") version_match = re.search(r'^version\s*=\s*"([^"]+)"', pyproject, re.MULTILINE) if version_match is None: return ["pyproject.toml: canonical project version is missing"] expected = version_match.group(1) errors: list[str] = [] for path in ROADMAP_DOCS: text = path.read_text(encoding="utf-8") match = BASELINE_PATTERN.search(text) if match is None: errors.append(f"{path.relative_to(REPO_ROOT)}: baseline claim is missing") elif match.group(1) != expected: errors.append( f"{path.relative_to(REPO_ROOT)}: baseline {match.group(1)} " f"does not match {expected}" ) return errors def check_collected_test_count() -> list[str]: """Reject test-collection regressions relative to the canonical baseline. ``tests/test_baseline.json`` records a verified historical run. Adding new tests must not make documentation consistency fail merely because that immutable snapshot is older than the current tree. A lower current count, however, remains a regression and fails the gate. """ result = subprocess.run( [sys.executable, "-m", "pytest", "--collect-only", "-q"], cwd=REPO_ROOT, capture_output=True, text=True, check=False, ) output = f"{result.stdout}\n{result.stderr}" actual_match = COLLECTED_PATTERN.search(output) if actual_match is None: return ["pytest collection did not report a collected test count"] if result.returncode != 0: return ["pytest collection failed; a partial count is not a valid baseline"] baseline_path = REPO_ROOT / "tests" / "test_baseline.json" try: baseline = json.loads(baseline_path.read_text(encoding="utf-8")) declared = int(baseline["full_collection"]["collected"]) except (OSError, ValueError, KeyError, TypeError): return ["tests/test_baseline.json: canonical collected-test snapshot missing"] actual = int(actual_match.group(1)) if actual < declared: return [ "tests/test_baseline.json: test collection regressed " f"from baseline {declared} to {actual}" ] return [] def main() -> int: """Run documentation checks and return a shell-friendly status code.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--check-tests", action="store_true", help="also ensure pytest collection has not regressed below the baseline", ) args = parser.parse_args() errors = check_markdown_links(markdown_files()) errors.extend(check_baseline_version()) if args.check_tests: errors.extend(check_collected_test_count()) if errors: for error in errors: print(f"ERROR: {error}") return 1 print("Documentation consistency checks passed.") return 0 if __name__ == "__main__": raise SystemExit(main())