Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/patterns/04-docs-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
86 changes: 86 additions & 0 deletions scripts/verify_docs.py
Original file line number Diff line number Diff line change
@@ -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 `<name>.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())