fix(editor): normalize chord-template frets, wire-coerce arp, clear solo provenance - #153
Conversation
…olo provenance Closes #152. Three pre-existing data-integrity bugs in the chord save path, surfaced by CodeRabbit on #151 and each verified by running the real module. 1. relinkChordTemplate stored `frets.slice()` verbatim. A preserved template can arrive narrower than the chart — buildHandshapeChordIdMap's preserve-append hands it a template straight off the wire — so a 6-wide row survived on a 7/8-string chart, and a non-finite slot (undefined / NaN / a hand-edited string) rode through untouched. It now stores the width-normalized row. The fold that _fretKeyForL already applied for the lookup key is now a shared `_normFretsToL`, used by both. Key, preserved-lookup and stored row can no longer disagree — the same class of bug as the raw-join dedupe key fixed in #151. `fingers` was always padded to L this way; `frets` now matches. 2. `arp: !!(old && old.arp)` turned the STRING "false" into true. _safeWireBool exists in that same module for exactly this — its own comment names the case — and simply was not used. A hand-edited or legacy sloppak with `arp: "false"` no longer switches arpeggio on across a load->save round-trip. 3. A chord reduced to one note left `_fromChord` / `_chordId` on the survivor; they are now cleared alongside `_fn`. None of the three can reach disk — the backend's `_note()` whitelists the keys it writes — but `_fn` is READ BACK by `_groupFn`, so a stale one would be adopted by majority vote if that note were later dragged into a chord. The comment there claimed the delete was about the wire. It isn't; corrected. Also fixed the stale claim above flattenChords that reconstructChords groups by "time+_fromChord" (it groups by rounded time). NOT a bug, checked and closed out in the issue: `time: cn.time || ch.time` in _flattenArrChords does not drop first-beat notes. Chord-member `time` is absolute, so a chord on beat one has cn.time === ch.time === 0. Verified against Arcturus - The Sham Mirrors - Kinetic.feedpak: 66 chords, every member time equal to its chord's, none zero. Verified: node --test 86/86, pytest 248/248. Eight new cases in tests/chords.test.mjs, and EACH was re-run against the un-fixed code to confirm it fails there — a guard that passes both ways guards nothing. End-to-end: a real save -> reload round-trip through the running server on the chord-heavy Arcturus pack (8 templates, 66 chords). Intercepted the actual POST body: every fret row L-wide, every fret finite, every `arp` a real boolean, no editor-internal field on any note; "Saved successfully"; templates and chords both survive the reload. That proves the new shape is ACCEPTED by routes.py — the discriminating evidence for the bugs themselves is the unit suite. Headless draw / hit-test / resize harnesses green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes three data-integrity bugs in the chord-template save/relink path by ensuring fret rows are width-normalized, arp is wire-coerced correctly (avoiding the "false" → true trap), and chord-provenance internal fields are cleared when a chord collapses to a solo note.
Changes:
- Introduces a shared fret-row normalization fold (
_normFretsToL) and uses it for both template keying and storedfretsrows. - Switches
relinkChordTemplate’sarpcoercion to_safeWireBoolto correctly handle"false"and other wire spellings. - Clears
_fn,_fromChord, and_chordIdon lone notes duringreconstructChords, and adds targeted regression tests covering all three fixes.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/chords.js |
Normalizes stored template frets, wire-coerces arp, and clears chord provenance on solo survivors to prevent stale internal state from persisting. |
tests/chords.test.mjs |
Adds regression tests for fret-row widening/sanitization, arp coercion, and removal of chord-provenance fields on solo notes. |
CHANGELOG.md |
Documents the three fixed chord-template save-path issues and clarifies the rationale for clearing _fn. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughUpdates chord-template persistence in src/chords.js: fret rows are normalized to chart string width with non-finite slots folded to -1 via a new _normFretsToL helper used by relinkChordTemplate and _fretKeyForL; arp coercion switches to _safeWireBool; solo notes have _fromChord/_chordId cleared alongside _fn. Adds regression tests and changelog entries. ChangesChord template and provenance fixes
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Closes #152. Three pre-existing data-integrity bugs in the chord save path, surfaced by CodeRabbit on #151 and each verified by running the real module, not inferred from the diff.
1.
relinkChordTemplatestored an un-normalized fret rowIt did
frets: frets.slice()on whatever it was handed. A preserved template can arrive narrower than the chart —buildHandshapeChordIdMap's preserve-append passes one straight off the wire — and a non-finite slot (undefined/NaN/ a hand-edited string) rode through untouched.The fold
_fretKeyForLalready applied for the lookup key is now a shared_normFretsToL, used by both. Key, preserved-lookup, and stored row can no longer disagree — the same class of bug as the raw-join dedupe key fixed in #151.fingershas always been padded toLthis way;fretsnow matches. Padding only ever appends-1(unfretted), so no fret data is lost.2.
arp: !!(old && old.arp)turned the string"false"intotrue_safeWireBoollives in that same module for exactly this — its own comment names the case — and simply wasn't used. A hand-edited or legacy sloppak carryingarp: "false"no longer switches arpeggio on across a load→save. Every input the old code accepted as true (true,'true','1',1) still reads true.3. Solo notes kept
_fromChord/_chordIdA chord reduced to one note pushed the survivor by reference with its chord provenance intact. Now cleared alongside
_fn.But not for the reason CodeRabbit gave. None of the three can reach disk — the backend's
_note()whitelists the keys it writes. The reason_fnmust go is that_groupFnreads it back: a stale_fnwould be adopted by majority vote if that note were later dragged into a chord. The comment there claimed the delete was about the wire; corrected. Also fixed the stale claim aboveflattenChordsthatreconstructChordsgroups bytime+_fromChord— it groups by roundedtimealone.Not a bug — checked and closed out
CodeRabbit's fourth finding, that
time: cn.time || ch.timedrops first-beat notes. It doesn't: chord-membertimeis absolute, so a chord on beat one hascn.time === ch.time === 0. Verified onArcturus - The Sham Mirrors - Kinetic.feedpak: 66 chords, every member's time equal to its chord's, none zero.Verification
node --test86/86,pytest248/248.Eight new cases in
tests/chords.test.mjs— and each was re-run against the un-fixed code to confirm it fails there. A guard that passes both ways guards nothing.Real save → reload round-trip through the running server, on the chord-heavy Arcturus pack (8 templates, 66 chords). I intercepted the actual POST body rather than trusting the UI:
fretswidth ≠ 6arp_fromChord/_chordId/_fnSaved successfullyTo be precise about what that proves: the round-trip shows the new shape is accepted and preserved by
routes.py. The discriminating evidence for the bugs themselves is the unit suite above.Headless draw / hit-test / resize harnesses green. Codex preflight: NO ISSUES.
🤖 Generated with Claude Code
Summary by CodeRabbit