#!/usr/bin/env bash # scripts/quality/check-doc-freshness.sh # # Cheap, string-only documentation-freshness gate. Catches the cross-file # drift that a "documentation pass" (see doc/release-policy.md) is meant to # prevent — without building anything, so it runs in well under a second. # # Invariants checked: # 1. Release version agrees across CITATION.cff, CHANGELOG.md (newest # section) and CLAUDE.md ("Current release:" line). # 2. Release date agrees across CITATION.cff and CHANGELOG.md. # 3. The "N documents across ..." count in CLAUDE.md's Documentation map # equals the actual number of doc/**/*.md files. # # NOT checked here (they need a build — run them as part of the release # documentation pass, see doc/release-policy.md): # * doc/api/tests.md counts → scripts/check-test-counts.sh (needs BUILD_DIR) # * doc/api/headers.md regeneration → scripts/gen-headers-md.py (needs Doxygen XML) # # Usage: # bash scripts/quality/check-doc-freshness.sh # # Exit codes: # 0 all string invariants hold # 1 at least one drift found # 2 prerequisite missing (a required file is absent) set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$ROOT" || exit 2 for f in CITATION.cff CHANGELOG.md CLAUDE.md; do if [ ! -f "$f" ]; then echo "FAIL: required file not found: $f" >&2 exit 2 fi done fail=0 note() { printf ' %-4s %s\n' "$1" "$2"; } # Strip an optional leading "v" so "v0.10.0" and "0.10.0" compare equal. strip_v() { printf '%s' "${1#v}"; } # ── Invariant 1 + 2: version & date triple ────────────────────────────────── cit_ver=$(strip_v "$(grep -E '^version:' CITATION.cff | head -1 | awk '{print $2}')") cit_date=$(grep -E '^date-released:' CITATION.cff | head -1 | awk '{print $2}') chg_header=$(grep -m1 -E '^## \[' CHANGELOG.md) chg_ver=$(strip_v "$(printf '%s' "$chg_header" | sed -E 's/^## \[([^]]+)\].*/\1/')") chg_date=$(printf '%s' "$chg_header" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' | head -1) claude_ver=$(strip_v "$(grep -m1 'Current release:' CLAUDE.md | sed -E 's/.*\*\*(v?[0-9][0-9.]*)\*\*.*/\1/')") echo "── version / date consistency ──" note "CIT" "CITATION.cff version=$cit_ver date=$cit_date" note "CHG" "CHANGELOG.md version=$chg_ver date=$chg_date" note "CLD" "CLAUDE.md version=$claude_ver" if [ "$cit_ver" = "$chg_ver" ] && [ "$cit_ver" = "$claude_ver" ]; then note "OK" "version agrees ($cit_ver)" else note "DRIFT" "version mismatch: CITATION=$cit_ver CHANGELOG=$chg_ver CLAUDE=$claude_ver" fail=1 fi if [ "$cit_date" = "$chg_date" ]; then note "OK" "date agrees ($cit_date)" else note "DRIFT" "date mismatch: CITATION=$cit_date CHANGELOG=$chg_date" fail=1 fi # ── Invariant 3: documentation-map count ──────────────────────────────────── stated=$(grep -m1 -E '^[0-9]+ documents across' CLAUDE.md | awk '{print $1}') actual=$(find doc -name '*.md' -type f | wc -l | tr -d ' ') echo echo "── documentation-map count ──" note "CLD" "CLAUDE.md states: $stated documents" note "DISK" "doc/**/*.md on disk: $actual" if [ "$stated" = "$actual" ]; then note "OK" "doc count agrees ($actual)" else note "DRIFT" "CLAUDE.md says $stated, disk has $actual — update the 'N documents across' line" fail=1 fi echo if [ "$fail" -ne 0 ]; then echo "FAIL: documentation drift detected." echo "Recovery: run a documentation pass (see doc/release-policy.md →" echo " 'Documentation passes'); fix the lines above, then re-run." exit 1 fi echo "OK: documentation strings are consistent." echo "Reminder: at release also run scripts/check-test-counts.sh and" echo " scripts/gen-headers-md.py (both need a build)." exit 0