refactor(editor): extract the string/lane model to src/lanes.js (R2, step 4) - #148
Conversation
…step 4)
The lane model — string count, labels, string<->lane mapping, lane colour —
moves out of src/main.js (21,036 -> 20,800). lanes.js reads S, touches no DOM,
and mirrors lib/song.py:arrangement_string_count so editor and highway agree.
Moved: MAX_LANES, STRING_LABEL_COLORS (now module-private), isBassArr,
_seedExtendedStringsFromTuning, _stringCountFor, lanes, laneLabels, strToLane,
laneToStr, colorForLane.
FIRST reassigned-scalar lift in the editor. The per-frame lane cache was three
module-scope `let`s (_lanesCacheActive / _lanesCacheValue / _laneLabelsCacheValue)
written from two places in main.js: draw() seeds it, and onMouseMove seeds it and
restores it in a `finally` around every hit test. ES import bindings are read-only,
so the three scalars move onto one exported container `LC` (.active/.value/.labels)
— the shape the stems pilot used for SH/ST. main.js's only additions are the import
block and that mechanical rename; everything else is deletion.
Tests: bass_string_count and strings_modal drop their brace-counting source
extractors for real imports (bass_string_count had been re-declaring its own
`const MAX_LANES = 8`; strings_modal now injects the real _stringCountFor into
its sandbox instead of concatenating its source text). New tests/lanes.test.mjs
drives lanes.js against the real S: extended-range label rows (7/8-string guitar,
5/6-string bass), the string<->lane involution, label-keyed colour (a bass low E
is red like a guitar's), and the LC cache contract.
Verified: node --test 83/83, pytest 248/248. main.js diff mechanically checked to
be deletions + the import block + the LC rename, nothing else. No shadowing of any
imported symbol; graph stays acyclic (lanes -> state only). Served from local
uvicorn on core@main (R0): src/lanes.js 200 as text/javascript.
Headless Chromium, three harnesses, all green with output identical to the
pre-change baseline:
- draw path: loads a real chart, 6 lane colours through colorForLane, 39
(degree-label, colour) pairs all matching theory.js, out-of-key branch hit.
- state round-trip: S.snapIdx write/read-back through the DOM.
- hit test (new, for LC): drives real mousemoves across the canvas, clicks a
note -> "1 note selected, string: 0"; clicks a note on another lane ->
"string: 1". A lost `finally` restore would leave the cache hot with a stale
count and resolve the second click to the wrong string.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughExtracts the string/lane model (lane counting, labeling, coloring, and index transforms) from src/main.js into a new src/lanes.js module with an exported LC cache container. Updates src/main.js to consume these utilities, migrates related tests to ESM importing real functions, adds a new lanes.test.mjs suite, and updates CHANGELOG.md. ChangesLane Model Extraction
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant MainJS as main.js draw()
participant LanesModule as lanes.js
participant LC as LC cache
MainJS->>LC: LC.active = false
MainJS->>LanesModule: lanes()
LanesModule->>LanesModule: _stringCountFor(arrangement)
LanesModule-->>MainJS: laneCount
MainJS->>LC: LC.value = laneCount, LC.active = true
MainJS->>LanesModule: laneLabels()
LanesModule-->>LC: LC.labels
MainJS->>LanesModule: colorForLane(l)
LanesModule->>LC: read LC.labels
LanesModule-->>MainJS: color
MainJS->>LC: LC.active = false, LC.labels = null
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ast-grep (0.44.1)src/main.jsast-grep timed out on this file Comment |
There was a problem hiding this comment.
Pull request overview
This PR continues the editor’s ES-module split by extracting the string/lane model from src/main.js into a new functional module src/lanes.js, and updates call sites and tests to use the extracted exports while preserving the per-frame lane cache behavior via an exported LC container.
Changes:
- Added
src/lanes.jsto host string-count, lane-label, string↔lane mapping, and lane-color logic (plus the exportedLCcache container). - Refactored
src/main.jsto import the extracted lane/string APIs and to seed/clear theLCcache indrawNow()andonMouseMove(). - Updated existing tests to import real lane functions instead of brace-counting source extraction; added a new integration-style unit test for the module.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/strings_modal.test.mjs | Switches to ESM imports and injects the real _stringCountFor from src/lanes.js into the sandbox harness. |
| tests/lanes.test.mjs | New test covering extended-range labels, string↔lane inversion, label-keyed colors, and LC cache behavior. |
| tests/bass_string_count.test.mjs | Replaces source extraction with direct imports of _seedExtendedStringsFromTuning and _stringCountFor from src/lanes.js. |
| src/main.js | Removes inlined lane/string model logic and replaces it with imports from src/lanes.js, including LC cache seeding/restoring. |
| src/lanes.js | New module implementing the extracted string/lane model and the exported LC cache container. |
| CHANGELOG.md | Documents the step-4 ES-module migration and associated test refactors/additions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/lanes.js (1)
85-105: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicate branch body in
_seedExtendedStringsFromTuning.Both branches compute the identical
arr._extendedStrings = tuningLen - baseline;. Could collapse into a single condition for clarity, though behavior is unchanged from the verbatim move.♻️ Optional consolidation
- if (tuningLen > 6) { - arr._extendedStrings = tuningLen - baseline; - } else if (authoritativeLength && tuningLen > baseline - && !(isBass && tuningLen === 6)) { - arr._extendedStrings = tuningLen - baseline; - } + const shouldSeed = tuningLen > 6 || + (authoritativeLength && tuningLen > baseline && !(isBass && tuningLen === 6)); + if (shouldSeed) arr._extendedStrings = tuningLen - baseline;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lanes.js` around lines 85 - 105, The `_seedExtendedStringsFromTuning` function has duplicated branch logic that assigns the same `_extendedStrings` value in both the `tuningLen > 6` path and the `authoritativeLength` path. Refactor the conditional around `arrangements`/`arr._extendedStrings` so the shared assignment happens once after the appropriate guard is selected, while preserving the bass-specific exception and existing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/lanes.js`:
- Around line 85-105: The `_seedExtendedStringsFromTuning` function has
duplicated branch logic that assigns the same `_extendedStrings` value in both
the `tuningLen > 6` path and the `authoritativeLength` path. Refactor the
conditional around `arrangements`/`arr._extendedStrings` so the shared
assignment happens once after the appropriate guard is selected, while
preserving the bass-specific exception and existing behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 67f1196d-d119-4a08-9a3a-a09cc1a9338a
📒 Files selected for processing (6)
CHANGELOG.mdsrc/lanes.jssrc/main.jstests/bass_string_count.test.mjstests/lanes.test.mjstests/strings_modal.test.mjs
Step 4 of the editor's ES-module split (R2). The first functional module, and the first reassigned-scalar lift.
What moved
src/lanes.js(173 lines) — the string/lane model.main.js21,036 → 20,800._stringCountFor,lanes,_seedExtendedStringsFromTuning,isBassArr(module-private)laneLabelsstrToLane,laneToStrcolorForLane+STRING_LABEL_COLORS(module-private)It reads
S, touches no DOM, and mirrorslib/song.py:arrangement_string_countso the editor and the highway agree on string counts. Graph stays acyclic:lanes.js → state.js.The
LCcontainer — why this step is differentSteps 2 and 3 were pure moves. This one hits the hazard I flagged: ES import bindings are read-only, and the per-frame lane cache was three module-scope
lets —— written from two places that stay in
main.js.draw()seeds the cache for the frame;onMouseMove()seeds it and restores the previous value in afinallyaround every hit test (hitNote→strToY→strToLane→lanes(), which is O(N) uncached).So they move onto one exported container
LC(.active/.value/.labels) — the shape the stems pilot used for itsSH/STlifts. Mechanical rename;main.js's only additions are the import block and that rename, everything else is deletion (verified by reconstructing the diff programmatically).Tests: two more source-extractors die
bass_string_countbrace-counted_seedExtendedStringsFromTuningand_stringCountForout ofmain.jsand re-declared its ownconst MAX_LANES = 8. It now imports the real functions, so the clamp can't silently diverge from the source.strings_modalconcatenated_stringCountFor's source text into anew Functionsandbox. It now injects the real imported function as a parameter (thechord_at_cursorpattern from step 2).tests/lanes.test.mjsdriveslanes.jsagainst the realS— the first test composing two modules. It covers what_stringCountFor's own suite doesn't: extended-range label rows (7/8-string guitar, 5/6-string bass with their↓/↑arrows), the string⇄lane involution, label-keyed colour (a bass low E is red exactly like a guitar's — the whole point of keying by label), the unknown-label grey fallback, and theLCcache contract itself.Verification
node --test83/83,pytest248/248.main.jsfor real (non-comment) use — which is how Codex's one finding, an unusedisBassArrimport, got fixed: a naivegrep -ccounts comment mentions, and all five remainingisBassArrmentions were prose. It's now module-private.src/lanes.js200 astext/javascript.colorForLane; 39(degree-label, colour)pairs all matchingtheory.js's own tables; out-of-key branch hit for 6/12 tonics.S.snapIdxwrite → read-back through the DOM.LC) — drives real mousemoves across the canvas, clicks a note →"1 note selected, string: 0"; clicks a note on a different lane →"string: 1". A lostfinallyrestore would leave the cache hot with a stale count and resolve the second click to the wrong string.Note
This is the first step in this run with a genuinely behavioural surface (a hot-path cache used by drawing and hit-testing). The headless harnesses cover the guitar path end to end and the bass/extended-range logic is unit-tested, but an on-device pass is worth doing here in a way it wasn't for steps 2–3.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests