Commit 8b58011b by PLN (Algolia)

docs: design the gig-up merge — two verbs, not three names

The obvious reading is that the two gig-up scripts are forks that drifted. They
are not. gig-up.sh is a LAUNCHER (start things in order) and tools/gig-up.sh is
a GATE (14 assertions over the repo and the saved session) — two different tools
wearing one name. And a third, tools/gig-preflight.py, is already the gate over
the live machine; its own docstring says '--quiet (for gig-up.sh)', so the gate
was always meant to be called by the launcher and never was.

So the merge is a separation, not a diff: gig-up does, check-gig proves,
gig-up-window owns the terminal. Three entry points become two, each answering
one question, and the launcher ends by calling the gate — which makes today's
failure (a gate that existed but was unreachable from the path actually pressed)
structurally impossible.

Six phases, each independently revertable, and phase 0 is a harness because
neither script nor the preflight has any test coverage today. Nothing is deleted
until the phase replacing it has run on a real launch: a bad gig-up at a venue
is not recoverable under pressure.
parent a55d08e9
# Design: one launcher, one gate
**Status:** proposed, 2026-09-22. Not for execution before the Thursday gig unless
PLN says so; phases 1-2 are safe at any time, phase 5 is not.
PLN, 2026-09-22: *"lets do a proper merge imo now, it can be done clean, lets have
simpler tooling well move faster."*
## The problem is not a fork
The obvious reading is that `gig-up.sh` and `tools/gig-up.sh` are two forks of one
script that drifted. They are not. They are **two different tools wearing one
name**, and a third tool already does half of what one of them does.
| file | lines | what it actually is |
|---|---|---|
| `gig-up.sh` | 528 | a **launcher**: perf, cut-gpu, SuperDirt, wait-for-MIDI, session sweep, fader restore, Ardour, Pulsar, then a readiness report |
| `tools/gig-up.sh` | 620 | a **gate over the repo and the saved session**: 14 `run "…"` assertions (setlist vs backlog, every track compiles, pvlint, preload covers set, surface grid, fader baseline, orbit ghosts, recorder) plus a `--converge` launcher |
| `tools/gig-preflight.py` | 522 | a **gate over the live machine**: perf regime, RT audio, SuperDirt freshness, sample-symlink integrity, MIDI surface identity, editor, background load, xruns, monitor path |
The collision cost a real failure today. `parvagues.desktop` runs the root
launcher; the Bridge's RIG UP button runs `tools/gig-up.sh --converge`. Both
accept `--converge` and `--quiet`, so the two paths look interchangeable and are
not: the preload gate lived only on the path PLN does not press, and the
readiness gate only on the path the Bridge does not call. He found it by noticing
that a launch opened a second Ardour.
`gig-preflight.py`'s own docstring already names the shape we should have had:
`--quiet # only WARN/FAIL lines (for gig-up.sh)`. The gate was always meant to
be called by the launcher. It just never was.
## The design: two verbs
The repo already has a naming rule (`CLAUDE.md`, "One vocabulary"): verbs are
Check, Gen, Read, Lint, Grade, Pack, in fixed word order, so a name can be guessed
without an index. `check-mix`, `check-preload`, `check-drift`,
`check-boot-blocks` all follow it. So:
- **`gig-up.sh`** - *do*. Everything that starts, waits, restores or converges.
One file, at the repo root, where the launcher already points.
- **`tools/check-gig.sh`** - *prove*. One gate, read-only by construction, in two
layers: the **repo/set layer** (today's `tools/gig-up.sh` assertions) and the
**machine layer** (today's `gig-preflight.py`, called as a module).
- **`tools/gig-up-window.sh`** - the terminal. Already separated, shipped
2026-09-22.
Three entry points become two, and each answers one question: *is the rig up?* and
*can it play?* The launcher ends by calling the gate, so there is exactly one
thing to run before doors and it cannot be the stale copy.
```
parvagues.desktop ─┐
├─► tools/gig-up-window.sh ─► gig-up.sh ─► tools/check-gig.sh
Bridge "RIG UP" ──┘ (log + hold) (act) │ (prove)
└─► gig-preflight.py
(machine layer)
```
*Every arrow is a call, not a copy. That is the whole point.*
## Flags after the merge
Ownership follows the verb. An action flag belongs to the launcher; a scope flag
belongs to the gate.
| flag | today | after |
|---|---|---|
| `--converge` | both | `gig-up` |
| `--headphones` | root | `gig-up` |
| `--bt-off` / `--bt-on` | tools | `gig-up` (rfkill is an action) |
| `--quiet` | both | both, same meaning: WARN/FAIL only |
| `--live` / `--audio` | tools | `check-gig` (opt-in deeper probes) |
| `--value` | tools | `check-gig` |
## Phases
Each phase is independently verifiable and independently revertable. **Nothing is
deleted until the phase that replaces it has run on a real launch.** The boot path
is the one script that cannot be broken: a bad `gig-up` at a venue is not
recoverable under pressure.
**Phase 0 - a harness, because there is none.** `tools/tests/` has no coverage for
either script or for `gig-preflight.py`. Capture the current output of all three
into golden files (`--quiet` where available, so the goldens are verdict lines
rather than timings), and add a test that each script still emits them. Without
this, every later phase is a blind refactor of the boot path.
**Phase 1 - `--converge` moves to the launcher.** The two implementations must be
compared before either is trusted; keep the root one, and point
`tools/bridge/rig.py:35` (`GIG_UP`) at `gig-up.sh`. This is the only caller change
in the whole plan. *Acceptance:* Bridge RIG UP converges the same systemd units,
verified against `rig_units.py`'s inventory.
**Phase 2 - `--bt-off` / `--bt-on` move to the launcher.** Three lines, plus the
`set -u` ordering care already recorded in `tools/gig-up.sh` (the rfkill call has
to come after the tty colour setup or `${G}` faults). *Acceptance:* default
behaviour byte-identical, `--bt-off` blocks Bluetooth.
**Phase 3 - rename the gate.** `git mv tools/gig-up.sh tools/check-gig.sh`, strip
the launcher bits that phases 1-2 moved out, keep every `run "…"` assertion.
*Acceptance:* the phase-0 goldens still match, modulo the name.
**Phase 4 - one gate, two layers.** `check-gig.sh` calls
`gig-preflight.py --quiet` as its machine layer. *Acceptance:* a count - every
check that existed in either tool still runs, and the total is the sum minus the
known duplicates (perf regime and xruns are checked by both today).
**Phase 5 - the launcher ends with the gate.** Replace the root script's ad-hoc
readiness block (surface state, `check-mix`, `check-drift`, `check-preload`) with
one `check-gig.sh --quiet`. *Acceptance:* a real launch log shows the same
findings, and no check runs twice. This is the phase that changes what PLN sees
when he presses the button, so it wants a calm evening, not a gig day.
**Phase 6 - delete the duplication**, only after phase 5 has run for real.
## What this does not do
- It does not unify the *output style*. The launcher speaks in `• / ✓ / !` steps
and the gate speaks in `run "name"` verdicts. Those are different registers for
different jobs and merging them would cost clarity to buy tidiness.
- It does not touch `gig-preflight.py`'s three design rules (green is not
evidence, every FAIL carries its fix, it never changes anything). Those are the
reason the machine layer is trustworthy, and the merge inherits them for the
whole gate: **`check-gig` mutates nothing**, which is what makes it safe to call
from the launcher on every boot.
- It does not rename `gig-preflight.py`. It becomes the gate's machine layer and
keeps its name, which is referenced from docs and `rig-doctor.py`.
## Risks
- **The boot path.** Mitigated by phase 0 and by never deleting ahead of proof.
- **A third caller we have not found.** `grep -rn 'gig-up'` today finds only
`tools/bridge/rig.py` and comments, but a desktop action or a shell alias could
exist outside the repo. Phase 1 should re-run that grep over `~/.local/share`
and `~/.config` as well as the repo.
- **`--converge` divergence.** Both implementations converge units; if they
disagree about *which*, phase 1 changes behaviour for the Bridge. Compare
against `rig_units.py`, which owns the policy, rather than against each other.
## Why this pays for itself
Today, answering *"is the rig ready?"* means knowing which of three tools to run
and which of two buttons was pressed last. After this it is one command, and the
launcher runs it for you. The failure we hit today - a gate that existed but was
unreachable from the path actually used - becomes structurally impossible, because
there is only one gate and only one launcher, and the launcher calls it.
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