Skip to content

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

Merged
byrongamatos merged 3 commits into
mainfrom
feat/keys-hand-surface
Jul 16, 2026
Merged

feat(editor): keys hand authoring — set, stamp, shade; hand edits refresh the notation#303
byrongamatos merged 3 commits into
mainfrom
feat/keys-hand-surface

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Replaces #302 — it was stacked on #299's branch and GitHub auto-closed it (unrecoverably: a closed PR can't retarget) when that branch was deleted after the merge. This is the same change rebased onto post-wave main (clean rebase, no conflicts) with all gates re-run: 335 pytest, 189 JS suites green (the 2 failures — song_fit, mixer_meter_teardown — reproduce on pristine post-wave main; environmental), lint 0 errors / 3 baseline warnings.

What

The hand arc, Step B. The per-note hand field (#299, merged) 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). 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. 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. The relift honors per-note hands via core #992.

The pipeline this completes

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

Tests

tests/hand_authoring.test.mjs (parser incl. accidentals + junk; stamp orientation pinned to core split_hands; per-note command exec→undo→redo round-trip) + a fingerprint pytest (hand flips it, other techniques and junk don't, shapes agree). USER-GUIDE §5 gains "Hands on keys tracks".

🤖 Generated with Claude Code

https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

Summary by CodeRabbit

  • New Features
    • Added per-note left/right hand authoring for keys tracks with undo support.
    • Added split-based hand stamping for selected notes (or whole track) with editable overrides in a single undo step.
    • Added optional piano-roll hand shading to distinguish LH/RH vs unassigned notes.
    • Added Note ▸ Hand (keys), Track ▸ Assign hands by split… (keys), and View ▸ Hand shading (keys) actions.
  • Documentation
    • Expanded the user guide with a new “Hands on keyboard tracks” subsection.
  • Bug Fixes
    • Updated preserved-notation reuse so hand-split changes invalidate stale saved notation.
  • Tests
    • Added/expanded tests for split parsing, stamping/undo behavior, and hand-inclusive MusicXML fingerprinting.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 50f60a23-bf68-48e2-8b45-c7be1b259ae4

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
📝 Walkthrough

Walkthrough

Adds keys-track hand authoring with per-note left/right assignments, split-based stamping, undo support, piano-roll shading, menu actions, hand-aware notation fingerprints, tests, and documentation.

Changes

Keys hand authoring

Layer / File(s) Summary
Hand assignment and undoable updates
src/hand.js, src/commands.js, tests/hand_authoring.test.mjs
Adds split-pitch parsing, per-note hand stamping, selection actions, shading preference state, browser entry points, and undoable per-note technique updates with coverage for parsing, stamping, undo, and redo.
Hand menus and piano-roll rendering
src/menu-bar.js, src/draw.js
Adds Note, Track, and View menu actions and applies LH/RH colors to eligible piano-roll notes when shading is enabled.
Notation fingerprint and documented behavior
routes.py, tests/test_musicxml_notation.py, docs/USER-GUIDE.md, CHANGELOG.md
Includes valid hand assignments in note fingerprints and documents hand authoring, shading, split stamping, and saved notation behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Editor
  participant editorAssignHandsBySplit
  participant SetTechScalarPerNoteCmd
  participant NoteState
  participant PianoRoll
  Editor->>editorAssignHandsBySplit: enter split pitch
  editorAssignHandsBySplit->>SetTechScalarPerNoteCmd: stamp lh/rh values
  SetTechScalarPerNoteCmd->>NoteState: update techniques.hand
  NoteState->>PianoRoll: redraw notes
  PianoRoll->>Editor: show hand-specific shading
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% 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 summarizes the main editor hand-authoring changes: set, stamp, shading, and notation invalidation on hand edits.
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 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.

…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
ChrisBeWithYou force-pushed the feat/keys-hand-surface branch from 926c8fb to d0b4856 Compare July 16, 2026 18:57

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

🤖 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 64-70: Update the “Known tracked follow-ups” section to remove or
revise the clauses claiming hand edits do not refresh notation and relifting
does not honor per-note hands, preserving only follow-ups that remain
unimplemented and eliminating the contradiction with the changelog entry.

In `@docs/USER-GUIDE.md`:
- Line 160: Update the “Hands on keys tracks” section heading to use clear
compound wording, such as “Hands on keys-track arrangements” or “Hands on
keyboard tracks,” while leaving the section content unchanged.

In `@routes.py`:
- Around line 2571-2586: Update the fingerprint tuple built by _tup so its hand
component is always orderable, mapping absent/invalid hands and valid "lh"/"rh"
to consistent comparable values while preserving distinct fingerprints. Ensure
all note and chord-note fingerprint sorting uses these tuples safely, and add a
regression test covering duplicate/unison notes that differ only by hand.
🪄 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: 2a6ff771-a86d-4706-a845-50e6c09b686d

📥 Commits

Reviewing files that changed from the base of the PR and between d154a02 and 926c8fb.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • docs/USER-GUIDE.md
  • routes.py
  • src/commands.js
  • src/draw.js
  • src/hand.js
  • src/menu-bar.js
  • tests/hand_authoring.test.mjs
  • tests/test_musicxml_notation.py

Comment thread CHANGELOG.md
Comment thread docs/USER-GUIDE.md Outdated
Comment thread routes.py Outdated
…son 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>
@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.

@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.

🧹 Nitpick comments (1)
tests/test_musicxml_notation.py (1)

130-132: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert hand sensitivity for chord members, not only non-crashing behavior.

This assertion only proves that _notes_fingerprint returns a non-empty digest; it would still pass if chord-member hands were ignored. Compare the mixed-hand chord with an all-assigned counterpart and with the reversed member order.

Suggested test strengthening
-    chord = {"time": 0.0, "notes": [_editor_note(0.0, 60, "rh"), _editor_note(0.0, 60)]}
-    assert _notes_fingerprint([], [chord])  # no raise → truthy hex digest
+    mixed_chord_notes = [_editor_note(0.0, 60, "rh"), _editor_note(0.0, 60)]
+    mixed_chord = {"time": 0.0, "notes": mixed_chord_notes}
+    all_rh_chord = {
+        "time": 0.0,
+        "notes": [_editor_note(0.0, 60, "rh"), _editor_note(0.0, 60, "rh")],
+    }
+    reversed_chord = {"time": 0.0, "notes": list(reversed(mixed_chord_notes))}
+
+    assert _notes_fingerprint([], [mixed_chord]) != _notes_fingerprint([], [all_rh_chord])
+    assert _notes_fingerprint([], [mixed_chord]) == _notes_fingerprint([], [reversed_chord])
🤖 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/test_musicxml_notation.py` around lines 130 - 132, Strengthen the
chord-member test around _notes_fingerprint by comparing the mixed-hand chord
against an equivalent all-assigned chord and against the same members in
reversed order. Assert that hand assignment affects the fingerprint while member
ordering remains order-safe, rather than only asserting that a digest is
returned.
🤖 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 `@tests/test_musicxml_notation.py`:
- Around line 130-132: Strengthen the chord-member test around
_notes_fingerprint by comparing the mixed-hand chord against an equivalent
all-assigned chord and against the same members in reversed order. Assert that
hand assignment affects the fingerprint while member ordering remains
order-safe, rather than only asserting that a digest is returned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d4407076-74bc-43da-bd6f-43a6b9246fbf

📥 Commits

Reviewing files that changed from the base of the PR and between d0b4856 and a293eff.

📒 Files selected for processing (2)
  • routes.py
  • tests/test_musicxml_notation.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • routes.py

…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

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 merged commit a5bdae7 into main Jul 16, 2026
4 checks passed
@byrongamatos
byrongamatos deleted the feat/keys-hand-surface branch July 16, 2026 19:34
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