Skip to content

refactor(editor): move the note command classes to src/commands.js (R2, step 18) - #169

Merged
byrongamatos merged 2 commits into
mainfrom
refactor/r2-step18-commands
Jul 9, 2026
Merged

refactor(editor): move the note command classes to src/commands.js (R2, step 18)#169
byrongamatos merged 2 commits into
mainfrom
refactor/r2-step18-commands

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Twenty-first module. src/main.js 16,088 → 15,042. All 18 note command classes plus the helpers that construct and execute them.

The chunk the last six steps kept working around

My own memory note said “47 command classes remain, interleaved with their feature code.” That was only half true — 18 of them sit in one contiguous run under the Undo/Redo banner. And step 17’s shared host object is what made the lift cheap: nine main.js callbacks would otherwise have needed a ninth setXHooks().

commands.js is DOM-free, on purpose

Two functions would have broken that, so they moved back to main.js:

function why it can’t live in a command module
_rollConfirmPosition builds the ambiguous-pitch popover
_resizeForLaneChange schedules a requestAnimationFrame

Both arrive as host hooks. Keeping them out is exactly what lets the suites import the real classes under node with no document — and it retired the last typeof resizeCanvas === function guard, which only ever existed to keep a sliced block eval-able in a sandbox.

Tests

Eight suites stopped brace-matching classes out of main.js. suggest_position_wiring lost its entire 40-line sandbox and 46 lines overall. import_guitar_track was CJS and is now .mjs.

Converting strings_modal exposed a fixture that only made sense under a stub. Its “low-side add” case used a 4-string Banjo against an injected arr => arr.tuning.length. The real _stringCountFor has no banjo: it reads a 4-length tuning on a non-bass part as a padded guitar and reports 6, so the add produced 7 strings, not 5. It is a Bass now. The behaviour under test is unchanged — it is finally being tested against the model the editor actually ships.

A bug in my own tooling

While picking this step, the section-coupling scanner I use to rank candidates told me this region needed 4 main.js symbols. It needed 7.

It stripped strings with regexes, so /\//g read as a line comment and deleted the rest of its line, and /["]/ opened a phantom string that swallowed code until the next quote. Both occur in main.js. It is now a single-pass tokenizer that tracks regex literals, with a self-check asserting no top-level declaration is lost between raw and stripped source (raw=539 stripped=539 lost=0). no-undef caught the three it missed here, which is why the boundary is right anyway.

Why a headless harness, again

verify_commands.py selects a note, presses Up, and asserts it moves one lane, the canvas repaints, the inspector follows the model, and undo restores both.

check hooks wired editorCurrentNoteIndices unwired
clicking selects a note PASS PASS
moveStringUp actually moves it PASS FAIL
the canvas repaints on the new lane PASS FAIL
the inspector follows PASS FAIL
undo moved it back PASS PASS

Comment out that one hook and all 88 unit tests still pass. The inert default returns [], so _execMoveString finds nothing to move and reports nothing — no error, no status line, an entirely silent dead key.

Verification

node --test 88/88 · pytest 248/248 · npm run lint 0 errors (9 warnings) · all 13 headless harnesses pass.

Codex diffed commands.js against HEAD:src/main.js and found zero deltas beyond the expected export keywords and host.* rewrites.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Expanded undoable editing across notes, sustain, frets, techniques, and chord-related harmony changes.
    • Strengthened pitch-based placement workflows, including suggested positions, confirmations, and pitch/string resolution during drag/click.
    • Improved lane-change resizing behavior to better coordinate canvas updates.
  • Bug Fixes

    • Prevented crashes in environments without a DOM by making status updates a no-op and ensuring command logic can run DOM-free.
    • Refined roll/read-only and selection/undo edge cases, including time and sustain undo/redo routing.

…2, step 18)

src/main.js 16,088 -> 15,042. Twenty-first module; the graph stays acyclic.
All 18 note command classes plus the helpers that construct and execute them.

This is the chunk the previous six steps kept working around: the memory note
said "47 command classes remain, interleaved with their feature code". They
are not, in fact, all interleaved — 18 of them sit in one contiguous run under
the Undo/Redo banner, and step 17's shared `host` object is what made the lift
cheap. Nine main.js callbacks would otherwise have needed a ninth setXHooks().

commands.js is DOM-FREE, and that is the boundary, not a coincidence. Two
functions would have broken it, so they moved back to main.js:

  _rollConfirmPosition   builds the ambiguous-pitch popover
  _resizeForLaneChange   schedules a requestAnimationFrame

Both arrive as host hooks. Keeping them out is exactly what lets the suites
import the real classes under node with no document — and it removed the last
`typeof resizeCanvas === 'function'` guard, which only ever existed to keep a
sliced block eval-able in a sandbox.

Tests: eight suites stopped brace-matching classes out of main.js.
suggest_position_wiring lost its entire 40-line sandbox and 46 lines overall.
import_guitar_track was CJS and is now .mjs.

Converting strings_modal exposed a fixture that only made sense under a stub.
Its "low-side add" case used a 4-string 'Banjo' against an injected
`arr => arr.tuning.length`. The REAL _stringCountFor has no banjo: it reads a
4-length tuning on a non-bass part as a padded guitar and reports 6, so the add
produced 7 strings, not 5. The fixture is a 'Bass' now — the behaviour under
test (low add shifts lanes, undo round-trips) is unchanged, but it is finally
being tested against the model the editor actually ships.

While picking this step I found and fixed a bug in my own section-coupling
scanner: it stripped strings with regexes, so `/\//g` read as a line comment
and deleted the rest of the line, and `/['"]/` opened a phantom string that ate
code until the next quote. It under-reported this region's dependencies by
three. Rewritten as a single-pass tokenizer that tracks regex literals, with a
self-check that no top-level declaration is lost (saved alongside the
harnesses).

Verified beyond the unit tests, which cannot see host wiring: verify_commands.py
selects a note, presses Up, and asserts it moves one lane, the canvas repaints,
the inspector follows, and undo restores both. Comment out the single
`editorCurrentNoteIndices` hook and all 88 unit tests still pass while four of
its seven checks fail — the inert default returns [], so _execMoveString finds
nothing to move and reports nothing.

node --test 88/88, pytest 248/248, npm run lint 0 errors, Codex clean (it
diffed commands.js against HEAD:src/main.js and found zero deltas beyond the
expected `export` and `host.*` rewrites), all 13 headless harnesses pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 9, 2026 21:09
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d1bb4e2b-163c-4282-b50f-61d089959f4e

📥 Commits

Reviewing files that changed from the base of the PR and between 9870b33 and 6520d3f.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • src/commands.js
  • src/ui.js
  • tests/commands_dom_free.test.mjs
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/commands.js

📝 Walkthrough

Walkthrough

The PR moves command execution into src/commands.js, adds host callback defaults and DOM-safe status handling, rewires src/main.js hook/import usage, and updates Node tests and changelog text to use direct module imports and shared state.

Changes

Command layer refactor

Layer / File(s) Summary
Core note editing commands
src/commands.js
Adds undoable note, sustain, fret, technique, chord-function, and selection-preserving command operations.
Fretted-roll position resolution
src/commands.js
Adds pitch/string movement, suggested-position handling, roll additions, acceptance, and position cycling commands.
String and chart arrangement commands
src/commands.js
Adds tuning normalization, undoable string changes, lane resizing integration, and chart replacement rollback.
Main-module and host wiring
src/host.js, src/main.js, src/ui.js
Adds inert host callbacks, imports command APIs, guards status updates without a DOM, and wires editor callbacks and deferred lane resizing.
Direct-import test harnesses and changelog
tests/*.mjs, CHANGELOG.md
Replaces command extraction from main.js with direct imports, shared state, and host-hook setup across the affected tests; updates the changelog entry accordingly.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: moving the note command classes into src/commands.js.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/r2-step18-commands

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the editor to move the contiguous block of 18 “note command” undo/redo classes (and their helper executors) out of src/main.js into a new src/commands.js, and updates the host-hook wiring and test harnesses to import the real command implementations under node.

Changes:

  • Introduces src/commands.js containing the note command classes plus helpers such as move-to-string, suggest-position write path, string add/remove, and chart-replace swap/restore.
  • Updates src/main.js to import these commands/helpers and wires additional host hooks (editBlipAt, editorCurrentNoteIndices, renderInspector, resizeForLaneChange, rollConfirmPosition).
  • Refactors multiple unit tests to stop slicing classes/functions out of main.js and instead import real commands + use host hooks.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/suggest_position_wiring.test.mjs Switches from slicing main.js blocks to importing commands + wiring required host hooks.
tests/suggest_position_move.test.mjs Imports real pitch-move command/helpers and seeds real state rather than eval-slicing.
tests/strings_modal.test.mjs Imports string commands/normalize helper; stubs required host hook for resize side effects.
tests/roll_position_cycle.test.mjs Imports real cycle-position executor and move-to-string command; uses host hook for selection.
tests/roll_edge_resize.test.mjs Imports sustain resize commands directly instead of brace-slicing.
tests/inspector_time.test.mjs Uses real commands; ensures the “real S” notes array matches the test-controlled array.
tests/import_guitar_track.test.mjs Converts to ESM; imports swap/restore helpers and continues slicing the remaining import helpers.
tests/cross_arr_undo.test.mjs Imports MoveNoteCmd directly and removes sliced class extraction.
src/main.js Removes inlined note commands/helpers; imports from commands.js and expands host-hook wiring.
src/host.js Adds additional host hook slots needed by commands.js while keeping inert defaults for node.
src/commands.js New module containing the 18 note command classes + helper/executor functions.
CHANGELOG.md Documents the refactor, the DOM boundary intent, and the associated test updates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/commands.js
Comment thread src/commands.js Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/main.js (1)

1211-1234: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the stale AddStringCmd doc fragment here
The position/string-count paragraph doesn’t describe _resizeForLaneChange; keep only the layout-side-effect note. If that text is still needed, move it onto AddStringCmd in src/commands.js.

🤖 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/main.js` around lines 1211 - 1234, The leading `position`/string-count
documentation inside `_resizeForLaneChange` is stale and does not belong on this
helper. Remove that AddStringCmd-specific paragraph from `src/main.js` and keep
only the layout-side-effect explanation for `_resizeForLaneChange`; if that
description is still needed, move it to `AddStringCmd` in `src/commands.js`
instead.
tests/import_guitar_track.test.mjs (1)

26-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Naive brace-counter extraction duplicated across 3 test files.

extractFn here, extractNamed in tests/inspector_time.test.mjs, and extractRe in tests/cross_arr_undo.test.mjs are three independent, near-identical brace-matching helpers, none of which are string/regex/comment-aware. That's the same failure mode the changelog says was just fixed in the section-coupling scanner (naive brace/regex stripping mis-parsing regex literals). It's harmless today only because the extracted functions happen to contain no {/} inside strings or regexes — a fragile invariant to rely on silently across three copies of the same logic.

Consider factoring one shared, slightly more robust extraction helper into a test-utils module and having all these suites import it.

🤖 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 `@tests/import_guitar_track.test.mjs` around lines 26 - 41, The brace-based
source extraction logic is duplicated and fragile across this test and the other
two suites. Factor the repeated helper into a shared test utility and update the
callers in extractFn, extractNamed, and extractRe to use it so the
brace-matching behavior is centralized and easier to harden against strings,
regexes, and comments.
🤖 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.

Inline comments:
In `@CHANGELOG.md`:
- Around line 12-14: The changelog entry in the note-command classes summary is
truncated: the `src/main.js` size statement should include the missing unit.
Update the sentence around the `src/main.js` reference so it reads naturally and
fully (for example, by adding the intended unit after the number), preserving
the rest of the wording in `CHANGELOG.md`.

In `@src/commands.js`:
- Line 29: Remove the unused midiToNote import from the import list in
commands.js. Keep the other symbols from keys.js that are actually used, and
update the import statement so it only includes PIANO_LANE_H, _rollPitchCtx, and
isKeysArr.

---

Nitpick comments:
In `@src/main.js`:
- Around line 1211-1234: The leading `position`/string-count documentation
inside `_resizeForLaneChange` is stale and does not belong on this helper.
Remove that AddStringCmd-specific paragraph from `src/main.js` and keep only the
layout-side-effect explanation for `_resizeForLaneChange`; if that description
is still needed, move it to `AddStringCmd` in `src/commands.js` instead.

In `@tests/import_guitar_track.test.mjs`:
- Around line 26-41: The brace-based source extraction logic is duplicated and
fragile across this test and the other two suites. Factor the repeated helper
into a shared test utility and update the callers in extractFn, extractNamed,
and extractRe to use it so the brace-matching behavior is centralized and easier
to harden against strings, regexes, and comments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0599ef07-9963-4ac8-af3b-24715eedfc71

📥 Commits

Reviewing files that changed from the base of the PR and between 5921a1d and 9870b33.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • src/commands.js
  • src/host.js
  • src/main.js
  • tests/cross_arr_undo.test.mjs
  • tests/import_guitar_track.test.mjs
  • tests/inspector_time.test.mjs
  • tests/roll_edge_resize.test.mjs
  • tests/roll_position_cycle.test.mjs
  • tests/strings_modal.test.mjs
  • tests/suggest_position_move.test.mjs
  • tests/suggest_position_wiring.test.mjs

Comment thread CHANGELOG.md
Comment thread src/commands.js Outdated
Copilot, on #169. Two findings, both correct:

- commands.js claimed to be DOM-free but reached `document` transitively
  through setStatus (src/ui.js), which throws under node. One guard in the
  shared function rather than at its ~180 call sites.
- `midiToNote` was an unused import.

Added tests/commands_dom_free.test.mjs to hold the boundary. It is the one
suite that must NOT import tests/_history_env.mjs, since that installs a
document stub and would hide exactly this regression.

The first draft of that suite was VACUOUS: it asserted `_execMoveString(1)`
does not throw, but with an empty selection that returns before ever calling
setStatus. Deleting the guard left it green. It now drives
`_execCyclePosition(1)`, whose first branch is setStatus('Select notes first')
— which fails on the unguarded code, as a guard's test must.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byrongamatos

Copy link
Copy Markdown
Collaborator Author

Both correct, both fixed — the first one especially, because it meant the module header was lying.

commands.js had no document reference of its own, but setStatus (src/ui.js) does, so the module could be imported under node and not run. The guard goes in setStatus rather than at its ~180 call sites: no document means no status line to write to.

To hold the boundary I added tests/commands_dom_free.test.mjs. It is the one suite that must not import tests/_history_env.mjs, since that installs a document stub and would hide precisely this regression.

Worth recording that my first draft of that suite was vacuous. It asserted _execMoveString(1) does not throw — but with an empty selection _execMoveString returns before ever calling setStatus, so deleting the guard left it green. It now drives _execCyclePosition(1), whose first branch is setStatus(Select notes first), and it fails on the unguarded code. A guard's test that passes without the guard is worse than no test.

midiToNote removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants