Skip to content

feat(editor): MusicXML keys imports keep their authored hand splits — the per-note hand field arrives - #299

Merged
byrongamatos merged 1 commit into
mainfrom
feat/keys-hand-import
Jul 16, 2026
Merged

feat(editor): MusicXML keys imports keep their authored hand splits — the per-note hand field arrives#299
byrongamatos merged 1 commit into
mainfrom
feat/keys-hand-import

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What

The keys LH/RH hand-assignment arc, slice A. MusicXML grand-staff scores carry authored per-stave hand assignments; until now the import deleted them (delete arr.notation, src/import.js) and every save re-derived hand splits with core's mean-pitch heuristic — the documented "produces wrong hand splits" path. Edited keys parts silently teach the wrong hands; this slice makes the authored data survive.

Authored notation rides the existing GP rail end to end

  • import.js keeps the payload and shifts its measure/beat times together with the notes (new @pure _shiftNotationTimes, the JS mirror of _warp_notation_sidecar's walk) — an unshifted payload would be born stale against the fingerprint.
  • add-arrangement stamps source:"musicxml" + source_notes_fp at the one moment payload and notes are known in sync (new _stamp_musicxml_notation, the counterpart of _attach_gp_notation) and returns it; the client carries it as _gp_notation — the field the save body already ships.
  • The authored-over-lift rail generalizes its source check to _AUTHORED_NOTATION_SOURCES ({"gp","musicxml"}) and now preserves the ORIGINAL provenance on every re-stamp (kept-verbatim and measure-merge paths) instead of rewriting it to "gp". Fingerprint invalidation ("edits win") and the measure-granular merge behave identically for both sources.

The per-note hand field ('lh'/'rh', absent = unassigned)

  • Registered in _NOTE_TECH_FIELDS — string-valued, never in the bool set, absent default None; the content-signature builders now share _note_tech_default so object- and dict-shaped notes can't drift.
  • Save wire emits the spelled-out hand key (rh is taken by right_hand), strictly validated to the enum — junk ("LH", "left", true, …) never rides.
  • Survives reconstructChords because it rides techniques (pinned by test).
  • MusicXML imports arrive with it pre-filled from staff provenance (staff 1→rh, 2→lh; an organ-pedal staff stays unassigned rather than guessed).

Companion + follow-ups

  • Companion: feedBack-plugin-musicxml-import feat(editor): handshape / arpeggio region authoring (E2) #8 (the parse-arrangement endpoint producing the arrangement + payload). Without it the MusicXML path stays "plugin not installed" exactly as before — the PRs are independently mergeable.
  • Tracked follow-ups (in CHANGELOG): core's sloppak loader learns hand for reload round-trips (small core PR, next in the arc); a hand EDIT doesn't yet invalidate a preserved authored sidecar (the fingerprint deliberately ignores techniques); split_hands respecting per-note overrides. Then the editor surface: roll shading by hand, movable split-point stamping command, hands-separate practice.

Tests

  • tests/test_musicxml_notation.py23 new: registry invariants, wire validation (incl. parametrized junk rejection), chord-member ride-along, stamping semantics, and the rail honoring / reopening / invalidating source:"musicxml" against the real core lift (same core-lib discovery + skip contract as test_notation_save.py). Fails on main by construction.
  • tests/musicxml_hand_import.test.mjs7 new: notation shift semantics (measure+beat, 1 ms grid, dur untouched, malformed tolerated) and hand surviving reconstructChords/flatten round-trips.
  • Gates: 325 pytest green, JS suite green (song_fit's failure reproduces on pristine main — environmental), lint 0 errors / 3 baseline warnings.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

Summary by CodeRabbit

  • New Features

    • Added per-note LH/RH hand annotations for keyboard arrangements (only lh/rh), including chord/member propagation.
    • MusicXML imports now preserve authored grand-staff hand splits and keep imported notation aligned.
  • Bug Fixes

    • Retains authored MusicXML notation when notes are unchanged; preserves it on no-edit saves and invalidates it after edits.
    • Improves robustness and timing alignment by shifting imported notation timestamps consistently during import.
  • Tests

    • Added unit and integration coverage for hand annotation preservation, save-wire behavior, notation stamping, and timing-shift handling.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 33255a32-8c50-4f47-a092-47b7f36652dc

📥 Commits

Reviewing files that changed from the base of the PR and between e92816d and 52ca211.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • routes.py
  • src/import.js
  • tests/musicxml_hand_import.test.mjs
  • tests/test_musicxml_notation.py

📝 Walkthrough

Walkthrough

Adds validated per-note hand metadata for keys arrangements and preserves MusicXML-authored notation through timestamp alignment, arrangement registration, sidecar persistence, and note-edit invalidation.

Changes

MusicXML authored notation and hand metadata

Layer / File(s) Summary
Per-note hand contract and round trips
routes.py, src/import.js, tests/musicxml_hand_import.test.mjs, tests/test_musicxml_notation.py
Registers techniques.hand, emits only lh/rh, includes it in signatures and wire output, and verifies chord reconstruction and serialization behavior.
Authored notation stamping and persistence
routes.py, tests/test_musicxml_notation.py, CHANGELOG.md
Stamps MusicXML notation with provenance and note fingerprints, returns it from arrangement registration, and preserves or invalidates authored sidecars according to note fingerprints.
MusicXML import alignment and registration
src/import.js, tests/musicxml_hand_import.test.mjs
Shifts imported notation timestamps with audio offsets and stores the server-stamped notation after arrangement registration.

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

Sequence Diagram(s)

sequenceDiagram
  participant MusicXMLImport
  participant AddArrangement
  participant SidecarPersistence
  MusicXMLImport->>MusicXMLImport: align notes and notation by audio offset
  MusicXMLImport->>AddArrangement: register Keys arrangement
  AddArrangement->>SidecarPersistence: persist stamped MusicXML notation
  SidecarPersistence->>SidecarPersistence: retain or invalidate by source_notes_fp
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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: preserving MusicXML keys hand splits and adding the per-note hand field.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 feat/keys-hand-import

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

@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: 1

🤖 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 `@src/import.js`:
- Around line 86-89: Update the nested iteration in the import flow to verify
that staff.voices and voice.beats are arrays before iterating, defaulting
malformed values such as objects to empty arrays. Preserve the existing voice,
beat, and timestamp validation behavior while preventing malformed plugin
payloads from throwing.
🪄 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: 808d011d-059e-488a-8e45-2242bc6398a2

📥 Commits

Reviewing files that changed from the base of the PR and between 35b7632 and 4f626be.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • routes.py
  • src/import.js
  • tests/musicxml_hand_import.test.mjs
  • tests/test_musicxml_notation.py

Comment thread src/import.js Outdated
@byrongamatos

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@byrongamatos
byrongamatos force-pushed the feat/keys-hand-import branch from e92816d to 52ca211 Compare July 16, 2026 18:44
@byrongamatos
byrongamatos merged commit 6fcd698 into main Jul 16, 2026
3 of 4 checks passed
@byrongamatos
byrongamatos deleted the feat/keys-hand-import branch July 16, 2026 18:45
ChrisBeWithYou pushed a commit that referenced this pull request Jul 16, 2026
…efresh the notation

The hand arc, Step B (stacked on feat/keys-hand-import / #299). The
per-note `hand` field becomes authorable and visible:

- Note ▸ Hand (keys): Left / Right / Clear on the selection — one undo
  step via SetTechScalarCmd. Runtime-guarded to keys arrangements.
- Track ▸ Assign hands by split…: the split point is a STAMPING
  generator, never a live layer (the piano-pedagogy call): pick a split
  note (default C4, note names or MIDI accepted — new @pure
  _parseSplitPitchPure), and every targeted note takes lh below / rh
  at-or-above in ONE undoable command. New SetTechScalarPerNoteCmd —
  SetTechScalarCmd's per-note-values sibling (no bend special-casing;
  plain scalar marks only). Selection scopes the stamp; per-note edits
  win from then on.
- Hand shading on the piano roll (View ▸, default ON): LH warm
  (#e8965a) / RH cool (#5a9de8) body color; unassigned keeps the octave
  palette so "no call made" never reads as an assignment. Same
  color-job argument as fretted-in-roll: Y already says the pitch, so
  color says WHO PLAYS IT.
- `hand` joins `_notes_fingerprint`'s identity tuple — the ONE
  technique that does — closing the tracked trap where a hand EDIT
  couldn't invalidate a preserved authored sidecar (it would freeze the
  old hands forever). Editor/wire shapes stay fingerprint-identical;
  junk enums read unassigned on both. One-time effect: previously
  stamped payloads re-fingerprint on next save and take the
  measure-granular merge (unedited bars keep authored hands). The
  relift honors per-note hands via core split_hands (core PR #992).

New src/hand.js module; menus in menu-bar.js; USER-GUIDE "Hands on
keys tracks" section.

Tests: tests/hand_authoring.test.mjs (4 — parser incl. accidentals and
junk, stamp orientation matching core, per-note command exec→undo→redo
round-trip incl. a bare-techniques note) + a fingerprint-invalidation
pytest (hand flips it; other techniques and junk don't; shapes agree).
Gates: 326 pytest, JS suite green (song_fit env failure = pristine
main), lint 0 errors / 3 baseline warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
ChrisBeWithYou pushed a commit that referenced this pull request Jul 16, 2026
…efresh the notation

The hand arc, Step B (stacked on feat/keys-hand-import / #299). The
per-note `hand` field becomes authorable and visible:

- Note ▸ Hand (keys): Left / Right / Clear on the selection — one undo
  step via SetTechScalarCmd. Runtime-guarded to keys arrangements.
- Track ▸ Assign hands by split…: the split point is a STAMPING
  generator, never a live layer (the piano-pedagogy call): pick a split
  note (default C4, note names or MIDI accepted — new @pure
  _parseSplitPitchPure), and every targeted note takes lh below / rh
  at-or-above in ONE undoable command. New SetTechScalarPerNoteCmd —
  SetTechScalarCmd's per-note-values sibling (no bend special-casing;
  plain scalar marks only). Selection scopes the stamp; per-note edits
  win from then on.
- Hand shading on the piano roll (View ▸, default ON): LH warm
  (#e8965a) / RH cool (#5a9de8) body color; unassigned keeps the octave
  palette so "no call made" never reads as an assignment. Same
  color-job argument as fretted-in-roll: Y already says the pitch, so
  color says WHO PLAYS IT.
- `hand` joins `_notes_fingerprint`'s identity tuple — the ONE
  technique that does — closing the tracked trap where a hand EDIT
  couldn't invalidate a preserved authored sidecar (it would freeze the
  old hands forever). Editor/wire shapes stay fingerprint-identical;
  junk enums read unassigned on both. One-time effect: previously
  stamped payloads re-fingerprint on next save and take the
  measure-granular merge (unedited bars keep authored hands). The
  relift honors per-note hands via core split_hands (core PR #992).

New src/hand.js module; menus in menu-bar.js; USER-GUIDE "Hands on
keys tracks" section.

Tests: tests/hand_authoring.test.mjs (4 — parser incl. accidentals and
junk, stamp orientation matching core, per-note command exec→undo→redo
round-trip incl. a bare-techniques note) + a fingerprint-invalidation
pytest (hand flips it; other techniques and junk don't; shapes agree).
Gates: 326 pytest, JS suite green (song_fit env failure = pristine
main), lint 0 errors / 3 baseline warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
byrongamatos added a commit that referenced this pull request Jul 16, 2026
…ing (CodeRabbit)

- CHANGELOG: the #299 'known follow-ups' claimed a hand edit doesn't invalidate
  the notation sidecar; this PR implements exactly that (hand joins the
  fingerprint), so drop the now-false clause and note it's done — keeping the
  still-open split_hands (core #992) and sloppak-loader follow-ups.
- USER-GUIDE: 'Hands on keys tracks' -> 'Hands on keyboard tracks' (clearer).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
byrongamatos added a commit that referenced this pull request Jul 16, 2026
…resh the notation (#303)

* feat(editor): keys hand authoring — set/stamp/shade, and hand edits refresh the notation

The hand arc, Step B (stacked on feat/keys-hand-import / #299). The
per-note `hand` field becomes authorable and visible:

- Note ▸ Hand (keys): Left / Right / Clear on the selection — one undo
  step via SetTechScalarCmd. Runtime-guarded to keys arrangements.
- Track ▸ Assign hands by split…: the split point is a STAMPING
  generator, never a live layer (the piano-pedagogy call): pick a split
  note (default C4, note names or MIDI accepted — new @pure
  _parseSplitPitchPure), and every targeted note takes lh below / rh
  at-or-above in ONE undoable command. New SetTechScalarPerNoteCmd —
  SetTechScalarCmd's per-note-values sibling (no bend special-casing;
  plain scalar marks only). Selection scopes the stamp; per-note edits
  win from then on.
- Hand shading on the piano roll (View ▸, default ON): LH warm
  (#e8965a) / RH cool (#5a9de8) body color; unassigned keeps the octave
  palette so "no call made" never reads as an assignment. Same
  color-job argument as fretted-in-roll: Y already says the pitch, so
  color says WHO PLAYS IT.
- `hand` joins `_notes_fingerprint`'s identity tuple — the ONE
  technique that does — closing the tracked trap where a hand EDIT
  couldn't invalidate a preserved authored sidecar (it would freeze the
  old hands forever). Editor/wire shapes stay fingerprint-identical;
  junk enums read unassigned on both. One-time effect: previously
  stamped payloads re-fingerprint on next save and take the
  measure-granular merge (unedited bars keep authored hands). The
  relift honors per-note hands via core split_hands (core PR #992).

New src/hand.js module; menus in menu-bar.js; USER-GUIDE "Hands on
keys tracks" section.

Tests: tests/hand_authoring.test.mjs (4 — parser incl. accidentals and
junk, stamp orientation matching core, per-note command exec→undo→redo
round-trip incl. a bare-techniques note) + a fingerprint-invalidation
pytest (hand flips it; other techniques and junk don't; shapes agree).
Gates: 326 pytest, JS suite green (song_fit env failure = pristine
main), lint 0 errors / 3 baseline warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

* fix(editor): keep notation fingerprint sort crash-safe on stacked unison hands

The `hand` field added to `_notes_fingerprint`'s identity tuple used a None
sentinel for unassigned. Two notes identical in (t,s,f,sus) but differing in
hand — a doubled unison (both hands on one pitch, real in piano writing) with
one note assigned and the other not — reach a None-vs-str compare in the sort
and raise TypeError, crashing the save. Use "" for unassigned instead: still
distinct from "lh"/"rh" (the hand still counts toward invalidation) and totally
ordered against them. Regression test crashes pre-fix, passes after.

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

* docs(editor): reconcile hand-authoring changelog + clarify guide heading (CodeRabbit)

- CHANGELOG: the #299 'known follow-ups' claimed a hand edit doesn't invalidate
  the notation sidecar; this PR implements exactly that (hand joins the
  fingerprint), so drop the now-false clause and note it's done — keeping the
  still-open split_hands (core #992) and sloppak-loader follow-ups.
- USER-GUIDE: 'Hands on keys tracks' -> 'Hands on keyboard tracks' (clearer).

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

---------

Co-authored-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
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