#!/usr/bin/env bash # # gen-index.sh — keep context/README.md's documentation index in sync with the repo. # # context/gen-index.sh # CHECK: report drift, exit 1 if the index is stale # context/gen-index.sh --write # rewrite the AUTOGEN block in context/README.md # context/gen-index.sh --help # # Only the region between the AUTOGEN markers in context/README.md is touched; # the hand-written header/scope note above it is left alone. Descriptions that # already exist in the index are PRESERVED (matched by path); files that are new # to the tree get a stub description pulled from their first heading/lead line, # ready for you to polish. Vendored/third-party trees are excluded, matching the # index's stated scope. # # Run it from anywhere; it locates the repo via this script's own path. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" INDEX="context/README.md" BEGIN='' END='' MODE="check" case "${1:-}" in --write) MODE="write" ;; ""|--check) MODE="check" ;; -h|--help) sed -n '3,17p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "unknown option: $1 (try --help)" >&2; exit 2 ;; esac # --- ordered sections: a path lands in the FIRST section whose regex matches --- TITLES=( "Orientation" "Emulator — plan, phases & operations" "Emulator — subsystems" "Emulator — deployment & ports" "Emulator — devices & support files" "Emulator — VelociRender firmware decompilation" "Emulator — render bridge" "dpl3-revive — DPL3 renderer port & format specs" "restoration — scene re-renders" "restoration — RIO firmware & hardware" "restoration — source410 reconstruction" ) REGEX=( '^(README|HISTORY)\.md$' '^emulator/(PLAN|PHASE[0-9].*|LAUNCH|ENV-VARS)\.md$' '^emulator/(VDB-NOTES|GAUGE-NOTES|SOUND-NOTES|NET-NOTES|RIO-NOTES|CAMERA-REVIEW-NOTES|RENDER-HARNESS|RENDERER-COLLAB)\.md$' '^emulator/(DEPLOYMENT-PLAN|XP-PORT-PLAN|EGRESS-HOLD-FIX-PLAN)\.md$' '^emulator/(vpx-device|pod-launch|net-boot|deploy|roms|rio-firmware)/' '^emulator/firmware-decomp/' '^emulator/render-bridge/' '^dpl3-revive/' '^restoration/README\.md$' '^restoration/rio-' '^restoration/source410/' ) # --- the doc set (same filter as the index scope) --- list_docs() { git ls-files '*.md' '*.MD' \ | grep -viE '/(vs|libpng|libpdcurses|sdl2|sdl2net|freetype)/' \ | grep -viE '^emulator/src/(NOTES|\.github)/' \ | grep -vxF "$INDEX" \ | sort } # --- preserved descriptions: parse the current AUTOGEN block, path -> desc --- declare -A DESC if [ -f "$INDEX" ]; then in_block=0 while IFS= read -r line; do [ "$line" = "$BEGIN" ] && { in_block=1; continue; } [ "$line" = "$END" ] && { in_block=0; continue; } [ "$in_block" = 1 ] || continue case "$line" in "- ["*"](../"*")"*) rest="${line#*](../}"; path="${rest%%)*}"; after="${rest#*)}" after="${after#"${after%%[![:space:]]*}"}" # ltrim after="${after#—}"; after="${after#-}" # drop leading em/hyphen dash after="${after#"${after%%[![:space:]]*}"}" # ltrim again [ -n "$after" ] && DESC["$path"]="$after" ;; esac done < "$INDEX" fi # --- stub description for a file new to the index --- extract_desc() { local f="$1" raw raw="$(awk 'NR>1 && $0 !~ /^#/ && $0 !~ /^---/ && $0 !~ /^>/ && $0 !~ /^[[:space:]]*$/ {print; exit}' "$f")" raw="$(printf '%s' "$raw" | sed -E 's/[`*]//g; s/[[:space:]]+/ /g; s/^ +//; s/ +$//')" [ "${#raw}" -gt 140 ] && raw="${raw:0:137}..." [ -n "$raw" ] && printf '%s' "$raw" || printf '(no description — add one)' } # --- build the block body on stdout --- gen_body() { local docs; docs="$(list_docs)" local -a remaining; mapfile -t remaining <<< "$docs" emit_section() { local title="$1" re="$2"; shift 2 local -a kept=() matched=() local p for p in "${remaining[@]}"; do [ -z "$p" ] && continue if [[ "$p" =~ $re ]]; then matched+=("$p"); else kept+=("$p"); fi done remaining=("${kept[@]}") [ "${#matched[@]}" -eq 0 ] && return 0 printf '\n## %s\n\n' "$title" for p in "${matched[@]}"; do local d="${DESC[$p]:-}"; [ -z "$d" ] && d="$(extract_desc "$p")" printf -- '- [%s](../%s) — %s\n' "$p" "$p" "$d" done } local i for i in "${!TITLES[@]}"; do emit_section "${TITLES[$i]}" "${REGEX[$i]}" done # anything unmatched still shows up — never silently dropped local -a leftover=() local p for p in "${remaining[@]}"; do [ -n "$p" ] && leftover+=("$p"); done if [ "${#leftover[@]}" -gt 0 ]; then printf '\n## Uncategorized\n\n' printf '%s\n' '> New locations the section rules do not cover yet — assign them in gen-index.sh.' printf '\n' for p in "${leftover[@]}"; do local d="${DESC[$p]:-}"; [ -z "$d" ] && d="$(extract_desc "$p")" printf -- '- [%s](../%s) — %s\n' "$p" "$p" "$d" done fi } # --- current block body (between the markers) on stdout --- current_body() { awk -v b="$BEGIN" -v e="$END" ' $0==b {inb=1; next} $0==e {inb=0} inb==1 {print} ' "$INDEX" } grep -qF "$BEGIN" "$INDEX" && grep -qF "$END" "$INDEX" || { echo "error: AUTOGEN markers not found in $INDEX" >&2; exit 2; } NEW="$(gen_body)" if [ "$MODE" = "check" ]; then if diff <(current_body) <(printf '%s\n' "$NEW") >/dev/null; then echo "context index is current ($(list_docs | grep -c . ) docs)." exit 0 fi echo "context index is STALE — run: context/gen-index.sh --write" >&2 echo "--- diff (current -> generated) ---" >&2 diff <(current_body) <(printf '%s\n' "$NEW") >&2 || true exit 1 fi # MODE=write: splice NEW between the markers, leave everything else intact tmp="$(mktemp)" awk -v b="$BEGIN" -v e="$END" -v body="$NEW" ' $0==b {print; print body; skip=1; next} $0==e {skip=0; print; next} skip!=1 {print} ' "$INDEX" > "$tmp" mv "$tmp" "$INDEX" echo "wrote $INDEX ($(list_docs | grep -c . ) docs)."