Skip to content

feat(editor): keys hand authoring — set, stamp, shade; hand edits refresh the notation - #302

Closed
ChrisBeWithYou wants to merge 2 commits into
feat/keys-hand-importfrom
feat/keys-hand-surface
Closed

feat(editor): keys hand authoring — set, stamp, shade; hand edits refresh the notation#302
ChrisBeWithYou wants to merge 2 commits into
feat/keys-hand-importfrom
feat/keys-hand-surface

Conversation

@ChrisBeWithYou

Copy link
Copy Markdown
Contributor

What

The hand arc, Step B — stacked on #299 (base = feat/keys-hand-import; merge that first, then this). The per-note hand field #299 introduced becomes authorable and visible:

  • Note ▸ Hand (keys) — Left / Right / Clear on the selection, one undo step.
  • Track ▸ Assign hands by split… — the split point is a stamping generator, never a live layer (the piano-pedagogy panel's call): pick a split note (default C4; note names with accidentals or raw MIDI — 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). Selection scopes the stamp; per-note edits win from then on.
  • Hand shading on the piano roll (View ▸, default on) — LH warm / RH cool body color; unassigned notes keep the octave palette so "no call made" never reads as an assignment.
  • Hand edits refresh the saved notation: hand joins _notes_fingerprint's identity tuple — the one technique that does — closing the trap flagged in feat(editor): MusicXML keys imports keep their authored hand splits — the per-note hand field arrives #299's changelog 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 their next save and take the measure-granular merge (unedited bars keep their authored hands). The relift then honors per-note hands via core #992.

The pipeline this completes

MusicXML authored hands (musicxml-import #8#299) · reload round-trip (core #990) · hand-aware relift (core #992) · authoring + visibility + honest invalidation (this PR). Next slice: hands-separate practice (mute one hand, with grading masked).

Tests

tests/hand_authoring.test.mjs (parser incl. accidentals + junk; stamp orientation pinned to core split_hands's >= middle C → rh; per-note command exec→undo→redo round-trip incl. a bare-techniques note) + a fingerprint pytest (hand flips it, other techniques and junk don't, editor/wire shapes agree). Gates: 326 pytest, JS suite green (song_fit env failure reproduces on pristine main), lint 0 errors / 3 baseline warnings. USER-GUIDE §5 gains "Hands on keys tracks".

🤖 Generated with Claude Code

https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

ChrisBeWithYou and others added 2 commits July 16, 2026 08:49
… the per-note hand field arrives

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`) and every save re-derived hand splits with
core's mean-pitch heuristic — the documented "produces wrong hand
splits" path.

Authored notation now rides the existing GP rail end to end:

- src/import.js keeps the payload, shifts its measure/beat times 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), and lets add-arrangement stamp it.
- add-arrangement stamps `source:"musicxml"` + `source_notes_fp` at the
  one moment payload and notes are known in sync (new
  `_stamp_musicxml_notation`, the MusicXML counterpart of
  `_attach_gp_notation`) and returns it; the client carries it as
  `_gp_notation`, which the save body already ships.
- The authored-over-lift rail generalizes its source check to
  `_AUTHORED_NOTATION_SOURCES` ({"gp","musicxml"}) and preserves the
  ORIGINAL provenance on every re-stamp (kept-verbatim, measure-merge)
  instead of rewriting it to "gp". Fingerprint invalidation and the
  measure-granular merge behave identically for both sources.

And the per-note `hand` field ('lh'/'rh', absent = unassigned) becomes
real editor data: registered in `_NOTE_TECH_FIELDS` (string-valued,
default None — the signature builders now share `_note_tech_default` so
object- and dict-shaped notes can't drift), emitted on the save wire as
the spelled-out `hand` key (`rh` is taken by right_hand), strictly
validated to the enum, and it survives `reconstructChords` because it
rides `techniques`. MusicXML imports arrive with it pre-filled from
staff provenance (staff 1→rh, 2→lh; organ-pedal staves stay unassigned
rather than guessed).

Companion PR: feedBack-plugin-musicxml-import #8 (the parse-arrangement
endpoint that produces the arrangement + payload; without it the
MusicXML path stays "plugin not installed" exactly as before).

Tracked follow-ups (in CHANGELOG): core sloppak loader learns `hand`
for reload round-trips; hand EDITS don't yet invalidate a preserved
authored sidecar (the fingerprint deliberately ignores techniques);
`split_hands` respecting per-note overrides.

Tests: tests/test_musicxml_notation.py (23 — registry invariants, wire
validation incl. junk rejection, chord-member ride-along, stamping, and
the rail honoring/invalidating source:"musicxml" against the real core
lift) + tests/musicxml_hand_import.test.mjs (7 — notation shift
semantics, hand through reconstructChords/flatten round-trips). Full
gates: 325 pytest green, JS suite green (song_fit's failure reproduces
on pristine main — environmental), lint 0 errors / 3 baseline warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
…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
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e128ff26-0fe5-42d4-92d0-2aba3ef80a3f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/keys-hand-surface

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

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