doc-drift.sh 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. #!/usr/bin/env bash
  2. # Doc-drift gate: documentation must describe what is actually on disk.
  3. #
  4. # Checks:
  5. # 1. Component counts on disk vs claims in README.md header, AGENTS.md
  6. # overview bullets, and docs/PLAN.md inventory table
  7. # 2. Every skill directory has a row in a README skill table
  8. # 3. Every repo-relative markdown link in README.md / AGENTS.md resolves
  9. # to an existing file or directory (no ghost references)
  10. #
  11. # Exit 0 = clean, exit 1 = drift detected.
  12. set -u
  13. ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
  14. cd "$ROOT" || exit 1
  15. errors=0
  16. err() { echo "DRIFT: $*"; errors=$((errors + 1)); }
  17. # --- 1. Counts on disk ------------------------------------------------------
  18. skills_disk=0
  19. for d in skills/*/; do
  20. [ -f "$d/SKILL.md" ] && skills_disk=$((skills_disk + 1))
  21. done
  22. agents_disk=$(find agents -maxdepth 1 -name '*.md' | wc -l)
  23. hooks_disk=$(find hooks -maxdepth 1 -name '*.sh' | wc -l)
  24. rules_disk=$(find rules -maxdepth 1 -name '*.md' | wc -l)
  25. styles_disk=$(find output-styles -maxdepth 1 -name '*.md' | wc -l)
  26. commands_disk=$(find commands -maxdepth 1 -name '*.md' | wc -l)
  27. echo "Disk: agents=$agents_disk skills=$skills_disk styles=$styles_disk hooks=$hooks_disk rules=$rules_disk commands=$commands_disk"
  28. # --- README header claim: "**N agents. N skills. N styles. N hooks. N rules. ...**"
  29. header="$(grep -oE '\*\*[0-9]+ agents\. [0-9]+ skills\. [0-9]+ styles\. [0-9]+ hooks\. [0-9]+ rules\.' README.md | head -1)"
  30. if [ -z "$header" ]; then
  31. err "README.md: count header line not found (expected '**N agents. N skills. ...**')"
  32. else
  33. read -r r_agents r_skills r_styles r_hooks r_rules <<< \
  34. "$(echo "$header" | grep -oE '[0-9]+' | tr '\n' ' ')"
  35. [ "$r_agents" = "$agents_disk" ] || err "README header: $r_agents agents claimed, $agents_disk on disk"
  36. [ "$r_skills" = "$skills_disk" ] || err "README header: $r_skills skills claimed, $skills_disk on disk"
  37. [ "$r_styles" = "$styles_disk" ] || err "README header: $r_styles styles claimed, $styles_disk on disk"
  38. [ "$r_hooks" = "$hooks_disk" ] || err "README header: $r_hooks hooks claimed, $hooks_disk on disk"
  39. [ "$r_rules" = "$rules_disk" ] || err "README header: $r_rules rules claimed, $rules_disk on disk"
  40. fi
  41. # --- AGENTS.md overview bullets ---------------------------------------------
  42. check_agents_md() { # $1=regex $2=disk-count $3=label
  43. local claim
  44. claim="$(grep -oE "$1" AGENTS.md | head -1 | grep -oE '[0-9]+')"
  45. if [ -z "$claim" ]; then
  46. err "AGENTS.md: no '$3' count bullet found"
  47. elif [ "$claim" != "$2" ]; then
  48. err "AGENTS.md: $claim $3 claimed, $2 on disk"
  49. fi
  50. }
  51. check_agents_md '\*\*[0-9]+ expert agents\*\*' "$agents_disk" "agents"
  52. check_agents_md '\*\*[0-9]+ skills\*\*' "$skills_disk" "skills"
  53. check_agents_md '\*\*[0-9]+ output styles\*\*' "$styles_disk" "output styles"
  54. check_agents_md '\*\*[0-9]+ hooks\*\*' "$hooks_disk" "hooks"
  55. check_agents_md '\*\*[0-9]+ commands\*\*' "$commands_disk" "commands"
  56. # --- docs/PLAN.md inventory table -------------------------------------------
  57. check_plan() { # $1=row-label $2=disk-count
  58. local claim
  59. claim="$(grep -E "^\| $1 \|" docs/PLAN.md | head -1 | awk -F'|' '{gsub(/ /,"",$3); print $3}')"
  60. if [ -n "$claim" ] && [ "$claim" != "$2" ]; then
  61. err "docs/PLAN.md: $1 = $claim claimed, $2 on disk"
  62. fi
  63. }
  64. check_plan "Agents" "$agents_disk"
  65. check_plan "Skills" "$skills_disk"
  66. check_plan "Commands" "$commands_disk"
  67. check_plan "Rules" "$rules_disk"
  68. check_plan "Output Styles" "$styles_disk"
  69. check_plan "Hooks" "$hooks_disk"
  70. # --- 2. Every skill has a README row ----------------------------------------
  71. for d in skills/*/; do
  72. n="$(basename "$d")"
  73. [ -f "$d/SKILL.md" ] || continue
  74. grep -q "skills/$n/" README.md || err "README.md: skill '$n' has no table row"
  75. done
  76. # --- 3. Ghost-link check (README.md + AGENTS.md) ----------------------------
  77. for doc in README.md AGENTS.md; do
  78. while IFS= read -r path; do
  79. path="${path%%#*}" # strip anchors
  80. [ -z "$path" ] && continue
  81. [ -e "$path" ] || err "$doc: link target does not exist: $path"
  82. done < <(grep -oE '\]\((skills|agents|hooks|rules|output-styles|commands|docs|tools|tests|scripts)/[^)]*\)' "$doc" \
  83. | sed -E 's/^\]\(//; s/\)$//')
  84. done
  85. echo
  86. if [ "$errors" -eq 0 ]; then
  87. echo "doc-drift: clean"
  88. exit 0
  89. else
  90. echo "doc-drift: $errors issue(s) found"
  91. exit 1
  92. fi