architecture: the file map (ES modules, no build step) and the harness modes as they are now

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
This commit is contained in:
teahubxyzandClaude Fable 5.1 committed 2026-10-21 22:34:48 +00:00
1 parent 4bbe457421
commit db33db4673
1 file changed
+37 -8
+37 -8
View File
@@ -1,7 +1,32 @@
# How g*ndora fits together
One page, one script, one worker. This is the map: what each part owns, how a track moves through the system, and
the invariants that the playback code relies on. Keep it current when you change any of them.
One page, a handful of ES modules, one worker. This is the map: what each part owns, how a track moves through the
system, and the invariants that the playback code relies on. Keep it current when you change any of them.
## File map
Plain `<script type="module">`, static files served as-is, no build step. `index.html` is markup + CSS + two script
tags (`app.js` as a module, `brain.js` as a classic script with `defer`, so it runs after the module).
| File | Owns |
|---|---|
| `core.js` | `state` (incl. the one `<audio>`), `hooks`, `$`, `LS`, `toast`, `sleep`, `asDataUrl`, `mmss`, the job counter `ids` |
| `covers.js` | pure: `hash32`, `prng`, `keyHue`/`KEYS`, `palette`/`PALETTES`, `drawCover` and the looks, `drawMark`, `drawOnAir` |
| `player.js` | the state machine and everything that touches `state.audio`: `setSource`, `toCooking` … `toIdle`, `playJob`, `advance`, `step`, `prime`, the crackle/bed, `SILENCE`, scrubbing, Media Session, the analyser, the transport buttons |
| `render.js` | `venice()` behind an injectable fetcher (`setFetcher`), `renderTrack`, `speak` is dj's; `makeNoise`/`noiseSpec`, `makeTone`/`toneSpec`, `wavBlob` |
| `dj.js` | `djPrompt`/`askDJ`, `askBreak`, `askMash`, `DJ_SCHEMA`, modes, breaks, voices, personas, `speak` |
| `store.js` | `db` (STORAGE.md), `saveStation`, `remember`, `logEvent` (the ledger), tapes incl. the virtual KEPT tape, `tapeSpec` |
| `mashup.js` | `mashReference` (OfflineAudioContext) and `mashup` |
| `ui/player.js` | cover/title/tags/lyrics, keep/share, the trail, ticks, reactions, emoji tray + picker, sheets scrim |
| `ui/library.js` | the stations row, the library view, now-playing bar, tapes |
| `ui/sheet.js` | the station sheet |
| `ui/taste.js` | the taste view (`reactionWeight`, `drawTaste`, `openTaste`) |
| `app.js` | boot: `openStation`, `makeJob`/`cookNext`, links and share, SW + update pill, theme, the key box; sets `window.g` and the globals brain.js uses |
| `brain.js` | unchanged: a classic script reading `hooks`, `state`, `db`, `$`, the cover/taste drawers and `seq` as globals |
`window.g = { state, openStation, cookNext, crackle, hooks, db, wavBlob, $, hash32, render: { setFetcher } }` is for
tooling (the harness fragments). The modules import each other in a cycle (`player` ↔ `ui/*` ↔ `app`), which is fine
because only function declarations cross it at load time; `core.js` and `covers.js` import nothing.
## The parts
@@ -107,17 +132,21 @@ rseed (render seed), cost, renderer, reaction, hits, guess, break/sponsored/spok
`tools/harness.sh` loads the live page with the backend stubbed (DJ returns a canned sheet, render returns a generated
wav after a configurable delay) and prints one state line per step. Two modes:
sh tools/harness.sh # tools/harness.html: the station loop, autoplay allowed, from file://
sh tools/harness.sh # tools/harness.html: the station loop, autoplay allowed
sh tools/harness.sh resume # tools/harness-resume.html: resume a stored station, autoplay blocked
sh tools/harness.sh safari # the loop in WebKit (safaridriver), with a real click as the activation
The loop drives the player through: first play, pre-cook, handoff on end, the crackle hold and its handover,
pause/resume, skip. It runs under headless Chrome's virtual time (`--dump-dom --virtual-time-budget`), so it is quick
and deterministic.
pause/resume, skip. All modes serve the page and the modules from a temp folder on 127.0.0.1 (modules and IndexedDB
both need http) and run in real time: Chrome over the DevTools protocol (`tools/harness-cdp.py`, python3 stdlib
only), WebKit over safaridriver (`tools/harness-safari.py`). The fragments are inline modules, so they run after
`app.js`, reach the page through `window.g` and stub the backend with `render.setFetcher`.
The resume mode seeds IndexedDB with a station and one rendered track, calls `openStation` with no user gesture and
expects the TAP TO PLAY wait (`state.needTap`, `#cover.tap`) with the next track cooked behind it. IndexedDB needs
http, so the page is served from a temp folder on 127.0.0.1; and virtual time does not wait for IndexedDB, so this
mode is driven in real time over the DevTools protocol by `tools/harness-cdp.py` (python3 stdlib only).
expects the TAP TO PLAY wait (`state.needTap`, `#cover.tap`) with the next track cooked behind it.
`node tools/check-pure.mjs` loads `covers.js` and `render.js`'s generators in node (a canvas stub stands in for the
DOM) and draws every look and generates a wav, a noise and a tone once.
Every line should show the expected `phase`/`cur`/`next`/`paused`/`holding`. Run both before and after any change to
playback, the crackle, the hold, the state machine or `playJob`/`advance`. Neither can test real audio output, the