Commit Graph
2 Commits
Author SHA1 Message Date
36cf77dc44 refactor(highway): carve the 2D drawing layer into highway-draw.js (R3c) (#917)
18 functions, 1,245 lines. highway.js 3,972 -> 2,727 (-31%). The biggest R3c slice: notes,
sustains, chords, strum groups, unison bends and lyrics — everything the default renderer
paints each frame.

━━━ MUTABILITY, NOT LOCATION, DECIDES WHERE A THING BELONGS ━━━

Three per-instance caches came out with this slice, and they are why it needed care:

    _frameMismatchWarned   a warn-once Set of chord ids     (feedBack#88)
    _chordRenderInfo       a WeakMap of chord -> chain info
    _lyricMeasureCache     Map<fontSize, Map<text, width>>

All three are MUTATED. Left at module scope they would be SHARED ACROSS PANELS — one
highway's lyric widths and chord chains stomping another's, silently, with nothing throwing.
createHighway() is a factory (the constitution publishes window.createHighway so a plugin can
build a second highway), so they are lifted onto hwState, which is exactly what hwState is for.

The shimmer LUT went the OTHER way — to MODULE scope in highway-geometry.js. It is a
deterministic xorshift table, byte-for-byte identical for every instance, so sharing it is not
merely safe but BETTER: built once for the page rather than once per panel.

Same slice, opposite directions, decided entirely by whether the thing mutates.

━━━ MY SCRIPT WAS WRONG TWICE. THE GATES CAUGHT BOTH. ━━━

1. HAND-LISTED THE MOVE SET. I listed 10 functions and missed six that drawChords needs
   (_ensureChordRenderCache, bsearchChords, getChordTemplateInfo, _computeChordBox,
   _updateFretLinePreview, _drawFretLineChordPreview). The no-undef gate named every one. The
   set is now DERIVED from the dependency closure — 18, not 10.

2. JUDGED PURITY TOO EARLY, and this one is subtle. I classified _computeChordBox as pure
   because its ORIGINAL body never mentions hwState. Then the call-site rewriter injected
   `fretX(hwState, …)` INTO it — fretX takes hwState now (#916) — leaving a function that
   references an hwState it was never given. Purity has to be judged from the body AS IT WILL
   BE, so the classifier iterates to a fixed point: a function needs hwState if it mentions it,
   OR calls anything that now takes it. That moved _computeChordBox to the stateful side.

VERIFIED. A/B against origin/main: IDENTICAL, zero page errors. The PLUGIN BUNDLE contract is
byte-identical (b.fretX arity 3, b.getNoteState arity 2, both stable references, both correct
under the old calling convention). PERF GATE PASSES AT 1.92ms against its 12ms budget — and
this is the slice that could really have cost something: the ENTIRE per-frame drawing path is
now cross-module. It costs nothing measurable.

TESTS. highway_teaching_marks follows strumGroupBuckets to the new module. The two source-shape
harnesses now read highway.js AND every static/js/highway-*.js, rather than being re-pinned at
whichever file currently holds a function — re-pinning breaks again next time, and a shape
assertion that silently stops finding its target is indistinguishable from one that passes.

node 1045, pytest 2416, ESLint 0, no-undef 0, Codex 0.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:00:29 +02:00
1a386c272d refactor(highway): carve the PURE geometry primitives into highway-geometry.js (R3c) (#915)
6 functions, 53 lines. highway.js 4,158 -> 4,105. NOT ONE CALL SITE CHANGES.

project, roundRect, bnvNormalizedPoints, teachingFingerLabel, teachingDegreeLabel,
chordHarmonyLabels — the shared primitives every drawing function leans on.

━━━ PURITY IS THE WHOLE POINT OF THIS SLICE ━━━

Every one of these is a pure function of its arguments. None touches hwState. None closes over
the canvas context — roundRect() already took `ctx` explicitly, and the rest need nothing but
numbers. project() reads only the module-level constants from #914.

That matters because createHighway() is a FACTORY: a plugin can build a second highway for its
own panel, so anything holding per-instance state must be PASSED hwState rather than importing
it, or two panels silently share one clock and palette. These six hold no state at all, so
they move VERBATIM — the module boundary is invisible to every caller.

The asserts are mechanical and in the extractor: it REFUSES to move a function whose body
mentions hwState, or that references `ctx` without taking it as a parameter. Purity is
checked, not assumed.

━━━ WHAT IS DELIBERATELY LEFT BEHIND ━━━

The four primitives that DO need hwState — fretX, fillTextReadable, _noteState, _paintGemGlow
— stay in the factory for now. They need an explicit hwState parameter threaded through 53
call sites, which is a real behavioural change and belongs in its own commit rather than
smuggled in beside a provably-identical move. Separating the provable from the risky is the
whole discipline of this epic.

TESTS. Three harnesses brace-match these functions out of the source and run them in a
sandbox; they now read static/js/highway-geometry.js. `export function x` still contains
`function x`, so the extractor needed no change — only the path.

VERIFIED. A/B against origin/main: 15 probes IDENTICAL, zero page errors. PERF GATE PASSES AT
1.91ms against its 12ms budget — and this is the one that could plausibly have cost something:
project() runs for every visible note on every frame and is now a CROSS-MODULE call. It costs
nothing measurable. That is the answer #910 was built to give.

node 1045, pytest 2416, ESLint 0, Codex 0.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 12:31:56 +02:00