Summary
The editor's undo commands store note array indices captured at construction time. On save, _buildSaveBody() runs flattenChords() + reconstructChords(), which rewrites arr.notes (and relocates same-time multi-note groups into arr.chords) — but the undo history is not cleared. So undoing a command created before a save can index into the wrong note or hit undefined.
Surfaced during the post-merge review of #16 (teaching marks). SetTeachingMarkCmd was flagged, but it is not the origin — it copies the exact index pattern used by every existing command.
Root cause
- Commands snapshot
this.indices / this.index into notes() at construction and roll back via notes()[i]: ChangeFretCmd, ResizeSustainCmd, MoveNoteCmd, SetBendShapeCmd, SetBendIntentCmd, SetTeachingMarkCmd, … (screen.js).
reconstructChords() does arr.notes = newNotes and moves same-time groups into arr.chords (screen.js ~line 666/714/728).
S.history = new EditHistory() runs only on song load (screen.js ~3173), not on save (_buildSaveBody ~3453).
Net: (stale undo stack) + (renumbered arr.notes after save) → wrong-note or undefined mutation on undo.
Why it's more reachable via teaching marks
"Group as strum" (ch) operates on multi-note same-time selections — exactly the groups reconstructChords() moves out of arr.notes into arr.chords. So that workflow is more likely than a typical single-note edit to have its indices invalidated by the next save.
Repro (sketch)
- Select a 2+ note same-time group, "Group as strum" (or any index-based edit).
- Save (sloppak or chart) →
reconstructChords() moves those notes into a chord, rewriting arr.notes.
- Undo → the command indexes into the post-reconstruct
arr.notes and mutates the wrong note / throws.
Options
- Invalidate the undo/redo stack on successful save (mirror the load-time
new EditHistory()). Simplest and correct; cost is losing cross-save undo.
- Identity-stable targets — give notes a stable id (or store object refs) and have commands resolve targets by id at exec/rollback, with
reconstructChords() preserving identity. More work, keeps cross-save undo.
- Re-key the live history after
reconstructChords() by mapping old→new positions. Fiddly; (1) or (2) preferred.
Recommend (1) as an immediate, low-risk fix; consider (2) later if cross-save undo is wanted.
Scope / honesty
Pre-existing and systemic — affects all index-based commands, not specific to teaching marks. No data-loss on the happy path (no save between edit and undo). Filing separately rather than patching one command.
Part of got-feedBack/feedBack#334 (surfaced during the teaching-marks review loop).
Summary
The editor's undo commands store note array indices captured at construction time. On save,
_buildSaveBody()runsflattenChords()+reconstructChords(), which rewritesarr.notes(and relocates same-time multi-note groups intoarr.chords) — but the undo history is not cleared. So undoing a command created before a save can index into the wrong note or hitundefined.Surfaced during the post-merge review of #16 (teaching marks).
SetTeachingMarkCmdwas flagged, but it is not the origin — it copies the exact index pattern used by every existing command.Root cause
this.indices/this.indexintonotes()at construction and roll back vianotes()[i]:ChangeFretCmd,ResizeSustainCmd,MoveNoteCmd,SetBendShapeCmd,SetBendIntentCmd,SetTeachingMarkCmd, … (screen.js).reconstructChords()doesarr.notes = newNotesand moves same-time groups intoarr.chords(screen.js~line 666/714/728).S.history = new EditHistory()runs only on song load (screen.js~3173), not on save (_buildSaveBody~3453).Net:
(stale undo stack) + (renumbered arr.notes after save) → wrong-note or undefined mutation on undo.Why it's more reachable via teaching marks
"Group as strum" (
ch) operates on multi-note same-time selections — exactly the groupsreconstructChords()moves out ofarr.notesintoarr.chords. So that workflow is more likely than a typical single-note edit to have its indices invalidated by the next save.Repro (sketch)
reconstructChords()moves those notes into a chord, rewritingarr.notes.arr.notesand mutates the wrong note / throws.Options
new EditHistory()). Simplest and correct; cost is losing cross-save undo.reconstructChords()preserving identity. More work, keeps cross-save undo.reconstructChords()by mapping old→new positions. Fiddly; (1) or (2) preferred.Recommend (1) as an immediate, low-risk fix; consider (2) later if cross-save undo is wanted.
Scope / honesty
Pre-existing and systemic — affects all index-based commands, not specific to teaching marks. No data-loss on the happy path (no save between edit and undo). Filing separately rather than patching one command.
Part of got-feedBack/feedBack#334 (surfaced during the teaching-marks review loop).