feedBack/static/js
Byron Gamatos cb236e6c04
refactor(app): carve the A–B loop out of app.js (R3a) (#888)
static/js/loops.js (261) — bodies VERBATIM. app.js 8,421 → 8,224.
The second slice out of the strongly-connected core.

It OWNS the loop state — loopA, loopB, _loopMutationGen. Nothing outside writes
them: restartCurrentSong() looked like it did, but it declares its own local `let
loopA/loopB` shadows, so the module-level bindings only ever change in setLoop /
setLoopStart / setLoopEnd / clearLoop. All four move here. No state container.

DIRECTION IS THE WHOLE DESIGN. loops and section-practice are mutually dependent —
the SCC in miniature. clearLoop() must drop section-practice's selection, and
practiceSection() must call setLoop(). Both edges cannot be imports or no-cycle
(rightly) rejects it. So:

    section-practice  ->  reaches loops through the HOST SEAM (host.setLoop, …)
    loops             ->  imports section-practice DIRECTLY

section-practice is the higher-level feature — a consumer of loops, not the reverse
— so it is the one that takes the indirection. app.js hands the loop module's
exports across into the seam for it. Graph stays acyclic; no-cycle passes.

THE CONTRACT TEST EARNED ITS KEEP IMMEDIATELY. It failed on the first build with
"these hooks are wired by app.js but no module reads them: playSong". My dependency
scan had counted a mention of playSong() inside a COMMENT in loops.js as a real
call. Wired but unused is precisely the "fossil of a rename" case the test exists
for — and it caught it on a path no test executes.

VERIFIED BY DRIVING BOTH SIDES OF THE SEAM. A/B against origin/main in two browsers,
real song loaded:
  * setLoop(5,12) -> true; getLoop() -> 5,12 — IDENTICAL
  * clearLoop() (loops -> section-practice, a direct import) -> getLoop() ->
    null,null — IDENTICAL
  * onPhraseNext() (section-practice -> loops, ACROSS THE SEAM) -> ok — IDENTICAL
  * loadSavedLoop / saveCurrentLoop / deleteSelectedLoop on window — IDENTICAL
  * zero page errors either side. An unwired hook throws, so a live app is itself
    proof the seam is wired.

Harness: loop_api extracts the loop helpers by signature — retargeted to loops.js,
`export` stripped for the vm sandbox, and the sandbox's existing _audioSeek /
_audioTime / formatTime spies are now routed through a `host` object so every
assertion holds unchanged, just through the indirection the real code uses. It is
SPLIT: one test still reads app.js for the window.feedBack API surface, which stayed.

pytest 2396, node 1040/1040, ESLint 0 (no-cycle clean), tailwind clean.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:32:31 +02:00
..
audio-el.js refactor(app): give the <audio> element a module of its own (R3a) (#886) 2026-07-11 20:44:01 +02:00
diagnostics-export.js refactor(app): carve the diagnostics-bundle export out of app.js (R3a) (#881) 2026-07-11 18:52:47 +02:00
dom.js refactor(app): carve the DOM/modal primitives out of app.js (R3a) (#882) 2026-07-11 18:59:44 +02:00
highway-colors.js refactor(app): carve the highway string-colours out of app.js (R3a) (#883) 2026-07-11 19:22:14 +02:00
host.js refactor(app): the host seam + carve section practice out of app.js (R3a) (#887) 2026-07-11 20:58:41 +02:00
loops.js refactor(app): carve the A–B loop out of app.js (R3a) (#888) 2026-07-11 21:32:31 +02:00
plugin-loader.js refactor(app): carve settings backup + plugin updates out of app.js (R3a) (#885) 2026-07-11 19:42:54 +02:00
section-practice.js refactor(app): the host seam + carve section practice out of app.js (R3a) (#887) 2026-07-11 20:58:41 +02:00
settings-io.js refactor(app): carve settings backup + plugin updates out of app.js (R3a) (#885) 2026-07-11 19:42:54 +02:00
tuning-display.js refactor(app): carve the tuning-display helpers out of app.js (R3a) (#884) 2026-07-11 19:37:44 +02:00
viz.js refactor(app): carve the viz layer out of app.js — and delete the loader seam (R3a) (#880) 2026-07-11 18:41:52 +02:00