Skip to content

feat(editor): author chord harmony fn + template voicing (§6.3.1, §6.6) - #21

Merged
byrongamatos merged 1 commit into
mainfrom
feat/chord-harmony-authoring
Jun 21, 2026
Merged

feat(editor): author chord harmony fn + template voicing (§6.3.1, §6.6)#21
byrongamatos merged 1 commit into
mainfrom
feat/chord-harmony-authoring

Conversation

@byrongamatos

Copy link
Copy Markdown
Collaborator

Part of got-feedBack/feedBack#334

PR 3 of 3. Editor authoring for the per-chord harmony annotations carried by core wire (got-feedBack/feedBack#540) and drawn by the highway (got-feedBack/feedBack#541). Based on main (the core PRs are in a separate repo, so this is not git-stacked).

What

Adds E1 chord-inspector authoring for both annotations, mirroring the teaching-marks editor work (#16 / #17):

  • voicing (chord template, §6.6) — a <select> in the chord inspector (open/triad/shell/drop2/drop3/barre), undoable via the existing EditChordTemplateCmd. Added to relinkChordTemplate's carry-forward set so the save rebuild doesn't BLANK it (the same E0 gotcha as name/displayName/fingers/arp).
  • fn (chord instance, §6.3.1) — rn/q/deg inputs, one undo unit (EditChordFnCmd).

fn instance round-trip (the tricky part)

fn rides the chord instance, but the editor flattens chords to notes during editing and rebuilds them on save. Stamping fn on the notes is unsafe — a note dragged out of a chord would carry a stale fn into another group (caught in review). Instead fn is keyed by chord time in a transient arr._chordFn map (there is exactly one chord per time in the reconstruct model), independent of the movable notes:

  • flattenChords seeds the map from loaded chords.
  • EditChordFnCmd writes/rolls-back the map entry (one undo unit).
  • reconstructChords reads the map back onto the rebuilt chord and prunes orphan entries for times that no longer hold a chord.

Serialization (routes.py)

  • Load: _song_to_dict hydrates chord fn + template voicing.
  • Save: _arr_dict_to_wire emits both default-omitted and range-guarded — new _chord_fn_wire mirrors core's _validate_fn (never a partial / out-of-range fn); voicing only when a non-empty string.
  • GP/keys/drums import paths are untouched (no reliable per-chord function/voicing).

Honesty rule

Display/teaching authoring only. No scoring / NoteVerifier path is touched.

Deferral: client-side deg auto-fill

The editor has no active-key/tonic accessor (per-note sd auto-derivation is server-side), so deg is a manual 0–11 input, mirroring the existing per-note sd field. Noted for a follow-up if a key accessor lands.

Tested locally

  • python -m pytest90 passed, 2 skipped (incl. new tests/test_chord_harmony.py: fn emit/omit/range-guard, voicing emit/omit/trim/non-string).
  • node tests/chord_relink.test.js13 passed (voicing carry-forward + _normChordFn/_mergeChordFn).
  • node tests/teaching_marks.test.js + node tests/edit_history_reset.test.js still pass.
  • node --check screen.js clean.
  • Independent Codex review (read-only, diff-only): caught one P2 (note-stamped _chordFn could go stale on a move) — fixed by the time-keyed map + orphan prune above; re-review clean.

CI may be red on infra; the above was verified locally.

🤖 Generated with Claude Code

Add E1 chord-inspector authoring for the two feedpak 1.7.0 harmony annotations,
mirroring the teaching-marks editor work (#16/#17):

- voicing (TEMPLATE): a select in the chord inspector, undoable via the existing
  EditChordTemplateCmd. Added to relinkChordTemplate's carry-forward set so the
  save rebuild doesn't BLANK it (same E0 gotcha as name/displayName/fingers/arp).
- fn (INSTANCE): rn/q/deg inputs, one undo unit (EditChordFnCmd). fn rides the
  chord instance, so it's keyed by chord time in a transient arr._chordFn map
  (one chord per time) rather than stamped on movable notes — a note dragged out
  of a chord can't carry a stale fn into another group. reconstructChords reads
  the map back and prunes orphans for times that no longer hold a chord.

routes.py round-trips both: _song_to_dict hydrates chord fn + template voicing on
load; _arr_dict_to_wire emits them default-omitted and range-guarded (new
_chord_fn_wire mirrors core's _validate_fn — never a partial/out-of-range fn;
voicing only when a non-empty string). Display only — no scoring path touched.

Client-side deg auto-fill is deferred: the editor has no active-key/tonic
accessor (per-note sd auto-derivation is server-side), so deg is manual 0..11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byrongamatos
byrongamatos merged commit 584bd3f into main Jun 21, 2026
@byrongamatos
byrongamatos deleted the feat/chord-harmony-authoring branch June 21, 2026 09:04
byrongamatos added a commit that referenced this pull request Jun 21, 2026
…#21 follow-up) (#22)

Post-merge Codex review of #21 found fn desynced when a chord moved: fn was stored
in a time-keyed map `arr._chordFn`, so dragging a chord (which changes its time)
orphaned its fn, and a stale old-time entry could leak to a new chord there. The
time key is fragile against EVERY time-mutating edit — drag, global shift,
time-scale, offset, tempo remap — not just drag.

Make fn ride the chord INSTANCE the robust way: carry it on the note objects (which
all those edits mutate in place), and adopt a group's fn by majority on rebuild.

- flattenChords: seed each chord note's `_fn` from the chord's fn (every note of the
  chord gets the same value) instead of the time-keyed store.
- new _groupFn (pure, node-tested): a group's fn = the `_fn` carried by > half its
  notes, else null. Authoring writes `_fn` to all of a chord's notes (unanimous), so
  a real chord always keeps its fn; a lone note dragged in from another chord is
  outvoted and can't impose a stale fn — the exact property the time-keyed store was
  protecting, now without losing fn on a move.
- reconstructChords: adopt fn via _groupFn; a length-1 (solo) group drops its `_fn`
  so the internal field can't ride into the saved wire.
- _selectedChordContext: read fn via _groupFn over the time-group.
- EditChordFnCmd: write `_fn` to every note at the chord's time, snapshotting prior
  per-note `_fn` by object ref for undo. Removed the `arr._chordFn` store.

+3 _groupFn tests (move-survival, single-note-leak prevention, no-majority/empty).
Codex-reviewed (2 rounds — caught the solo-note wire leak): clean. node 16/16,
editor pytest 90 passed.

Part of got-feedBack/feedBack#334.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.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.

1 participant