diff --git a/.claude/skills/gates/SKILL.md b/.claude/skills/gates/SKILL.md index 5dc0ec496d..0a597a59b7 100644 --- a/.claude/skills/gates/SKILL.md +++ b/.claude/skills/gates/SKILL.md @@ -24,7 +24,7 @@ Check these before believing any result. for exactly this reason — if installed packages do not match `package-lock.json`, treat any test, lint, or typecheck result as void until `npm ci` has run. Its own failure message says as much. - **`verify:cheap` stops at the first failing check.** Everything after that point never ran. Do not - describe the change as broadly verified when the gate died at check 2 of 31. + describe the change as broadly verified when the gate died at check 2 of 32. - **`format:check` is required in CI but is not part of `verify:cheap`.** A locally green `verify:cheap` can still fail CI on formatting. Run `npx prettier --write ` before pushing — scoped to your files, never `prettier --write .`, which sweeps the whole tree. diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000000..f8ad49755a --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,112 @@ +#!/bin/sh +# Keep generated documentation synchronized before relevant commits. The hook +# writes generated outputs but never stages them: if anything changes, the +# commit stops so the author can review and stage the exact diff. +set -eu + +if [ "${SKIP_DOCS_SYNC_HOOK:-}" = "1" ]; then + exit 0 +fi + +staged_files=$(git diff --cached --name-only --diff-filter=ACMRD) +working_files=$( + { + git diff --name-only --diff-filter=ACMRD + git ls-files --others --exclude-standard + } | sort -u +) + +matches_staged() { + printf '%s\n' "$staged_files" | grep -Eq "$1" +} + +sync_sitemap=0 +sync_inventory=0 +check_module_map=0 + +if matches_staged '^(src/app/|mockups/|src/lib/(app-modes|document-flow-routes|differentials|dsm|formulation|forms|services|specifiers|therapies)\.ts$|scripts/generate-site-map\.ts$|docs/site-map\.md$)'; then + sync_sitemap=1 +fi +if matches_staged '^(scripts/|package\.json$|docs/scripts-index\.md$)'; then + sync_inventory=1 +fi +if matches_staged '^(src/app/|src/lib/|supabase/schema\.sql$|docs/codebase-index\.md$)'; then + check_module_map=1 +fi +if [ "$sync_sitemap" = "0" ] && [ "$sync_inventory" = "0" ] && [ "$check_module_map" = "0" ]; then + exit 0 +fi + +# Generators read the working tree. Refuse to combine staged inputs with other +# unstaged inputs that could make the generated docs describe the wrong commit. +mixed_inputs="" +if [ "$sync_sitemap" = "1" ]; then + mixed_inputs=$(printf '%s\n' "$working_files" | grep -E '^(src/app/|mockups/|src/lib/(app-modes|document-flow-routes|differentials|dsm|formulation|forms|services|specifiers|therapies)\.ts$|scripts/generate-site-map\.ts$)' || true) +fi +if [ "$sync_inventory" = "1" ]; then + inventory_inputs=$(printf '%s\n' "$working_files" | grep -E '^(scripts/|package\.json$)' || true) + mixed_inputs=$(printf '%s\n%s\n' "$mixed_inputs" "$inventory_inputs" | sed '/^$/d' | sort -u) +fi +if [ "$check_module_map" = "1" ]; then + module_inputs=$(printf '%s\n' "$working_files" | grep -E '^(src/app/|src/lib/|supabase/schema\.sql$)' || true) + mixed_inputs=$(printf '%s\n%s\n' "$mixed_inputs" "$module_inputs" | sed '/^$/d' | sort -u) +fi +if [ -n "$mixed_inputs" ]; then + echo "[pre-commit] Documentation inputs have unstaged or untracked changes:" >&2 + printf '%s\n' "$mixed_inputs" >&2 + echo "[pre-commit] Stage or separate these inputs before regenerating commit documentation." >&2 + exit 1 +fi + +docs_to_check="" +if [ "$sync_sitemap" = "1" ]; then + docs_to_check="$docs_to_check docs/site-map.md" +fi +if [ "$sync_inventory" = "1" ]; then + docs_to_check="$docs_to_check docs/scripts-index.md" +fi +if [ "$check_module_map" = "1" ]; then + docs_to_check="$docs_to_check docs/codebase-index.md" +fi + +# Generators rewrite whole files. Refuse before running them when a selected +# output has an unstaged edit, otherwise regeneration can erase local work and +# leave the hook looking clean afterward. +dirty_generated_docs=$(git diff --name-only -- $docs_to_check) +if [ -n "$dirty_generated_docs" ]; then + echo "[pre-commit] Generated documentation has unstaged changes:" >&2 + printf '%s\n' "$dirty_generated_docs" >&2 + echo "[pre-commit] Stage or separate these outputs before regeneration." >&2 + exit 1 +fi + +if ! command -v npm >/dev/null 2>&1; then + echo "[pre-commit] npm is required for documentation synchronization." >&2 + echo "[pre-commit] Install the repository toolchain, or bypass once with SKIP_DOCS_SYNC_HOOK=1." >&2 + exit 1 +fi + +repo_root=$(git rev-parse --show-toplevel) +cd "$repo_root" + +echo "[pre-commit] Synchronizing generated documentation..." +if [ "$sync_sitemap" = "1" ]; then + npm run sitemap:update +fi +if [ "$sync_inventory" = "1" ]; then + node scripts/update-docs-inventory.mjs + npm run docs:check-inventory +fi +if [ "$check_module_map" = "1" ]; then + npm run docs:check-index +fi + +unstaged_docs=$(git diff --name-only -- $docs_to_check) +if [ -n "$unstaged_docs" ]; then + echo "[pre-commit] Documentation changed or remains unstaged:" >&2 + printf '%s\n' "$unstaged_docs" >&2 + echo "[pre-commit] Review and stage these files, then commit again." >&2 + exit 1 +fi + +echo "[pre-commit] Documentation is synchronized." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d5e709600..58a5a5c97e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -226,6 +226,9 @@ jobs: - name: Codebase index coverage run: npm run docs:check-index + - name: Documentation inventory drift + run: npm run docs:check-inventory + - name: Check docs scripts run: npm run docs:check-scripts - name: Check docs links diff --git a/AGENTS.md b/AGENTS.md index 70b1ac6fc7..a7e074c985 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -201,8 +201,10 @@ action must perform one; a page that ships must be reachable. (`src/lib/app-modes.ts`, `src/lib/tools-catalog.ts`, `src/lib/universal-search.ts`), not hardcoded strings scattered across components. - **New-route checklist.** Add the page → link it from real nav (sidebar / launcher / mode home / - search) → `npm run sitemap:update` → document it in `docs/codebase-index.md` → add a + search) → `npm run docs:update` → document it in `docs/codebase-index.md` → add a reachability/coverage assertion. A production page route with no inbound link is an orphan. + The committed pre-commit hook runs this synchronization for relevant staged changes and stops + when generated docs need review/staging; it never stages files automatically. - **Gates.** `eslint-rules/require-button-wiring.mjs` (in `npm run lint`) fails on an un-wired `