#!/usr/bin/env bash # Staleness verifier for ffmpeg-ops docs — offline structural + live build-drift. # # --offline (default): structural integrity, NO ffmpeg needed. Assets parse as # JSON, every reference/script/asset on disk is cited from SKILL.md, and every # relative link in SKILL.md resolves. Runs in PR CI; may block. # --live: does the documentation still match an actual ffmpeg? Extracts the # encoders/filters the docs rely on and checks them against the INSTALLED # build (`-encoders`/`-filters`/`-h full`). Core items missing = drift # (exit 10); build-optional items (libx265, libvmaf, ...) only warn. # Runs in the scheduled freshness workflow; never blocks a PR. # # Usage: verify-commands.sh [--offline | --live] [-q] # Input: none (inspects the skill's own files; --live also the ffmpeg on PATH) # Output: stdout = findings (one per line, "DRIFT:" / "STRUCT:" prefixed) # Stderr: progress, warnings # Exit: 0 clean, 2 usage, 7 ffmpeg unavailable (--live only; advisory), # 10 drift/structural finding # # Examples: # verify-commands.sh --offline # verify-commands.sh --live # verify-commands.sh --live -q; echo "exit=$?" set -uo pipefail EXIT_OK=0; EXIT_USAGE=2; EXIT_UNAVAILABLE=7; EXIT_DRIFT=10 MODE="offline"; QUIET=0 while [[ $# -gt 0 ]]; do case "$1" in --offline) MODE="offline" ;; --live) MODE="live" ;; -q|--quiet) QUIET=1 ;; -h|--help) sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit "$EXIT_OK" ;; *) echo "ERROR: unknown argument: $1 (try --help)" >&2; exit "$EXIT_USAGE" ;; esac shift done SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SKILL_MD="$SKILL_DIR/SKILL.md" findings=0 emit() { [[ "$QUIET" -eq 1 ]] || printf '%s\n' "$1" >&2; } finding() { printf '%s\n' "$1"; findings=$((findings + 1)); } # Pick a working python for JSON validation (Windows Store stub exits non-zero). PYTHON="" for c in python3 python py; do if command -v "$c" >/dev/null 2>&1 && "$c" -c "" >/dev/null 2>&1; then PYTHON="$c"; break; fi done # ── offline: structural ────────────────────────────────────────────────────── offline_checks() { emit "== verify-commands --offline (structural)" [[ -f "$SKILL_MD" ]] || { finding "STRUCT: SKILL.md missing"; return; } # 1. assets parse as JSON for a in "$SKILL_DIR"/assets/*.json; do [[ -e "$a" ]] || continue if [[ -n "$PYTHON" ]]; then "$PYTHON" -c "import json,sys; json.load(open(sys.argv[1], encoding='utf-8'))" "$a" \ >/dev/null 2>&1 || finding "STRUCT: asset not valid JSON: $(basename "$a")" elif command -v jq >/dev/null 2>&1; then jq empty "$a" >/dev/null 2>&1 || finding "STRUCT: asset not valid JSON: $(basename "$a")" fi done # 2. every shipped resource is cited from SKILL.md (dead weight check) for d in references scripts assets; do for f in "$SKILL_DIR/$d"/*; do [[ -f "$f" ]] || continue base="$(basename "$f")" [[ "$base" == ".gitkeep" ]] && continue grep -q "$base" "$SKILL_MD" \ || finding "STRUCT: $d/$base exists on disk but is never cited from SKILL.md" done done # 3. every relative resource link in SKILL.md resolves while IFS= read -r path; do [[ -e "$SKILL_DIR/$path" ]] \ || finding "STRUCT: SKILL.md links to missing file: $path" done < <(grep -oE '\]\((references|assets|scripts|tests)/[^)#]+\)' "$SKILL_MD" \ | sed -E 's/^\]\(//; s/\)$//' | sort -u) } # ── live: docs vs the installed build ──────────────────────────────────────── live_checks() { emit "== verify-commands --live (installed-build drift)" if ! command -v ffmpeg >/dev/null 2>&1; then echo "ffmpeg not on PATH — live check unavailable (advisory, not a failure)" >&2 exit "$EXIT_UNAVAILABLE" fi local encoders filters hfull docs encoders="$(ffmpeg -hide_banner -encoders 2>/dev/null)" filters="$(ffmpeg -hide_banner -filters 2>/dev/null)" hfull="$(ffmpeg -hide_banner -h full 2>/dev/null)" docs="$(cat "$SKILL_MD" "$SKILL_DIR"/references/*.md 2>/dev/null)" # Filters that exist in EVERY ffmpeg build — absence means the filter was # renamed/removed upstream, i.e. our docs drifted. local core_filters=(scale crop pad fps overlay concat setpts atempo amix silencedetect silenceremove loudnorm palettegen paletteuse select tile transpose trim atrim split format) # Build-optional (external libs / hw): warn only. local optional_tokens=(libx264 libx265 libsvtav1 libaom-av1 libvpx-vp9 libopus libmp3lame drawtext subtitles lut3d zscale tonemap libvmaf minterpolate vidstabdetect vidstabtransform bwdif hqdn3d nlmeans xstack showwaves showspectrum colorbalance colortemperature colorchannelmixer colorhold vibrance haldclut chromashift) # CLI options the cookbook depends on; renamed/removed = drift (-vsync class). local core_options=(fps_mode movflags avoid_negative_ts map_metadata filter_complex frames pix_fmt) # NOTE: the flags column width varies across ffmpeg majors (3 chars <=7.x, # 2 chars in 8.x) — match any flag run, then the exact filter name token. for f in "${core_filters[@]}"; do grep -qE "^ +[A-Z.|]+ +$f +" <<<"$filters" \ || finding "DRIFT: core filter '$f' not in installed ffmpeg (renamed/removed upstream?)" done for opt in "${core_options[@]}"; do grep -q -- "-$opt" <<<"$hfull" \ || finding "DRIFT: documented option '-$opt' unknown to installed ffmpeg" done # Every software encoder the docs name must at least be a known encoder name # in this build — missing here is a warning (build config), not drift, EXCEPT # the universal natives (aac, ffv1) which every build ships. for enc in aac ffv1; do grep -qE "^ [A-Z.]{6} +$enc " <<<"$encoders" \ || finding "DRIFT: native encoder '$enc' not in installed ffmpeg" done for tok in "${optional_tokens[@]}"; do if grep -qF "$tok" <<<"$docs"; then grep -qE "(^ [A-Z.]{6} +$tok )|(^ +[A-Z.|]+ +$tok +)" <<<"$encoders"$'\n'"$filters" \ || emit " warn: '$tok' documented but absent from this build (build-optional — not drift)" fi done # Deprecated-flag tripwire: docs must not RECOMMEND -vsync (mentioning it as # deprecated in footgun tables is fine; a code fence using it is not). if grep -E '^\s*ffmpeg .*-vsync ' <<<"$docs" | grep -vq 'fps_mode'; then finding "DRIFT: a documented command still uses deprecated -vsync (use -fps_mode)" fi } case "$MODE" in offline) offline_checks ;; live) live_checks ;; esac if [[ "$findings" -eq 0 ]]; then emit "verify-commands ($MODE): clean" exit "$EXIT_OK" fi emit "verify-commands ($MODE): $findings finding(s)" exit "$EXIT_DRIFT"