diff --git a/Tools/audit_headers.sh b/Tools/audit_headers.sh new file mode 100755 index 0000000..05f70d7 --- /dev/null +++ b/Tools/audit_headers.sh @@ -0,0 +1,194 @@ +#!/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