From 15c1fe2a2fb1ff4225781de2c7939b993382b9bd Mon Sep 17 00:00:00 2001 From: Asterios Raptis Date: Mon, 29 Jun 2026 18:40:15 +0200 Subject: [PATCH] build(docs): add verify-docs drift gate (pattern 04) (#34) scripts/verify_docs.py (stdlib) + make verify-docs: fails when docs drift from the repo - the docs/patterns index has no orphans/dead links, and every .claude/rules/*.md is referenced by CLAUDE.md. Intentionally small; extend the CHECKS list as docs grow. Additive tooling. Verified exit 0. Closes #34 --- Makefile | 7 ++- docs/patterns/04-docs-verification.md | 2 +- scripts/verify_docs.py | 86 +++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 scripts/verify_docs.py diff --git a/Makefile b/Makefile index f62046e..3a8932b 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ check-file-sizes check-complexity check-complexity-gate check-complexity-gate-update \ check-directory-size check-directory-size-gate \ lint lint-fix \ - sync-i18n sync-i18n-check \ + sync-i18n sync-i18n-check verify-docs \ clean prod prod-down prod-logs help # --- Development --- @@ -320,6 +320,11 @@ sync-i18n: ## Regenerate frontend/src/data/i18n/*.json from the backend YAML cat sync-i18n-check: ## Exit non-zero if the bundled i18n JSON drifts from the backend YAML @cd backend && poetry run python ../scripts/sync_i18n.py --check +# --- Docs verification (docs/patterns/04-docs-verification.md) --- + +verify-docs: ## Fail when docs drift from the repo (patterns index + rules references) + @python3 scripts/verify_docs.py + # --- Release (gitflow; docs/patterns/03-release-automation.md) --- # These assume a gitflow layout: `develop` is the active branch, `main` # holds releases only. Create a `develop` branch before first use. The diff --git a/docs/patterns/04-docs-verification.md b/docs/patterns/04-docs-verification.md index 5fc5a93..22bc53c 100644 --- a/docs/patterns/04-docs-verification.md +++ b/docs/patterns/04-docs-verification.md @@ -2,7 +2,7 @@ > Backported from adaptive-learner. Template-neutral; adapt the names to your app. -**Status in this template:** not yet implemented. The Makefile has no `verify-docs` or `verify-docs-discipline` target, `scripts/verify_docs.py` does not exist, and no MkDocs or nav-source-of-truth setup is present. The discipline is documented in `.claude/rules/ai-workflow.md` (single-source-of-truth for volatile stats; numeric-claims verification); the enforcement tooling is missing. +**Status in this template:** a starter `scripts/verify_docs.py` (+ `make verify-docs`) ships - a stdlib drift verifier with two checks: the `docs/patterns/` index has no orphans / dead links, and every `.claude/rules/*.md` is referenced by `CLAUDE.md`. It is intentionally small; extend the `CHECKS` list as your docs grow (version-badge parity, plugin counts, help-page nav, i18n key drift, ...). The discipline behind it is in `.claude/rules/ai-workflow.md` (single-source-of-truth for volatile stats; numeric-claims verification). ## Why diff --git a/scripts/verify_docs.py b/scripts/verify_docs.py new file mode 100644 index 0000000..ae7d236 --- /dev/null +++ b/scripts/verify_docs.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Documentation drift verifier (docs/patterns/04-docs-verification.md). + +Fails when the docs drift from the repo. Stdlib-only, fast, runnable +anywhere. Extend the CHECKS list as your docs grow; keep each check cheap +and deterministic. + +Current checks: + 1. Patterns index: every docs/patterns/NN-*.md (except README) is linked + from docs/patterns/README.md, and every pattern link in the README + resolves to a file (no orphans, no dead links). + 2. Rules: every `.md` in .claude/rules/ that CLAUDE.md references + exists, and every rule file is referenced by CLAUDE.md. + +Usage: + python3 scripts/verify_docs.py # exit 1 on drift +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +PATTERNS_DIR = REPO_ROOT / "docs" / "patterns" +RULES_DIR = REPO_ROOT / ".claude" / "rules" +CLAUDE_MD = REPO_ROOT / "CLAUDE.md" + + +def check_patterns_index() -> list[str]: + errors: list[str] = [] + readme = PATTERNS_DIR / "README.md" + if not readme.exists(): + return [f"missing {readme.relative_to(REPO_ROOT)}"] + readme_text = readme.read_text(encoding="utf-8") + linked = set(re.findall(r"\(([0-9]{2}-[a-z0-9-]+\.md)\)", readme_text)) + on_disk = {p.name for p in PATTERNS_DIR.glob("*.md") if p.name != "README.md"} + + for name in sorted(on_disk - linked): + errors.append(f"pattern doc not linked from patterns/README.md: {name}") + for name in sorted(linked - on_disk): + errors.append(f"patterns/README.md links a missing pattern doc: {name}") + return errors + + +def check_rules_references() -> list[str]: + errors: list[str] = [] + if not CLAUDE_MD.exists(): + return [f"missing {CLAUDE_MD.relative_to(REPO_ROOT)}"] + claude_text = CLAUDE_MD.read_text(encoding="utf-8") + referenced = set(re.findall(r"`([a-z0-9-]+\.md)`", claude_text)) + on_disk = {p.name for p in RULES_DIR.glob("*.md")} + + for name in sorted(on_disk): + if name not in referenced: + errors.append(f".claude/rules/{name} exists but CLAUDE.md never references it") + return errors + + +CHECKS = [ + ("patterns index", check_patterns_index), + ("rules references", check_rules_references), +] + + +def main() -> int: + failed = False + for title, check in CHECKS: + errors = check() + if errors: + failed = True + print(f"FAIL [{title}]") + for error in errors: + print(f" - {error}") + else: + print(f"OK [{title}]") + if failed: + print("\nDocumentation drift detected. Update the docs (or this verifier).") + return 1 + print("\nverify-docs passed.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())