Commit 1219c704 by PLN (Algolia)

feat(gig): one command says GO or NO-GO — and it caught two bugs on its first run (#44)

A fortnight of debugging produced a shelf of good checks: check-boot,
check-mix, silent-eval, fix-mute-roles, pvlint, orphan-orbits. Every one of
them worked. Every one of them was also a command PLN would have to REMEMBER —
and on Sat 8 Aug at ~19:00 he is in a field, alone, with a soundcheck window
and no assistant. A check you have to remember under pressure is a check you
do not run.

So gig-up.sh contains no new checking logic at all. It is a running order, an
exit code, and a decision about which failures are GO-BLOCKING. Its whole value
is that there is now one thing to type.

WHAT IS HARD vs SOFT, and why the line is drawn there
  HARD  boot helpers    one parse error in BootTidal.hs silences EVERY track
                        at once, invisibly, while a stale ghci holds the old
                        definitions. First check for that reason.
  HARD  ardour faders   earned it three sessions running, a DIFFERENT set of
                        tracks each time: 07-28 had 05/06/08/10 at -inf,
                        07-30 had 06/10/12, 07-31 had 10. Three recurrences is
                        not bad luck, it is a property of MIDI-learned faders
                        that drift from the saved session in both directions.
  HARD  compiles / mute map / pvlint
  SOFT  transition ghosts   real (#77) and known-open. A gate that goes red for
                            work you have consciously deferred is a gate that
                            gets ignored.
  SOFT  LCXL present        legitimately unplugged at a kitchen table. At the
                            venue, a warn here means the set has no hands.

TWO BUGS FOUND BY THE FIRST RUN — which is the argument for building it
  1. check-boot.sh and check-tracks.sh were mode 644. NOT EXECUTABLE. The
     pre-gig audio gate has never once been runnable as `tools/check-tracks.sh`
     since it was written; it only ever ran when someone typed `zsh tools/...`.
     Every green memory of it is a green memory of a different invocation.
  2. pvlint's `--setlist` is a MODIFIER ("these paths are ordered"), not a
     mode — it still requires paths. `python3 -m pvlint --setlist` exits 2 on
     an argparse error, which from the outside looks exactly like a lint
     failure. gig-up now borrows set-coherence's setlist_tracks() rather than
     teaching a fourth file to parse the setlist.

Both are the same shape and it is this rig's signature failure: a thing that
was verified once, in a context that no longer holds. Assembling the checks
into one caller is what re-executed them under fresh conditions.

DESIGN NOTES
  * Cold by default. Makes no sound, sends no MIDI, writes no .tidal. Safe to
    run mid-set, at 3am, or backstage. The audio gate is --audio, and it skips
    itself if the cold gate is already red — booting 13 tracks to confirm what
    a typecheck just told you is 10 wasted minutes at a venue.
  * Never touches CC 77-84 (Ardour track gains; 77 down is total silence) and
    never sweeps 93 (gPanic).
  * Scope is the SETLIST, not the corpus. 14 corpus tracks do not compile
    (#112) and must never make the gig gate red.
  * Failures print FIRST and carry their fix command inline, because the read
    order in a field is "what is broken / what do I type".
  * GO states its own limit every time: check-mix reads the SAVED session, so
    green means "Ardour will BOOT with this fader up", never "the mixer IS
    correct". Saying it in those words is how the -inf bug stops hiding behind
    a clean report.

Verified: NO-GO path observed for real (2 failures, both genuine, both fixed);
GO path 7/7 green; --help, --quiet and unknown-flag paths exercised.
parent 4c3bec01
#!/usr/bin/env bash
# gig-up — ONE command that says GO or NO-GO for the whole chain.
#
# Why this exists (2026-07-31, J-8 to OPAL)
# -----------------------------------------
# A fortnight of debugging produced a shelf of good checks — check-boot,
# check-mix, silent-eval, fix-mute-roles, pvlint, orphan-orbits — and every one
# of them is currently a command PLN would have to REMEMBER. On Sat 8 Aug at
# ~19:00 he is in a field, alone, with a soundcheck window and no assistant.
# A check you have to remember under pressure is a check you do not run.
#
# So this is not new logic. It is a running order and an exit code. Every check
# below already exists and already works; gig-up only decides which are
# GO-BLOCKING and prints the fix inline so the answer and the remedy arrive
# together.
#
# The one that earned its HARD FAIL
# ---------------------------------
# `check-mix.py` finding an orbit at -inf has now been caught three sessions in
# a row, with a DIFFERENT set of tracks each time:
#
# 2026-07-28 Tidal 05, 06, 08, 10 = -inf
# 2026-07-30 Tidal 06, 10, 12 = -inf
# 2026-07-31 Tidal 10 = -inf (after the Ctrl+S that fixed 06 and 12)
#
# It recurs because the D-row faders are MIDI-learned to LCXL CC 77-84, so the
# saved session and the physical desk drift independently in BOTH directions.
# Three recurrences is not bad luck, it is a property of the setup — hence a
# hard gate rather than a warning. NB the remedy splits: a fader that is still
# -inf AFTER a save is genuinely down (raise it), not merely unpersisted (Ctrl+S).
#
# What it deliberately does NOT do
# --------------------------------
# * make sound — the audio gate is `--audio` (check-tracks.sh, ~45s
# per track). Default is cold, so this is safe to run
# mid-set, at 3am, or in a quiet backstage room.
# * send any MIDI — the surface check reads ALSA only. It never touches
# CC 77-84 (Ardour track gains; CC77 down is total
# silence) and never sweeps CC 93 (gPanic).
# * touch a .tidal — read-only, always. PLN rehearses on these files and
# Pulsar saves the BUFFER, not the disk.
# * judge the corpus — scope is the SETLIST. 14 corpus tracks do not compile
# (#112) and must never make the gig gate red.
#
# Usage
# -----
# tools/gig-up.sh # the cold gate — safe anywhere, ~1 min
# tools/gig-up.sh --audio # + check-tracks.sh (makes sound, ~10 min)
# tools/gig-up.sh --quiet # only the verdict and any failures
#
# Exit 0 = GO. Non-zero = do not start; the reasons are printed first.
set -u
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" || exit 2
AUDIO=0
QUIET=0
for a in "$@"; do
case "$a" in
--audio) AUDIO=1 ;;
--quiet|-q) QUIET=1 ;;
-h|--help) sed -n '2,58p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'; exit 0 ;;
*) echo "gig-up: unknown option $a (try --help)" >&2; exit 2 ;;
esac
done
if [ -t 1 ]; then R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; D=$'\033[2m'; Z=$'\033[0m'
else R=; G=; Y=; D=; Z=; fi
LOG="$(mktemp -t gig-up-XXXXXX.log)"
FAILURES=() # one "name|fix command" per blocking failure, printed FIRST at the end
WARNINGS=()
# run <name> <fix-hint> <cmd...>
# Blocking. Non-zero exit => the gig does not start.
run() {
local name="$1" fix="$2"; shift 2
printf ' %-22s' "$name"
{ echo "########## $name : $*"; "$@"; } >>"$LOG" 2>&1
local rc=$?
if [ $rc -eq 0 ]; then
echo "${G}ok${Z}"
else
echo "${R}FAIL${Z} ${D}(rc=$rc)${Z}"
FAILURES+=("$name|$fix")
fi
return $rc
}
# soft <name> <hint> <cmd...>
# Advisory. Reports, never blocks — for things that are known-open work or
# depend on hardware that is legitimately absent at this moment.
soft() {
local name="$1" hint="$2"; shift 2
printf ' %-22s' "$name"
{ echo "########## $name : $*"; "$@"; } >>"$LOG" 2>&1
if [ $? -eq 0 ]; then echo "${G}ok${Z}"
else echo "${Y}warn${Z}"; WARNINGS+=("$name|$hint"); fi
}
# --- context: is the rig up? -------------------------------------------------
# Not a check, an interpretation key. Several results below mean different
# things cold vs live, and reading the report without knowing which is how you
# get a confidently wrong verdict.
rig_state() {
local sc=no ard=no
pgrep -x scsynth >/dev/null 2>&1 && sc=yes
pgrep -x ardour >/dev/null 2>&1 || pgrep -x ardour8 >/dev/null 2>&1 \
|| pgrep -x ardour9 >/dev/null 2>&1 && ard=yes
echo "$sc $ard"
}
read -r SC ARD <<<"$(rig_state)"
(( QUIET )) || {
echo
echo "gig-up — proving the chain. ${D}scsynth=$SC ardour=$ARD $( [ "$SC" = no ] && echo '(rig DOWN — cold checks only are meaningful)')${Z}"
echo
}
# --- 1. the helpers typecheck ------------------------------------------------
# First because one parse error in BootTidal.hs silences EVERY track at once —
# the worst failure this rig has, and invisible while a stale ghci still holds
# the old definitions.
run "boot helpers" \
"read the ghc error: tools/check-boot.sh" \
tools/check-boot.sh
# --- 2. the saved Ardour faders ----------------------------------------------
run "ardour faders" \
"raise it on the desk, then Ctrl+S in Ardour, then re-run. Still -inf after a save = genuinely down." \
python3 tools/check-mix.py --quiet
# --- 3. every setlist track compiles -----------------------------------------
# Cold, seeded, no rig required. Baseline 13/13 as of 2026-07-31.
run "setlist compiles" \
"python3 tools/silent-eval.py --seeded --keep # then read the ghc error for the named track" \
python3 tools/silent-eval.py --seeded
# --- 4. the mute map has not drifted -----------------------------------------
# gM1 = the kick alone, gM2 = the other percs, gM3 = bass + melodics. The
# filters group differently on purpose (gF1 = the whole rhythm bloc), so this
# check exists precisely because the two maps LOOK like they should match.
run "mute map" \
"python3 tools/fix-mute-roles.py --apply # then re-read the diff before committing" \
python3 tools/fix-mute-roles.py --check
# --- 5. lint, errors only ----------------------------------------------------
# pvlint's `--setlist` is a MODIFIER ("these paths are ordered"), not a mode —
# it still needs the paths. Rather than teach a fourth file how to parse the
# setlist (check-tracks.sh, orphan-orbits.py and set-coherence.py each already
# know), borrow the one parser that is importable. One parser per concept.
run "pvlint (setlist)" \
"cd tools && python3 -m pvlint --setlist \$(...) # --fix applies the safe ones" \
bash -c 'cd tools && python3 -m pvlint --quiet --setlist $(python3 -c "
import importlib.util, pathlib
spec = importlib.util.spec_from_file_location(\"sc\", \"set-coherence.py\")
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
print(\" \".join(str(p) for p in m.setlist_tracks()))")'
# --- 6. orbit ghosts across transitions --------------------------------------
# Advisory ON PURPOSE. Ghosting orbits are real (#77) and known-open; they make
# a transition messy, not the gig impossible. A gate that goes red for open work
# is a gate that gets ignored.
soft "transition ghosts" \
"python3 tools/orphan-orbits.py --matrix # #77 is the fix, not tonight" \
python3 tools/orphan-orbits.py
# --- 7. the surface is plugged in --------------------------------------------
# READ-ONLY: asks ALSA who is on the bus. Sends nothing. Advisory because the
# desk is legitimately unplugged when this runs at a kitchen table — but at the
# venue, a warn here means the set has no hands.
soft "LCXL present" \
"replug the LCXL (USB OUT endpoint stalls — LEDs dark but input alive), then tools/lcxl-init.py" \
bash -c 'aseqdump -l 2>/dev/null | grep -qi "Launch Control XL"'
# --- verdict -----------------------------------------------------------------
# Failures print FIRST and carry their fix, because the read order in a field is
# "what is broken / what do I type", not "here is a report".
echo
if [ ${#FAILURES[@]} -gt 0 ]; then
echo "${R}NO-GO — ${#FAILURES[@]} blocking failure(s):${Z}"
for f in "${FAILURES[@]}"; do
echo " ${R}${Z} ${f%%|*}"
echo " fix: ${f#*|}"
done
echo
fi
if [ ${#WARNINGS[@]} -gt 0 ] && ! (( QUIET )); then
for w in "${WARNINGS[@]}"; do
echo " ${Y}!${Z} ${w%%|*}${w#*|}"
done
echo
fi
# --- 8. the audio gate, opt-in ----------------------------------------------
# Last, and only on request: it makes sound and takes ~45s per track. Skipped if
# the cold gate is already red — booting 13 tracks to confirm what a typecheck
# just told you is 10 wasted minutes at a venue.
if (( AUDIO )); then
if [ ${#FAILURES[@]} -gt 0 ]; then
echo "${Y}--audio skipped: fix the cold failures first (they will only fail louder).${Z}"
elif [ "$SC" = no ]; then
echo "${Y}--audio skipped: scsynth is not running. Boot the rig, then re-run.${Z}"
else
echo "audio gate — every declared orbit must make sound (~45s/track):"
tools/check-tracks.sh || FAILURES+=("audio gate|read the check-tracks log printed above")
echo
fi
fi
echo "${D}detail: $LOG${Z}"
if [ ${#FAILURES[@]} -gt 0 ]; then
echo "${R}gig-up: NO-GO.${Z}"
exit 1
fi
echo "${G}gig-up: GO.${Z}"
# State the limit every time, in these words. check-mix reads the SAVED session
# file, so a green fader line means "Ardour will BOOT with this fader up" — it
# says nothing about where the physical desk is right now, and the two drift
# independently (CC 77-84 are MIDI-learned; the desk wins on touch). A clean
# report otherwise reads as the stronger claim, which is how the -inf bug
# survived three sessions.
(( QUIET )) || {
echo "${D} Proves the set BOOTS correct. NOT that the mixer IS correct —${Z}"
echo "${D} check-mix reads the saved session, not the live desk. Ctrl+S in${Z}"
echo "${D} Ardour before trusting a green fader line.${Z}"
[ "$AUDIO" = 0 ] && echo "${D} Nothing here made sound: add --audio for the empirical gate.${Z}"
}
exit 0
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment