verify-commands.sh 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. #!/usr/bin/env bash
  2. # Staleness verifier for ffmpeg-ops docs — offline structural + live build-drift.
  3. #
  4. # --offline (default): structural integrity, NO ffmpeg needed. Assets parse as
  5. # JSON, every reference/script/asset on disk is cited from SKILL.md, and every
  6. # relative link in SKILL.md resolves. Runs in PR CI; may block.
  7. # --live: does the documentation still match an actual ffmpeg? Extracts the
  8. # encoders/filters the docs rely on and checks them against the INSTALLED
  9. # build (`-encoders`/`-filters`/`-h full`). Core items missing = drift
  10. # (exit 10); build-optional items (libx265, libvmaf, ...) only warn.
  11. # Runs in the scheduled freshness workflow; never blocks a PR.
  12. #
  13. # Usage: verify-commands.sh [--offline | --live] [-q]
  14. # Input: none (inspects the skill's own files; --live also the ffmpeg on PATH)
  15. # Output: stdout = findings (one per line, "DRIFT:" / "STRUCT:" prefixed)
  16. # Stderr: progress, warnings
  17. # Exit: 0 clean, 2 usage, 7 ffmpeg unavailable (--live only; advisory),
  18. # 10 drift/structural finding
  19. #
  20. # Examples:
  21. # verify-commands.sh --offline
  22. # verify-commands.sh --live
  23. # verify-commands.sh --live -q; echo "exit=$?"
  24. set -uo pipefail
  25. EXIT_OK=0; EXIT_USAGE=2; EXIT_UNAVAILABLE=7; EXIT_DRIFT=10
  26. MODE="offline"; QUIET=0
  27. while [[ $# -gt 0 ]]; do
  28. case "$1" in
  29. --offline) MODE="offline" ;;
  30. --live) MODE="live" ;;
  31. -q|--quiet) QUIET=1 ;;
  32. -h|--help) sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit "$EXIT_OK" ;;
  33. *) echo "ERROR: unknown argument: $1 (try --help)" >&2; exit "$EXIT_USAGE" ;;
  34. esac
  35. shift
  36. done
  37. SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
  38. SKILL_MD="$SKILL_DIR/SKILL.md"
  39. findings=0
  40. emit() { [[ "$QUIET" -eq 1 ]] || printf '%s\n' "$1" >&2; }
  41. finding() { printf '%s\n' "$1"; findings=$((findings + 1)); }
  42. # Pick a working python for JSON validation (Windows Store stub exits non-zero).
  43. PYTHON=""
  44. for c in python3 python py; do
  45. if command -v "$c" >/dev/null 2>&1 && "$c" -c "" >/dev/null 2>&1; then PYTHON="$c"; break; fi
  46. done
  47. # ── offline: structural ──────────────────────────────────────────────────────
  48. offline_checks() {
  49. emit "== verify-commands --offline (structural)"
  50. [[ -f "$SKILL_MD" ]] || { finding "STRUCT: SKILL.md missing"; return; }
  51. # 1. assets parse as JSON
  52. for a in "$SKILL_DIR"/assets/*.json; do
  53. [[ -e "$a" ]] || continue
  54. if [[ -n "$PYTHON" ]]; then
  55. "$PYTHON" -c "import json,sys; json.load(open(sys.argv[1], encoding='utf-8'))" "$a" \
  56. >/dev/null 2>&1 || finding "STRUCT: asset not valid JSON: $(basename "$a")"
  57. elif command -v jq >/dev/null 2>&1; then
  58. jq empty "$a" >/dev/null 2>&1 || finding "STRUCT: asset not valid JSON: $(basename "$a")"
  59. fi
  60. done
  61. # 2. every shipped resource is cited from SKILL.md (dead weight check)
  62. for d in references scripts assets; do
  63. for f in "$SKILL_DIR/$d"/*; do
  64. [[ -f "$f" ]] || continue
  65. base="$(basename "$f")"
  66. [[ "$base" == ".gitkeep" ]] && continue
  67. grep -q "$base" "$SKILL_MD" \
  68. || finding "STRUCT: $d/$base exists on disk but is never cited from SKILL.md"
  69. done
  70. done
  71. # 3. every relative resource link in SKILL.md resolves
  72. while IFS= read -r path; do
  73. [[ -e "$SKILL_DIR/$path" ]] \
  74. || finding "STRUCT: SKILL.md links to missing file: $path"
  75. done < <(grep -oE '\]\((references|assets|scripts|tests)/[^)#]+\)' "$SKILL_MD" \
  76. | sed -E 's/^\]\(//; s/\)$//' | sort -u)
  77. }
  78. # ── live: docs vs the installed build ────────────────────────────────────────
  79. live_checks() {
  80. emit "== verify-commands --live (installed-build drift)"
  81. if ! command -v ffmpeg >/dev/null 2>&1; then
  82. echo "ffmpeg not on PATH — live check unavailable (advisory, not a failure)" >&2
  83. exit "$EXIT_UNAVAILABLE"
  84. fi
  85. local encoders filters hfull docs
  86. encoders="$(ffmpeg -hide_banner -encoders 2>/dev/null)"
  87. filters="$(ffmpeg -hide_banner -filters 2>/dev/null)"
  88. hfull="$(ffmpeg -hide_banner -h full 2>/dev/null)"
  89. docs="$(cat "$SKILL_MD" "$SKILL_DIR"/references/*.md 2>/dev/null)"
  90. # Filters that exist in EVERY ffmpeg build — absence means the filter was
  91. # renamed/removed upstream, i.e. our docs drifted.
  92. local core_filters=(scale crop pad fps overlay concat setpts atempo amix
  93. silencedetect silenceremove loudnorm palettegen paletteuse
  94. select tile transpose trim atrim split format)
  95. # Build-optional (external libs / hw): warn only.
  96. local optional_tokens=(libx264 libx265 libsvtav1 libaom-av1 libvpx-vp9 libopus
  97. libmp3lame drawtext subtitles lut3d zscale tonemap
  98. libvmaf minterpolate vidstabdetect vidstabtransform
  99. bwdif hqdn3d nlmeans xstack showwaves showspectrum
  100. colorbalance colortemperature colorchannelmixer
  101. colorhold vibrance haldclut chromashift)
  102. # CLI options the cookbook depends on; renamed/removed = drift (-vsync class).
  103. local core_options=(fps_mode movflags avoid_negative_ts map_metadata
  104. filter_complex frames pix_fmt)
  105. # NOTE: the flags column width varies across ffmpeg majors (3 chars <=7.x,
  106. # 2 chars in 8.x) — match any flag run, then the exact filter name token.
  107. for f in "${core_filters[@]}"; do
  108. grep -qE "^ +[A-Z.|]+ +$f +" <<<"$filters" \
  109. || finding "DRIFT: core filter '$f' not in installed ffmpeg (renamed/removed upstream?)"
  110. done
  111. for opt in "${core_options[@]}"; do
  112. grep -q -- "-$opt" <<<"$hfull" \
  113. || finding "DRIFT: documented option '-$opt' unknown to installed ffmpeg"
  114. done
  115. # Every software encoder the docs name must at least be a known encoder name
  116. # in this build — missing here is a warning (build config), not drift, EXCEPT
  117. # the universal natives (aac, ffv1) which every build ships.
  118. for enc in aac ffv1; do
  119. grep -qE "^ [A-Z.]{6} +$enc " <<<"$encoders" \
  120. || finding "DRIFT: native encoder '$enc' not in installed ffmpeg"
  121. done
  122. for tok in "${optional_tokens[@]}"; do
  123. if grep -qF "$tok" <<<"$docs"; then
  124. grep -qE "(^ [A-Z.]{6} +$tok )|(^ +[A-Z.|]+ +$tok +)" <<<"$encoders"$'\n'"$filters" \
  125. || emit " warn: '$tok' documented but absent from this build (build-optional — not drift)"
  126. fi
  127. done
  128. # Deprecated-flag tripwire: docs must not RECOMMEND -vsync (mentioning it as
  129. # deprecated in footgun tables is fine; a code fence using it is not).
  130. if grep -E '^\s*ffmpeg .*-vsync ' <<<"$docs" | grep -vq 'fps_mode'; then
  131. finding "DRIFT: a documented command still uses deprecated -vsync (use -fps_mode)"
  132. fi
  133. }
  134. case "$MODE" in
  135. offline) offline_checks ;;
  136. live) live_checks ;;
  137. esac
  138. if [[ "$findings" -eq 0 ]]; then
  139. emit "verify-commands ($MODE): clean"
  140. exit "$EXIT_OK"
  141. fi
  142. emit "verify-commands ($MODE): $findings finding(s)"
  143. exit "$EXIT_DRIFT"