📋 pantheon + revelation briefs — the process docs travel with the repo

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
monsterrobotparty 2026-07-14 13:21:57 +10:00
parent 42c17f83b0
commit 9adbafe342
2 changed files with 319 additions and 0 deletions

189
PANTHEON_BRIEF.md Normal file
View File

@ -0,0 +1,189 @@
# THE PANTHEON — build brief · GODSPEED · GODSQUASH · GODSPEAK · UNGODLY
> **You are an execution agent (Opus).** You have been assigned one lane of a four-feature build. Read **Part 0**, then execute ONLY your lane. Your output is reviewed by the planning model (Fable) before merge; end your run with the §0.8 report. This brief follows the GODRUM process (see `GODRUM_BRIEF.md` for the full history — GODRUM shipped through it in four staged, reviewed lanes).
---
## Part 0 — shared context (every agent reads this)
### 0.1 The four gods
| name | what | where it lives |
|---|---|---|
| **⚡ GODSPEED** | the arpeggiator — hold a chord, the machine runs with it, on godtime with the groove's swing | client, app scope |
| **🫀 GODSQUASH** | the master-bus compressor — one world-modulatable knob that makes the whole planet pump | client, Synth engine |
| **🗣 GODSPEAK** | the voice of god — the user's own wersperr (local MLX Whisper) speaks commands into the sky | Python worker + hub + client |
| **⏪ UNGODLY** | undo — *un-doing what god did*. ⌘Z time-travels the patch | client, app scope |
### 0.2 Ground truth
- Repo `/Users/m3ultra/Documents/godstrument`, branch base = **`main`** (post-GODRUM, ≥ `0fb6410`). The app is one file, `viz/index.html` (~9,100 lines): grimoire prose up top, one giant IIFE, the `Synth` IIFE inside it near the bottom. Vanilla JS + raw Web Audio, no libraries. Python side: `hub.py` (OSC 9000 in → normalize → WebSocket out, port from `config.json`), `run.py` (starts hub + workers; note the `--sports` opt-in-worker precedent), `workers/*.py` (house pattern: argparse `--host/--port`, **lazy imports with graceful "skipping (optional)" exit**, `SimpleUDPClient` to OSC 9000).
- **Line numbers drift — grep for names.** Verified anchors as of `main@0fb6410`: `OMNI.MODULES`/`LABELS` ~39473956 · `handleMessage` ~4357 · `mindStrike` ~5019 (resolves `mindKeys.notes[i]`, fires `Synth.pluck(note, vel)` + `midiPluck(2, note, vel, 320)` — the mind console's own MIDI channel) · `seqTick` ~2922 (16th-note tick, **swing already applied at the clock**, see the GODRUM `fireDrum` precedent inside it) · main keydown handler ~68906907 (taken: `p c o w z t b` + digits `15`; **`g` is free**; NOTE the single-letter handlers currently have **no meta/ctrl guards** — ⌘Z toggles zen today, a pre-existing quirk UNGODLY fixes) · `serializePatch`/`loadPatch` ~3160s · ctx menu `ctxItem(label, fn, kbd)` entries ~7100s · `Synth` IIFE opens ~8400s (`const Synth = (function () {`).
- **GODRUM conventions carry over verbatim**: worktree per agent, emoji commit style, surgical edits only, match the house voice in comments, params normalized 0..1, temporary verify hooks allowed but removed + grep-verified before commit, static-serve `viz/` to verify (`python3 -m http.server 89xx --directory viz`, dismiss `#landing` client-side), §0.7-style regression every time (**zero console errors; `♪ T O B G` and panels all work**), never deploy, never commit db/secrets/`patches/`, never touch `main`.
- **Hard territorial rule from GODRUM's lesson:** if your lane and a parallel lane could both insert at the same seam, they WILL conflict — your brief names your regions; stay inside them, and if you must cross, STOP and flag it in your report instead.
### 0.3 Git
```sh
cd /Users/m3ultra/Documents/godstrument
git worktree add ../godstrument-<LANE> -b pantheon/<branch> main
```
Branches: `pantheon/a-godspeed` · `pantheon/b-godsquash` · `pantheon/c-godspeak-worker` · `pantheon/d-ungodly` · `pantheon/e-closer`.
### 0.8 Report format (same as GODRUM)
1. Branch + worktree + commit(s). 2. What was built + decisions where the brief left room. 3. `git diff main --stat` + the full diff. 4. How you verified (deterministic tests + measurements + regression results). 5. Deviations & open questions. 6. Territory confirmation (hooks removed, grep-clean, nothing outside your regions).
### Stage map
| stage | lanes | parallel-safe because | status |
|---|---|---|---|
| 1 | **α GODSPEED ∥ β GODSQUASH** | α is app-scope-only; β is Synth-IIFE + three named one-line touches α never makes | ✅ APPROVED — α @ 0e17dbf, β @ ea13b2e; trial-merge clean, zero conflicts. β's quadratic makeup `1+sq²` is a reviewer-endorsed deviation (measured: reduction ≈ 5.95·sq² dB); the linear `1+sq·1.4` in B.2 below is superseded — do not re-implement it |
| 2 | **γ GODSPEAK-worker ∥ δ UNGODLY** | γ is Python-only (zero index.html); δ is index.html-only (zero Python) | ✅ APPROVED — γ @ e56e3bc, δ @ d2c8ad6; merged to main @ b8b1188. Reviewer live-verified undo/redo + ⌘Z-no-longer-zen + worker graceful-skip |
| 3 (current) | **ε the closer** | GODSPEAK client grammar + grimoire chapter for all four + manual + cross-feature QA + deploy checklist | full brief below |
Reviews between every stage. Do not start a later stage's work.
---
# Stage 1α — GODSPEED ⚡ (the arpeggiator)
**Territory:** viz/index.html app scope, OUTSIDE the Synth IIFE. You may touch: `mindStrike`, `seqTick` (additive hook, GODRUM's `fireDrum` style), the main keydown handler (the `G` key), one `ctxItem`, your own panel block + CSS block (place the panel function after `closeAmpler`/the AMPLER block; CSS after `#ampler`'s block). You may NOT touch: the Synth IIFE, `OMNI.MODULES`/`LABELS`, anything `squash` (that's β), exportMid, serializePatch.
**A.1 State & capture.**
```js
const arp = { on:false, held:[], rate:1, dir:"up", oct:1, gate:0.8, idx:0, up:true };
// rate: 4=1/4 · 2=1/8 · 1=1/16 · 0.5=1/32 dir: up|down|updown|random oct: 1..3 held: [{note, vel}] cap 8
```
Capture point is `mindStrike(i, vel)`: when `arp.on`, a strike **toggles** `{note: mindKeys.notes[i], vel}` in `arp.held` (latch — strike again to remove; keep the `mindKeys.flash[i] = 1` feedback) and does **not** pluck immediately. When `arp.on` is false, `mindStrike` is byte-identical to today. Since MIDI-in already routes note-ons through `mindStrike`, hardware gets latch capture for free — note that in your report; true hold-mode from MIDI note-offs is a flagged future upgrade, not v1.
**A.2 The clock.** Ride `seqTick()` — one additive call at its tail (like `beatRefresh`): `arpTick()`. Inside: bail unless `arp.on && arp.held.length`; a 16th-counter fires when `counter % arp.rate === 0` for rates ≥ 1; for 1/32, fire twice (immediate + `setTimeout(sub/2)`, `sub = 60000/bpm/4`, the ratchet idiom). Because seqTick fires at the swung onset, **GODSPEED inherits the groove** — that's the whole soul of the feature; verify it.
**A.3 Note selection.** Pool = `held` sorted ascending, expanded across `oct` octaves (`note + 12*o`). `up`/`down` cycle; `updown` ping-pongs without repeating the turnaround note; `random` picks uniformly. Fire via `Synth.pluck(note, vel)` + `midiPluck(2, note, vel, durMs)` where `durMs = sub * arp.rate * arp.gate` (clamped ≥ 30) — channel 2, the mind console's channel, exactly like `mindStrike`.
**A.4 UI.** `G` key + `ctxItem("⚡ GODSPEED — hold a chord, the machine runs", …)` toggle a compact `#godspeed` panel (house style, model on the beat header row): **on/off** (`.midibtn.on`) · rate select (1/4 1/8 1/16 1/32) · dir select · oct (13) · gate slider · a held-notes readout (note names — there's a `midiToName` in scope; grep it) · **clear** button. Arp toggled off → `held` cleared, `idx` reset. Add the `G` handler beside `B` (~6906); verify `g` is truly unbound first (grep, incl. the capture-phase keymap handler — digits precedent from GODRUM lane E).
**A.5 Verify.** Deterministic: drive `seqTick` (hook the internals temporarily), spy `Synth.pluck` — assert the exact note order for up/down/updown/random over 2 bars at each rate; 1/32 double-fires; gate scales `durMs`; swing: with "mpc 8-4" the off-16th fires late (assert via the seqTick call time, or by construction + note it). Live: open panel via `G` and the sky menu, latch three mind-mode notes, ▶ nothing needed — the arp runs on the clock; confirm audible + zero console errors + §0.7 regression + **GODRUM untouched** (beat panel plays, keys 15 still finger-drum — they must NOT feed the arp).
---
# Stage 1β — GODSQUASH 🫀 (the master squeeze)
**Territory:** inside the Synth IIFE, PLUS exactly three named external touches: (1) the `OMNI.MODULES` "fx rack" `keys` array — append `"squash"`; (2) `OMNI.LABELS` — add `"squash": "squash"`; (3) wherever `crush` is structurally registered as a destination in viz/index.html (grep `"crush"` and mirror EVERY structural appearance — dest declaration/registry, matrix nodes if any — so `squash` is routable by the world exactly like `crush`; `crush` is NOT in config.json, so config.json stays untouched unless your grep proves otherwise). Nothing else outside the IIFE.
**B.1 The chain.** In `build()`, where the master currently does `out.connect(lim)`: insert `out → comp → makeup → lim` (`comp = ctx.createDynamicsCompressor()`, `makeup = ctx.createGain()` at 1). Return `comp` and `makeup` on `N` (as `squash`, `squashMakeup`). Do NOT move the GODSONIQ tap (`N.out.connect(recNode)` happens later in `loadWorklets` and correctly stays pre-squash — captures are pre-limiter by design; say so in a comment). Set static: `knee 12`, `attack 0.004`.
**B.2 The one knob.** In `params()`:
```js
const sq = c01(d("squash", 0)); // 0 = transparent (no route, no squeeze)
S(N.squash.threshold, -6 - sq * 30, 0.1); // 6 → 36 dB
S(N.squash.ratio, 1 + sq * 7, 0.1); // 1:1 → 8:1
N.squash.release.setTargetAtTime(0.12 + sq * 0.13, t, 0.2);
S(N.squashMakeup.gain, 1 + sq * 1.4, 0.1); // gain-compensate the squeeze — tune this by measurement
```
Tune the makeup curve so perceived loudness stays roughly level while crest factor drops — measure, don't guess.
**B.3 Verify.** Offline renders (the house method): a busy program (pad + drums via `Synth.drum` if merged base has them — it does) at `squash` 0 / 0.5 / 1.0 → show **crest factor (peak/RMS) falls monotonically**, RMS roughly held by makeup, no clipping into the limiter (peak ≤ pre-squash peak + ε). **Null-ish test at 0:** ratio 1 → output within ~0.5 dB of a bypass render (DynamicsCompressor has fixed lookahead latency — compare envelopes, not sample-aligned nulls, and note the latency in your report). Live: OMNI shows the new `squash` knob in the fx rack; dragging it audibly pumps; a world route onto `squash` modulates it. §0.7 regression + GODRUM regression (drums still balanced — squash defaults to 0).
---
# Stage 2γ — GODSPEAK worker 🗣 (Python only — ZERO viz/index.html)
**Territory:** new file `workers/godspeak_worker.py`, a small addition to `hub.py`'s `_osc_handler` (+ broadcast), an opt-in flag in `run.py`. Nothing else. **This feature is local-rig only**: MLX Whisper is Apple-Silicon-only, so it runs where the user's Mac runs the hub — the VPS simply never has it (like the ToF/vision workers). Say this in the file's docstring.
**C.1 The worker.** Adapt the user's wersperr (`ssh://git@100.71.119.27:222/monster/wersperr.git`, 99 lines — clone it read-only and study it; model: `mlx-community/whisper-large-v3-turbo`, 16 kHz mono, hold **Right ⌥** via pynput, transcribe on release, ignore < ⅓ s). Changes from wersperr: instead of pbcopy/paste, send OSC to the hub — `/godspeak/rec 1` on key-down, `/godspeak/rec 0` on release, `/godspeak <text>` after transcription. House worker pattern throughout: argparse `--host 127.0.0.1 --port 9000`, **lazy imports** (`mlx_whisper`, `sounddevice`, `pynput`, `numpy`) with the graceful `"godspeak worker: … not available — skipping (optional)"` exit, warm-up transcribe of silence on boot, `--check` self-test. Deps do NOT go in requirements.txt (VPS must stay clean) — document `pip install mlx-whisper sounddevice pynput` (into the venv, Mac only) in the docstring.
**C.2 The hub.** In `_osc_handler`, special-case the `/godspeak` addresses (they're text/state, not norm signals — mirror how non-signal messages like the patch list reach clients): broadcast `{"godspeak": {"text": <str>}}` and `{"godspeak": {"rec": 0|1}}` to all ws clients immediately. Respect the hub's **read-only mode** (~line 114): in read-only, drop godspeak entirely (it's a command channel).
**C.3 run.py.** `--godspeak` opt-in flag exactly like the `--sports` precedent (~line 132): starts the worker, prints a one-liner. Never in the default set.
**C.4 Verify.** `--check` passes on this Mac (model loads, silence transcribes). With hub running locally: hold Right ⌥, say something, show the hub log/ws broadcast carrying the text (a 5-line ws test client is fine). Graceful-skip path: run with the imports missing (e.g. `python3 -m venv` bare env) → clean skip, exit 0. Confirm `git diff main --stat` touches ONLY the three Python files.
---
# Stage 2δ — UNGODLY ⏪ (undo — viz/index.html only, ZERO Python)
**Territory:** viz/index.html app scope, outside the Synth IIFE. Touches: a new snapshot module (place after `loadPatch`), `ungodlyMark(label)` calls at the mutation sites listed below, the keydown handler (⌘Z/⌘⇧Z **plus meta-guards on the existing single-letter keys** — today ⌘Z toggles zen; that quirk dies in this lane), one `ctxItem`. Do NOT touch: the Synth IIFE, GODSPEED's/AMPLER's blocks, exportMid.
**D.1 The engine.** Undo = time-travel over `serializePatch()``loadPatch` already restores everything (routes, tweaks, seqs incl. drums, arr, bpm, groove, scale, kit) and rebuilds open panels. So:
```js
const ungodly = { past: [], future: [], muted: false, MAX: 32, lastT: 0, lastLabel: "" };
function ungodlyMark(label) { // call BEFORE a mutation applies — the snapshot is the world as it was
if (ungodly.muted) return;
const s = JSON.stringify(serializePatch());
if (ungodly.past.length && ungodly.past[ungodly.past.length - 1].s === s) return; // no empty history
ungodly.past.push({ s, label }); if (ungodly.past.length > ungodly.MAX) ungodly.past.shift();
ungodly.future.length = 0;
}
function ungodlyGesture(label) { // continuous drags: one mark per gesture burst (same label within 800ms = same gesture)
const tNow = performance.now();
if (label === ungodly.lastLabel && tNow - ungodly.lastT < 800) { ungodly.lastT = tNow; return; }
ungodly.lastLabel = label; ungodly.lastT = tNow; ungodlyMark(label);
}
```
Undo pops `past` → pushes the CURRENT state onto `future``muted = true; loadPatch(JSON.parse(p.s)); muted = false` → ticker `"⏪ ungodly: " + p.label`. Redo mirrors. Empty → ticker `"⏪ nothing to take back — god has done nothing yet"`. Memory-only v1 (reload clears history — note it).
**D.2 The mark sites** (find each by name; `ungodlyMark` for discrete acts, `ungodlyGesture` for drags — always BEFORE the mutation):
route patch/unpatch via the UI (the cable actions + the ctx-menu route items — NOT `registerRoute` itself, which `loadPatch` calls) · `applyMood` entry (`"mood: " + name`) · dimension/patch load + vibe apply (`"loaded <name>"`) · zero enter and exit · groove preset clicks + groove sliders (gesture) in BOTH the beat panel and the arranger grooves tab · euclid fills (both panels) · beat-panel step toggles (gesture, `"beat edit"`) + expression-lane drags (gesture) + kit-knob drags (gesture) · arranger clip-cell cycles (gesture) · scale root/name changes · `resetTweaks` and mute/solo toggles (mark). Anything you find that mutates patch state and isn't listed: add it and report it.
**D.3 Keys & menu.** In the keydown handler: FIRST, `if (ev.metaKey || ev.ctrlKey) { if z → undo (shift → redo), preventDefault, return; }` — placed before the single-letter block so ⌘Z no longer reaches zen; then add `!ev.metaKey && !ev.ctrlKey` guards to the existing letter handlers (p/c/o/w/z/t/b/g — small, mechanical, in-territory). `ctxItem("⏪ UNGODLY — take it back (⌘Z)" + depth count, …)`.
**D.4 Verify.** Deterministic script: draw beat steps → apply a mood → patch a route → three undos restore each state exactly (compare `JSON.stringify(serializePatch())` to the recorded snapshots) → three redos return → new action after undo clears `future`. Gesture-debounce: a 20-event slider drag = ONE history entry. `muted` holds: undo itself creates no marks. ⌘Z does NOT toggle zen; plain `z` still does. Depth cap at 32. §0.7 + GODRUM regression, zero console errors.
---
# Stage 3ε — the closer (FINAL BRIEF)
> **You are Agent ε (Opus).** All four gods are built and merged to `main @ b8b1188`: GODSPEED (arp, `G`), GODSQUASH (`squash` dest + OMNI knob), the GODSPEAK worker (OSC `/godspeak` + `/godspeak/rec` → ws `{"godspeak":{text|rec}}`), and UNGODLY (⌘Z/⌘⇧Z, `ungodlyMark`/`ungodlyGesture`/`ungodlyUndo` in app scope). You close the pantheon: the voice grammar, the polish list, the grimoire chapter, the QA matrix, the deploy checklist. Branch `pantheon/e-closer` from `main` (confirm `arpTick`, `squash`, `godspeak_worker.py`, and `ungodlyUndo` are all in your base). Part 0 rules and §0.8 report bind.
## E.1 The GODSPEAK client (viz/index.html)
In `handleMessage` (grep it), add the `godspeak` branch:
- `msg.godspeak.rec` → a small 🗣 indicator pulses while recording (a fixed chip near the ♪ button, or a ticker line — house style, your call; it must appear on 1 and clear on 0).
- `msg.godspeak.text` → normalize (lowercase, strip punctuation, map number-words to digits over the 40180 range — a small word-number table: "ninety" → 90, "one twenty" → 120) and run the grammar. **First match wins; every executed command lands in the ticker as `🗣 <what happened>`; unmatched text falls through to the ticker as `🗣 "<text>"` — the word arrives in the world, never an error.**
Grammar (be forgiving with filler words — match on keywords, not exact phrases):
| utterance contains | action |
|---|---|
| `tempo`/`bpm` + N (digits or words) | `godtime.bpm = clamp(N, 40, 180)` unless `godtime.locked` (then ticker "godtime is locked") |
| `mood` + a fuzzy `MOODS` key (or a bare mood name) | `applyMood(match)` |
| `play`/`start` + `beat` · `stop` + `beat` | flip all five `drum.*` lanes on/off (reuse the beat panel's master-play logic — extract or mirror it) |
| `open`/`close` + `beat`\|`tracks`\|`omni`\|`ampler`\|`keys`\|`godspeed` | the matching panel toggle |
| `capture` (+ N `seconds`)? | `Synth.amplerArm(N || 3)` — if it returns false, ticker the same "press ♪ first" hint AMPLER uses |
| `chop` + `eight`/`sixteen`/8/16 | `Synth.amplerChop(n)` + re-render the AMPLER panel if open |
| `skin` + fuzzy skin name | `setSkin(match)` |
| `zen` | `setZen(!zenMode)` |
| `squash` + N `percent` | set the squash trim: `setParamLocal("squash","offset", N/100)` (register the dest first if needed) |
| `take it back` / `undo` | `ungodlyUndo()` — the voice of god, un-doing |
Fuzzy matching: lowercase-prefix or substring against the real key lists (`MOODS`, `SKINS`) — never invent names; no match → fall through to the ticker. **Voice commands are user acts: let the actions they call fire their existing `ungodlyMark`s naturally (applyMood already marks; don't double-mark).**
## E.2 The polish list (small, reviewed carry-overs — all in-scope)
1. Prune GODSPEED's dead `arp.up` state field (α flagged it).
2. Apply `groove.velo` velocity-deviation to arp notes in `arpFire` (mirror the drum branch's line) — the arp should breathe like the lanes do.
3. `ungodlyMark("tempo")` on the beat panel's BPM input commit (the one known unmarked mutation).
4. GODSPEAK worker docstring: add the two macOS one-time permissions (Microphone + Accessibility/Input Monitoring for pynput) — mirror wersperr's README note.
## E.3 The grimoire chapter (canon) + manual
Four `<li>` entries alongside the GODRUM chapter (same placement pattern Agent F used, before the stage; house voice; fact-check every claim against what you verified):
- **⚡ GODSPEED** — press `G`; enter a chakra, turn it on, strike keys to latch a chord; it runs on godtime and *swings with the groove*; rate/direction/octaves/gate; a MIDI keyboard latches too (it rides channel 2 out).
- **🫀 GODSQUASH** — the master squeeze in OMNI's fx rack; one knob, threshold dives and ratio climbs together, loudness stays level while the peaks kneel; route the world onto it and the planet pumps the mix.
- **🗣 GODSPEAK** — the voice of god (local rig only, Apple Silicon): `run.py --godspeak`, hold Right ⌥, speak — "tempo ninety", "storm mood", "capture six seconds", "chop sixteen", "take it back"; unmatched words arrive in the ticker; nothing leaves the machine (the user's own local Whisper).
- **⏪ UNGODLY** — ⌘Z takes it back, ⌘⇧Z re-does; thirty-two acts deep; every cable, mood, groove, kit knob and beat step; memory-only (a reload forgives everything).
Then `python3 build_manual.py` and commit the regenerated `viz/manual.html` (the Stage-4/F exception applies to closers).
## E.4 Cross-feature QA matrix
1. **Voice→everything** (drive `handleMessage` directly with synthetic `{godspeak:{text}}` messages — the worker needs a mic+model, so the ws path is verified by γ already): tempo, mood, play/stop beat, open/close each panel, capture/chop (audio unlocked first), skin, zen, squash N percent, **"take it back" undoes the mood it just set**. Unmatched → ticker verbatim. Junk/number-word parsing table.
2. **Undo × the new gods:** arp latch isn't in the patch (transient — confirm undo doesn't clear held notes); squash trim set by voice → ⌘Z reverts it (it's a tweak).
3. **GODSPEED × GODRUM:** arp + beat playing together, both swung, keys 15 don't latch, `G`/`B` panels coexist.
4. **GODSQUASH × GODRUM:** squash at 0.5 while the beat plays — offline-render crest drop, no limiter clip.
5. Full §0.7 regression (`♪ T O B G` + ⌘Z/⌘⇧Z + zen-not-on-⌘Z) — zero console errors.
6. `rec` indicator appears/clears on synthetic `{godspeak:{rec:1|0}}`.
## E.5 Deploy checklist (prepare, do NOT execute) & deliverables
Same shape as GODRUM's F.4: merge main is already done for stages 12, so: your branch → `main` (reviewer does it) · rsync per CLAUDE.md (briefs auto-excluded; `manual.html` ships; `workers/godspeak_worker.py` ships but is inert on the VPS — graceful skip) · restart · cold-start wait · smoke-test godstrument.pro (`B`,`G`, ⌘Z, OMNI squash knob, manual chapter) · note GODSPEAK is local-rig-only so the live site's voice test is `run.py --godspeak` on the Mac. Commit(s) on `pantheon/e-closer`; §0.8 report + the E.4 matrix ✓/✗ + the E.2 list each done/deviated.

130
REVELATION_BRIEF.md Normal file
View File

@ -0,0 +1,130 @@
# REVELATION — build brief · TESTAMENT · WHEELS · PSALM · CANON
> **You are an execution agent (Opus).** One lane of a four-feature wave. Read **Part 0**, execute ONLY your lane, end with the §0.8 report (defined in `GODRUM_BRIEF.md`, our process root — GODRUM and THE PANTHEON both shipped through this loop). The planning model (Fable) reviews every stage before merge.
---
## Part 0 — shared context
### 0.1 The four revelations
| name | what | maps to (the DAW-study gap) |
|---|---|---|
| **📼 TESTAMENT** | record the performance — the master mix to a file, one button | a live instrument that can't record itself |
| **☸ WHEELS** | per-lane pattern lengths (116) — polymeter, the wheel within a wheel | flexible MIDI/pattern editing |
| **🕊 PSALM** | sing to the instrument — mic pitch, quantized to the scale, played as notes (or latched into GODSPEED) | vocal integration, Godstrument-style |
| **✦ CANON** | factory dimensions — curated patches that ship with the instrument | Logic's stock-content advantage |
### 0.2 Ground truth & hard gate
- **GATE: do not branch until PANTHEON Stage 3ε has merged to `main`.** Confirm your base has ε's work (`git log --oneline -5` shows the ε commit; grep `godspeak` reaches a `handleMessage` branch). If ε hasn't landed, STOP and report. All line anchors below were verified at `main@b8b1188` (pre-ε) — **they WILL have drifted; grep for names, never trust raw numbers.**
- One-file app `viz/index.html` (~9.4k lines): app IIFE with the `Synth` IIFE near the bottom (`const Synth = (function () {`). Python side: `hub.py` (OSC→ws), `run.py`, `workers/`. Conventions, worktrees, commit style, §0.7 regression (zero console errors; `♪ T O B G` + ⌘Z all work), temp-hook discipline, never-deploy/never-main: all per `GODRUM_BRIEF.md` Part 0 — read it.
- Relevant machinery you inherit: the groove clock (`frame()` advances `seqPhase`; on each new 16th `seqCurStep = st16; seqTick()`), `seqs` (`{on, steps[16], vel, chance, rat, lock}` via `seqFor`/`drumSeqFor`, normalized by `migrateSeq`, serialized wholesale by `serializePatch`), the beat panel (`openBeat`, lanes, `beatRefresh`), the arranger grooves tab, `exportMid()` (ch-9 drum block added by GODRUM-E), `Synth.pluck(midi, vel)`, GODSPEED's `arp`/`arpLatch(note, vel)`, UNGODLY's `ungodlyMark`/`ungodlyGesture` (mark BEFORE mutating), the master chain `out → squash → squashMakeup → lim → destination` (~8585 pre-ε), the `Recorder` AudioWorklet (fixed-length captures — NOT what TESTAMENT uses), `gScale`/`SCALES`/`scaleRows`, and the hub's `_scan_patches` (serves named template patches from a dir to the client's template select).
- Branches: `revelation/t-testament` · `revelation/w-wheels` · `revelation/p-psalm` · `revelation/c-canon` · `revelation/o-closer` (ω), each from post-ε `main`.
### Stage map
| stage | lanes | parallel-safe because |
|---|---|---|
| 1 | **T 📼 ∥ W ☸** | T = Synth IIFE + one header button; W = app-scope sequencer/panels/exportMid — disjoint, and neither touches ε's `handleMessage` |
| 2 (after 1 merges) | **P 🕊 ∥ C ✦** | P = index.html only (new self-contained block); C = Python + a new `factory/` dir + JSON — file-disjoint |
| 3 (after 2 merges) | **ω the closer** | grimoire chapter (all four), manual, cross-feature QA, deploy checklist |
Reviews between stages. Do not start later-stage work.
---
# Stage 1T — 📼 TESTAMENT ("it is written")
**Territory:** inside the `Synth` IIFE, PLUS: one header button (`#testament`, created app-scope beside the ♪ button — note the ♪ button itself is DOM built *inside* the Synth IIFE at its tail; mirror that pattern and build yours there too, keeping you 100% IIFE-resident), one `ctxItem`, one small CSS block. Nothing else.
**T.1 The tap.** Recording captures the TRUE final mix — post-squash, post-limiter. In `build()`: expose `lim` on the returned `N` (one word added to the return object). The recorder itself is lazy:
```js
let testDest = null, testRec = null, testChunks = [], testT0 = 0, testTimer = null;
function testamentStart() {
if (!ctx) start(); // wake the graph — a recording can begin the session
if (testRec) return false; // already rolling
if (!testDest) { testDest = ctx.createMediaStreamDestination(); N.lim.connect(testDest); }
const mime = ["audio/webm;codecs=opus", "audio/webm", "audio/mp4"]
.find(m => window.MediaRecorder && MediaRecorder.isTypeSupported(m));
if (!mime) return false; // (Safari fallback lands on audio/mp4)
testChunks = []; testRec = new MediaRecorder(testDest.stream, { mimeType: mime, audioBitsPerSecond: 256000 });
testRec.ondataavailable = e => { if (e.data && e.data.size) testChunks.push(e.data); };
testRec.start(1000); // timesliced — a crash loses ≤1s
testT0 = ctx.currentTime; return true;
}
function testamentStop() { ... } // → Promise<{blob, secs, ext}> resolved in onstop
function testamentState() { return { on: !!testRec, secs: testRec ? ctx.currentTime - testT0 : 0 }; }
```
Export `testamentStart/Stop/State` on the Synth return object. The worklet `Recorder` (GODSONIQ/AMPLER) is untouched — different machinery, different purpose.
**T.2 The button.** `#testament` next to `#sound` (♪): shows `⏺`; while rolling it turns red (`.on`), pulses gently, and shows elapsed `m:ss` (update via a 1s interval only while rolling). Click toggles. On stop: build the file name `godstrument-testament-<yyyymmdd-hhmm>.<ext>` (derive the timestamp from `new Date()` at stop time), trigger the download (the `a[download]` + `URL.createObjectURL` idiom), and ticker: `📼 testament: <m:ss> — it is written`. Failure (no MediaRecorder) → ticker `📼 this browser cannot bear witness`, button stays off. Add `ctxItem("📼 TESTAMENT — record what god does", …)` near GODSONIQ.
**T.3 Verify.** Live: record ~6s of the sounding world (drums playing), stop → blob size > 0, elapsed matches ±0.5s, and **prove the audio is real**: load the blob into an `<audio>` element and play it, or decode it back through `decodeAudioData`/an offline probe and measure nonzero RMS. Record-while-♪-off (graph wakes, captures the beat you then start). Two consecutive recordings (state resets). §0.7 regression, zero console errors. Note honestly what the headless env can't do (audition by ear).
---
# Stage 1W — ☸ WHEELS ("a wheel within a wheel")
**Territory:** viz/index.html app scope, OUTSIDE the Synth IIFE: the seq factories (`seqFor`, `drumSeqFor`), `migrateSeq` (+ its self-check), the clock lines in `frame()`, `seqTick`, the S&H block in `computeDests`, `applyGroove` call sites, the beat panel + arranger grooves tab (lane-length UI + playhead paint), `exportMid`'s drum block. NOT the Synth IIFE, NOT `handleMessage`, NOT GODSPEED's arp block.
**W.1 The data.** Every seq gains `len` (int 116, default 16): add to both factories; `migrateSeq` clamps/defaults it (`Number.isFinite` → round → clamp 1..16, else 16) and the self-check gains one assertion (a len-12 seq round-trips; a len-less legacy seq gets 16). It rides `serializePatch` automatically — verify, don't assume.
**W.2 The clock.** Add ONE global absolute step counter: `let seqAbsStep = 0;`, incremented exactly where `seqCurStep` is assigned in `frame()` (`seqCurStep = st16; seqAbsStep++; seqTick();`). Then one tiny helper next to `seqFor`:
```js
const wheelStep = (sq) => (sq && sq.len && sq.len < 16) ? seqAbsStep % sq.len : seqCurStep;
```
Replace the per-seq step index in: `seqTick`'s drum branch (`const cur = wheelStep(sq)`), `seqTick`'s note branch, the S&H sampling in `computeDests`, and every `applyGroove(sq, step, v)` call site (grep them — pass `wheelStep(sq)`). A full-length lane (`len` 16) must behave **bit-identically to today** (the helper returns `seqCurStep` — that's the regression guarantee).
**W.3 The UI.** Per-lane length control in BOTH grid editors, minimal and house-style:
- Beat panel: a small `16`-showing number chip in each lane's lead (beside ⬢); click-drag or click-to-edit 116; `ungodlyGesture("wheel")` before mutating. Cells at index ≥ `len` render dimmed (a `.off` class, ~35% opacity) and clicking them does nothing; the playhead for that lane paints at `wheelStep(sq)`.
- Arranger grooves tab: the same control in the euclid row for the selected lane; the big grid dims ≥ `len` and its playhead uses `wheelStep`.
- Euclid fills should fill within `len` (pass `len` as the default `len` argument where the lane's control is present — keep the explicit euclid-steps input working as an override).
**W.4 Export.** In `exportMid`'s ch-9 drum block: replace the `bar × 16-step` nesting with one absolute loop — `for (let sa = 0; sa < arr.bars * 16; sa++) { const s2 = sq.len < 16 ? sa % sq.len : sa % 16; … tick = sa * TICKS16 … }` — so a 12-wheel hat exports exactly as it plays. Note lanes stay as-is (they're arranged by clips; polymeter for note clips is a flagged future, not v1).
**W.5 Verify.** Deterministic: kick len 16 + hat len 12 → drive 48 ticks (LCM) and assert the hat's fire pattern wraps at 12 while kick wraps at 16 (spy `Synth.drum` or the branch internals via a temp hook); len 1 fires every step; legacy patch (no len) loads → all 16, identical behavior to pre-WHEELS (byte-level: same fires over 32 ticks). S&H on a source with len 8 samples on the 8-wheel. UI: dimmed cells, per-lane playheads diverge visibly, undo reverts a len change. Export: byte-parse a 12-wheel pattern → note-ons at ticks matching `sa % 12`. §0.7 + GODRUM/PANTHEON regression, zero console errors.
---
# Stage 2P — 🕊 PSALM ("sing, and the instrument answers")
**Territory:** viz/index.html only — one new self-contained block (place after the GODSPEED block), one `ctxItem`, one small CSS chip. Nothing in the Synth IIFE; you consume `Synth.pluck` and `arpLatch` (both exist).
**P.1 The listener.** Mic analysis lives in its OWN throwaway `AudioContext` — the mic never enters the music graph (zero feedback risk; say so in a comment). `getUserMedia({ audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false } })``AnalyserNode` (fftSize 2048) → a `setInterval(…, 25)` loop (NOT rAF — hidden tabs freeze it) pulling time-domain frames.
**P.2 The pitch.** Autocorrelation in plain JS (house-idiomatic, no libraries): normalize the frame, compute ACF over lags spanning 801000 Hz, pick the first peak above 0.9 × max with parabolic interpolation; reject frames with RMS below the sensitivity threshold or ACF clarity < ~0.6. Convert `freq → midi` (float). Write the detector as a **pure function of a Float32Array** (`psalmDetect(buf, sampleRate) → {freq, clarity, rms}`) so it's unit-testable without a mic.
**P.3 The note.** Quantize the float midi to the global scale — reuse the instrument's own machinery (`gScale` + `scaleRows`/the quantize helpers — grep how the piano roll folds to scale and mirror it; nearest scale degree across octaves). Note-on logic: gate opens when RMS > threshold AND the same quantized note holds for 2 consecutive frames → fire ONCE; re-fire only when the quantized note *changes* (legato) or after the gate closes (RMS below threshold for ~150ms) and reopens. Velocity from RMS (clamped 0.31). Where it goes: **if `arp.on` → `arpLatch(note, vel)`** (sing your chord into GODSPEED, one note at a time — the killer demo); **else → `Synth.pluck(note, vel)`**.
**P.4 The UI.** `ctxItem("🕊 PSALM — sing, and the instrument answers", …)` toggles it (requests the mic on first enable; denial → ticker `🕊 the instrument cannot hear you — allow the microphone`, state off). While on: a small fixed chip (near the launcher stack) showing 🕊 + the live note name (`midiToName`), dimmed when the gate is closed; clicking the chip toggles off. A sensitivity slider on the chip's hover/click is optional — a sane fixed threshold with a `PSALM_SENS` const is acceptable v1. Privacy note in a comment: local analysis only, nothing leaves the page.
**P.5 Verify.** The detector is pure — unit-test it headlessly: synthesize Float32Array sines (220, 440, 554.37 Hz + a noisy one + silence) → assert freq within ±1%, clarity high, silence rejected; run the note-logic as a pure state machine on scripted `{freq, rms}` sequences → assert single-fire, legato re-fire, gate close/reopen, correct scale-fold (set `gScale` to minor pent, feed 445 Hz → the folded degree). Live mic = human lane; say so. UI chip renders/toggles; mic-denied path degrades cleanly. §0.7, zero console errors.
---
# Stage 2C — ✦ CANON (factory dimensions)
**Territory:** Python + data only: `hub.py` (`_scan_patches` region), a NEW git-tracked `factory/` directory of patch JSONs, `run.py` only if genuinely needed (it shouldn't be). ZERO index.html.
**C.1 The shelf.** Today `_scan_patches` serves user patches from the gitignored `patches/` dir. Add a second scan of `factory/` (git-tracked, ships with the deploy — confirm the CLAUDE.md rsync filter doesn't exclude it; it doesn't) whose entries appear in the same `patchList` with names prefixed `✦ ` (e.g. `✦ first light in stereo`). Factory patches are read-only: the hub's save path must never write into `factory/` (guard by name prefix). Client-side needs nothing — the template select already renders whatever `patchList` carries.
**C.2 The canon itself.** Author **four** patches as JSON (the `serializePatch` shape: `routes, tweaks, seqs, arr, bpm, groove, scale, kit`). Derive the exact shape by reading `serializePatch`/`loadPatch` and one existing saved patch if available. The four, each a one-line lesson in an instrument feature:
1. **`✦ the heartbeat`** — a GODRUM showcase: mpc 8-4 groove, a tasteful boom-bap kit (use the seeded pattern as a base, vary it), kick duck 0.5, bpm 88.
2. **`✦ the wheel within the wheel`** — WHEELS showcase: kick len 16, hat len 12, clap len 10, sparse, hypnotic, bpm 96. (Gate: only author this if WHEELS has merged — it has, you're Stage 2.)
3. **`✦ the squeeze`** — GODSQUASH showcase: a route from a pumping world source onto `squash` at high amount, drums under it, bpm 120.
4. **`✦ quiet arrival`** — a gentle first-light-style ambient patch: earth echo audible, sparse routes, no drums, slow bpm.
Validate each by round-tripping through the client's `migrateSeq` expectations (16-step arrays, sane ranges) — write a tiny `--check`-style python validator in the lane (a `test_factory.py`-named file so the deploy filter excludes it) that asserts shape.
**Taste disclaimer in your report:** these are structural drafts for the human to re-voice by ear; correctness of shape is your deliverable, beauty is his.
**C.3 Verify.** Run the hub locally (`--profile minimal`), confirm `patchList` carries the four `✦` names, load one via the client template select in a browser (full path), confirm the read-only guard (attempt a save named `✦ x` → rejected/redirected), and that a missing/empty `factory/` dir degrades silently. Zero index.html changes (`git diff --stat`).
---
# Stage 3ω — the closer (brief finalized after Stage 2 review)
Scope preview: grimoire chapter for all four (Ezekiel 1:16 belongs in the WHEELS entry — *"as it were a wheel in the middle of a wheel"*; PSALM's privacy line — the instrument listens locally, nothing leaves the machine; TESTAMENT's "it is written"; the CANON as the shelf of worked examples) · `build_manual.py` + commit manual · cross-feature QA (record a TESTAMENT of a WHEELS beat while PSALM latches the arp — the whole wave in one take) · deploy checklist. Do not start early.