195 lines
8.5 KiB
Bash
Executable File
195 lines
8.5 KiB
Bash
Executable File
#!/bin/bash
|
|
# ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
# PURPOSE
|
|
# Hold the documentation standard still. Every .sh and .php file in the repo opens with a
|
|
# structured header, and the AI index routes retrieval on those section names across both
|
|
# languages — a renamed or missing section is not a cosmetic problem, it is a file the
|
|
# assistant can no longer find by intent.
|
|
#
|
|
# Written 2026-08-24 after a three-week sprint at ~140 commits/week left 47 files drifted.
|
|
# Drift concentrates in the newest code, which is exactly the code nobody re-reads.
|
|
#
|
|
# OPERATIONAL MODEL
|
|
# Role decides the specification. The first four sections are identical everywhere, so one
|
|
# query answers "what does this do" for a bash script and a PHP endpoint alike. Sections 5+
|
|
# describe the shape of the thing — an endpoint has a REQUEST and a RESPONSE, a library has
|
|
# EXPORTS, a page RENDERS. Those tails never cross: REQUEST appears in 53 api/ files and
|
|
# zero others, RENDERS in 11 pages/ files and zero others.
|
|
#
|
|
# OPERATIONAL MODEL is not required of include/ or pages/. A pure function library has no
|
|
# lifecycle to describe, and on a page the section has come to mean something different —
|
|
# pages/auth.php and pages/scheduler.php use it to state blast radius, not mechanics. That
|
|
# is worth keeping rare; mandatory on all eleven pages it would become filler.
|
|
#
|
|
# Conditional sections are omitted, never stubbed. A file with no CONFIGURATION section
|
|
# reads no conf vars, and that absence is information worth being able to grep for. A
|
|
# section whose body is "None" destroys it.
|
|
#
|
|
# DESIGN PRINCIPLES
|
|
# Report the file, not a total.
|
|
# A count tells the operator a number; a list tells them what to open. Every failure
|
|
# names the file, the role it was judged as, and which rule it broke.
|
|
#
|
|
# Order is checked against present sections only.
|
|
# A file missing REQUEST should be told it is missing REQUEST, once — not told that
|
|
# and then told its order is wrong as a consequence. One defect, one line.
|
|
#
|
|
# The conf check is soft and says so.
|
|
# Detecting "this file reads configuration" is a heuristic on variable naming, and a
|
|
# heuristic that reports as a hard failure trains the operator to ignore the tool.
|
|
#
|
|
# It lints itself.
|
|
# This file is in scope and conforms. A standards checker exempt from its own standard
|
|
# is a checker nobody believes.
|
|
#
|
|
# OPERATIONAL SAFEGUARDS
|
|
# Read-only. Opens files, writes nothing, touches no conf and no state.
|
|
# Exits 1 when any file fails, 0 when the repo is clean — safe to gate a commit on.
|
|
# Unknown paths are skipped rather than guessed at, so a new directory is never judged
|
|
# against a specification that was not written for it.
|
|
#
|
|
# RUNTIME MODES
|
|
# ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
#
|
|
# audit_headers.sh
|
|
# Audit the whole repo. Lists every non-conforming file and exits 1 if any.
|
|
#
|
|
# audit_headers.sh --verbose
|
|
# Also list the files that pass, with the role each was judged as.
|
|
#
|
|
# audit_headers.sh --role=api
|
|
# Audit one role only: sh | tools | api | include | pages
|
|
#
|
|
# audit_headers.sh --spec
|
|
# Print the specification this build enforces, and exit.
|
|
#
|
|
# ══════════════════════════════════════════════════════════════════════════════════════════════
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
ONLY_ROLE=""
|
|
VERBOSE=false
|
|
SHOW_SPEC=false
|
|
for a in "$@"; do
|
|
case "$a" in
|
|
--verbose) VERBOSE=true ;;
|
|
--spec) SHOW_SPEC=true ;;
|
|
--role=*) ONLY_ROLE="${a#--role=}" ;;
|
|
*) echo "unknown argument: $a" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
# Mandatory sections, in required order. Conditional sections are deliberately absent here
|
|
# and handled as soft checks — see OPERATIONAL MODEL.
|
|
spec_for() {
|
|
case "$1" in
|
|
sh|tools) echo "PURPOSE|OPERATIONAL MODEL|DESIGN PRINCIPLES|OPERATIONAL SAFEGUARDS|RUNTIME MODES" ;;
|
|
api) echo "PURPOSE|OPERATIONAL MODEL|DESIGN PRINCIPLES|OPERATIONAL SAFEGUARDS|REQUEST|RESPONSE|DEPENDS ON" ;;
|
|
include) echo "PURPOSE|DESIGN PRINCIPLES|OPERATIONAL SAFEGUARDS|EXPORTS" ;;
|
|
pages) echo "PURPOSE|DESIGN PRINCIPLES|OPERATIONAL SAFEGUARDS|RENDERS|DEPENDS ON" ;;
|
|
esac
|
|
}
|
|
|
|
role_for() {
|
|
case "$1" in
|
|
*.sh) echo "sh" ;;
|
|
*/Plugin/*/Tools/*.php) echo "tools" ;;
|
|
*/Plugin/*/api/*.php) echo "api" ;;
|
|
*/Plugin/*/include/*.php) echo "include" ;;
|
|
*/Plugin/*/pages/*.php) echo "pages" ;;
|
|
esac
|
|
}
|
|
|
|
if $SHOW_SPEC; then
|
|
echo "Header specification — first four sections identical across all roles"
|
|
echo
|
|
for r in sh tools api include pages; do
|
|
printf ' %-8s %s\n' "$r" "$(spec_for "$r" | sed 's@|@ > @g')"
|
|
done
|
|
echo
|
|
echo " Conditional (omit when nothing to say): STATE FILES, CONFIGURATION, STACK SWITCHING"
|
|
echo " OPERATIONAL MODEL is not required of include/ or pages/."
|
|
exit 0
|
|
fi
|
|
|
|
cd "$REPO_ROOT" || { echo "cannot reach repo root: $REPO_ROOT" >&2; exit 2; }
|
|
|
|
fail=0 clean=0 total=0
|
|
|
|
while IFS= read -r f; do
|
|
role=$(role_for "$f")
|
|
[[ -z "$role" ]] && continue
|
|
[[ -n "$ONLY_ROLE" && "$role" != "$ONLY_ROLE" ]] && continue
|
|
spec=$(spec_for "$role")
|
|
total=$((total + 1))
|
|
problems=()
|
|
|
|
if [[ "$f" == *.sh ]]; then m='#'; else m='//'; fi
|
|
IFS='|' read -ra want <<< "$spec"
|
|
|
|
present=()
|
|
missing=()
|
|
for s in "${want[@]}"; do
|
|
n=$(grep -c "^$m $s\$" "$f")
|
|
case "$n" in
|
|
0) missing+=("$s") ;;
|
|
1) present+=("$s") ;;
|
|
*) present+=("$s"); problems+=("duplicate section: $s appears $n times") ;;
|
|
esac
|
|
done
|
|
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
problems+=("missing: $(IFS=', '; echo "${missing[*]}")")
|
|
fi
|
|
|
|
# Order — compare the spec sections in file order against spec order, present ones only.
|
|
if [[ ${#present[@]} -gt 1 ]]; then
|
|
keep=$(IFS='|'; echo "${present[*]}")
|
|
got=$(grep -oE "^$m [A-Z][A-Z /&-]{3,40}\$" "$f" \
|
|
| sed "s@^$m @@" \
|
|
| awk -v ok="$keep" 'BEGIN{n=split(ok,a,"|"); for(i=1;i<=n;i++) k[a[i]]=1} k[$0] && !seen[$0]++')
|
|
exp=$(printf '%s\n' "${present[@]}")
|
|
if [[ "$got" != "$exp" ]]; then
|
|
problems+=("order: $(echo "$got" | tr '\n' '>' | sed 's@>$@@')")
|
|
fi
|
|
fi
|
|
|
|
# Placement — nothing but <?php or a shebang may precede the header.
|
|
hl=$(grep -n "^$m PURPOSE\$" "$f" | head -1 | cut -d: -f1)
|
|
if [[ -n "$hl" && "$hl" -gt 1 ]]; then
|
|
above=$(head -n $((hl - 1)) "$f" | grep -vE "^(<\?php|#!/|[[:space:]]*\$|$m)" | head -1)
|
|
if [[ -n "$above" ]]; then
|
|
problems+=("placement: '$(echo "$above" | cut -c1-46)' precedes the header")
|
|
fi
|
|
fi
|
|
|
|
# Soft — CONFIGURATION only belongs to the roles whose tail defines it.
|
|
case "$role" in
|
|
sh|tools|include)
|
|
reads=""
|
|
if [[ "$f" == *.sh ]]; then
|
|
grep -qE '\$\{?(HOST[12]|RSYNC|AI|WATCHDOG|PARTNERSHIP|FALLBACK|CONF)_[A-Z0-9_]+' "$f" && reads=y
|
|
else
|
|
grep -q 'vv_conf_vars(' "$f" && reads=y
|
|
fi
|
|
if [[ "$reads" == y ]] && ! grep -q "^$m CONFIGURATION\$" "$f"; then
|
|
problems+=("soft: reads conf vars, no CONFIGURATION section")
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
if [[ ${#problems[@]} -gt 0 ]]; then
|
|
fail=$((fail + 1))
|
|
printf '%s [%s]\n' "${f#./}" "$role"
|
|
for p in "${problems[@]}"; do printf ' %s\n' "$p"; done
|
|
else
|
|
clean=$((clean + 1))
|
|
$VERBOSE && printf ' ok %-52s [%s]\n' "${f#./}" "$role"
|
|
fi
|
|
done < <(find . \( -name '*.sh' -o -name '*.php' \) -not -path './.git/*' | sort)
|
|
|
|
echo
|
|
echo "──────────────────────────────────────────────────────────────"
|
|
printf 'conforming %s / %s non-conforming %s\n' "$clean" "$total" "$fail"
|
|
[[ $fail -gt 0 ]] && exit 1
|
|
exit 0
|