Skip to content

fix(editor): route Sync / BPM-rescale / Offset through undoable total-reproject commands - #218

Merged
byrongamatos merged 5 commits into
mainfrom
fix/editor-tempo-op-commands
Jul 12, 2026
Merged

byrongamatos merged 5 commits into
mainfrom
fix/editor-tempo-op-commands

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

The P0

Three whole-song tempo operations — Sync tempo, the BPM box's constant-tempo rescale, and the audio Offset nudge — mutated the timeline directly, with no undo, and moved only the current arrangement's plain notes() plus the global beat grid and sections. Everything else was left behind:

  • every other arrangement (a bass/keys part next to the guitar)
  • all chords, anchors, handshapes, phrases
  • the drum tab (on the Sync and rescale paths)

So a multi-part song silently drifted out of phase and the damage couldn't be undone. This is the charrette's opening P0 (TEMPO-MAPPING-CHARRETTE.md).

The fix

All three now route through one command eachTempoMapCmd ('sync' / 'rescale') and the new TempoOffsetCmd ('offset') — whose beat-primary lift→reproject is total: _eachTimed walks every timed object in every part, so a grid change is structurally incapable of stranding a note. Fully undoable, exact-seconds rollback.

  • Pivot at the first downbeat (or the focused barline) instead of t=0: t' = t0 + (t − t0)·scale. A song with a pickup / lead-in no longer skews under a bare time *= factor. New pure _tempoPivotTimePure.
  • S.appliedOffset replaces the Offset input's hidden dataset.applied, so undo restores the applied shift (the next nudge's delta was computing off a stale base). _effectiveAudioOffset() reads it; TempoOffsetCmd owns it undoably; keeps the drum-hit ≥0 clamp.
  • Removed the Sync dialog's duplicate offset field — the toolbar Offset is the single, undoable path (design doc demotes it).
  • TempoMapCmd now carries songScope = true (matching TempoGridCmd) so these toolbar-fired ops aren't refused by the read-only-roll lock.

Tests

  • tests/tempo_op_commands.test.mjs (new): deep-diffs a two-arrangement song with chords + drums, proving every part moves on a sync stretch and undo restores the exact pre-edit seconds; TempoOffsetCmd rigid-shift + past-grid extrapolation pin + drum ≥0 clamp + exec→undo→redo; _tempoPivotTimePure cases; and source guards that each wrapper routes through the commands (fail-on-main wiring proof).
  • Removed obsolete apply_offset.test.js (sliced the deleted _shiftArrangementTimes); updated beat_lock.test.mjs's section case off the deleted inline loop.
  • npm test 114 files green, npm run lint 0 errors (3 pre-existing warnings = main's ratchet). routes.py untouched.

First PR of the tempo-mapping track (TEMPO-MAPPING-PR-PROMPTS.md, PR 1). PRs 3/6/9 depend on this pivot + offset command.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed whole-song tempo edits affecting multi-part songs, ensuring all timed elements move together and undo/redo restores timing correctly.
    • Tempo syncing/BPM changes now rescale around the first downbeat (or the focused barline) with consistent beat re-spacing.
    • Audio offset nudges are now fully undoable, including correct state restoration and no invalid negative drum-hit timings.
  • Improvements
    • Updated the Sync Tempo dialog by removing the duplicate time offset field and adding a manual audio BPM override.
  • Tests
    • Added regression coverage for tempo and offset operations across multi-arrangement content.

…-reproject commands

Sync tempo, the BPM box's constant-tempo rescale, and the audio Offset nudge
each mutated the timeline directly with no undo, walking only the current
arrangement's plain notes plus the global beats/sections. Every other
arrangement — and all chords/anchors/handshapes/phrases, plus the drum tab on
the partial paths — was left behind: silent multi-part corruption that couldn't
be undone.

All three now route through one command (TempoMapCmd / TempoOffsetCmd) whose
beat-primary lift->reproject is total (every timed object, every part) and fully
undoable. Sync and rescale pivot the scale at the first downbeat (or the focused
barline) rather than t=0, so a pickup/lead-in song no longer skews. The applied
audio offset moved from the DOM input's dataset.applied onto command-owned
S.appliedOffset so undo restores it; the Sync dialog's duplicate offset field
was removed in favor of the undoable toolbar Offset.

New tests/tempo_op_commands.test.mjs deep-diffs a two-arrangement song with
chords + drums to prove every part moves and undo restores the exact seconds,
plus source guards that the wrappers route through the commands. Removes the
obsolete apply_offset.test.js (sliced the deleted _shiftArrangementTimes) and
updates beat_lock.test.mjs's section case off the deleted inline loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b513aa2f-8c1d-4782-8f55-5ba51b678191

📥 Commits

Reviewing files that changed from the base of the PR and between d194eb0 and 046f09a.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • screen.html
  • src/file-ops.js
  • src/main.js
  • src/state.js
  • src/tempo.js
  • tests/tempo_markers.test.mjs
  • tests/tempo_op_commands.test.mjs
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/file-ops.js
  • screen.html
  • src/state.js
  • CHANGELOG.md
  • tests/tempo_op_commands.test.mjs

📝 Walkthrough

Walkthrough

Tempo sync, BPM rescaling, and audio offset edits now use undoable tempo commands with pivot-based beat reprojection. Applied offset state moved into editor state, the duplicate Sync offset field was replaced by a manual BPM override, and multi-part undo/redo coverage was added.

Changes

Tempo operation routing

Layer / File(s) Summary
Tempo command primitives
src/state.js, src/tempo.js
Adds pivot selection and TempoOffsetCmd, makes tempo commands song-scoped, tracks applied offset in state, and corrects phrase anchor handling.
Sync and BPM rescaling
src/sync-tempo.js, src/main.js
Routes sync and constant-tempo BPM changes through pivoted TempoMapCmd execution with lock-aware respacing.
Audio offset state and UI
src/main.js, src/file-ops.js, screen.html, src/sync-tempo.js
Routes offset changes through TempoOffsetCmd, resets command-owned offset state on load, and replaces the Sync offset input with a manual BPM override.
Cross-part validation and release notes
tests/tempo_op_commands.test.mjs, tests/beat_lock.test.mjs, tests/beat_primary.test.mjs, tests/tempo_markers.test.mjs, CHANGELOG.md
Adds multi-part timing, undo/redo, routing, beat-lock, phrase-remapping, and meter-marker coverage, and documents the changes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SyncDialog
  participant Editor
  participant History
  participant TempoMapCmd
  participant SongParts
  SyncDialog->>Editor: submit tempo factor
  Editor->>TempoMapCmd: create pivoted respaced grid
  Editor->>History: execute command
  History->>SongParts: reproject notes, phrases, drums, and sections
  History->>Editor: redraw and update status
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 accurately summarizes the core change: routing Sync, BPM rescale, and Offset through undoable reproject commands.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/editor-tempo-op-commands

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

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

🤖 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 `@src/main.js`:
- Around line 1413-1451: The editorApplyOffset function currently bypasses undo
history when S.beats.length is less than 2. Remove that special-case branch,
including its direct S.appliedOffset update and status handling, so every
meaningful offset change proceeds through TempoOffsetCmd and S.history.exec;
retain the existing no-op delta check and normal UI updates.
🪄 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: 25ca6c4b-f77a-4f9c-9586-ec952a770279

📥 Commits

Reviewing files that changed from the base of the PR and between 2a764a1 and dc3a51e.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • screen.html
  • src/file-ops.js
  • src/main.js
  • src/state.js
  • src/sync-tempo.js
  • src/tempo.js
  • tests/apply_offset.test.js
  • tests/beat_lock.test.mjs
  • tests/tempo_op_commands.test.mjs
💤 Files with no reviewable changes (2)
  • tests/apply_offset.test.js
  • screen.html

Comment thread src/main.js
byrongamatos and others added 2 commits July 12, 2026 19:09
…/redo

Undo restored S.appliedOffset but left the visible toolbar input stale, and
editorNudgeOffset computes the NEXT offset from el.value — so one +10ms click
after a Ctrl-Z re-applied the undone nudge on top (input 0.010 → passes 0.020,
delta = 0.020 − 0 = 0.020). exec/rollback now both sync the input to
S.appliedOffset (typeof-document guarded for the node suites). Regression test
fails pre-fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…— every phrase was stranded by tempo ops

Phrases anchor on start_time (input.js authoring, routes.py save); _eachTimed
visited ph.time, so lift skipped every phrase and reproject left them on the
old timeline through sync / BPM-rescale / offset — the exact stale-scale bug
class this PR set out to close, hidden because the fixtures (here AND in the
pre-A2 golden's frozen legacy reference) seeded the same wrong field, making
the phrase assertions vacuous. Now visits ('start_time', 'span') so end_time
(present on server-loaded phrases) rides too; fixtures use the real shape and
the tempo_op_commands phrase assertions fail pre-fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he direct S.appliedOffset write skipped history

beatOf/timeOf are identity on a degenerate grid, so the command's
lift->reproject is a no-op on note seconds and just carries the scalar,
undoably. The special case made a reachable offset change non-undoable:
Ctrl-Z restored nothing and the next nudge's delta computed off a base
history never recorded.

Addresses CodeRabbit review on PR #218.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ChrisBeWithYou pushed a commit that referenced this pull request Jul 12, 2026
Charrette UX P5 / arch 1 (first half). Select and delete multiple Tempo Map
barlines at once, without touching the single-focus model (S.tempoSel — which
inspector / tap / lock / modulate / suggest all key on).

- New S.tempoSelMulti: Set<downbeatIdx> (the S.drumSel pattern). Shift+click a
  pole extends the contiguous downbeat range; a drag on empty grid arms a
  tempo-marquee (the drum editor's deferred-3px `moved` idiom) that box-selects
  downbeats in its swept X range (plain replaces, Shift unions); Ctrl+A in mode
  selects every downbeat. The set is index-based, so it is CLEARED — never
  remapped — in TempoGridCmd exec/rollback and on Tempo Map exit.
- Render: a light amber wash across the selected range, and selected poles read
  amber (the focus keeps its unique halo). Rides the existing draw pass.
- Bulk delete = ONE TempoGridCmd: _tempoDeleteBarlinesPure demotes every selected
  INTERIOR downbeat (never the first/last — the existing guard, generalized to a
  set) + one renumber. Reachable via Del and right-click "Delete N barlines".
- Escape clears the selection, layered UNDER #215's suggest-dismiss (ghosts own
  Escape while showing).

tests/tempo_multiselect.test.mjs (6): marquee hit math, the delete transform
(demote+renumber, first/last guard), the bulk-delete round-trip through the
command, the exec/rollback set-clearing contract, and range selection. Verified
live: Ctrl+A → "105 barlines selected" (amber render), Escape clears, a marquee
box-selects, Ctrl+A+Del → "Deleted 103 barlines" (first/last kept), no errors.
npm test 115 green, lint 0 errors.

Independent of the in-review queue (deps only on merged #215); keep-both seams
with #220 (_tempoMapDraw) and #218/#225 on the shared tempo.js.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
byrongamatos pushed a commit that referenced this pull request Jul 12, 2026
…5a) (#226)

Charrette UX P5 / arch 1 (first half). Select and delete multiple Tempo Map
barlines at once, without touching the single-focus model (S.tempoSel — which
inspector / tap / lock / modulate / suggest all key on).

- New S.tempoSelMulti: Set<downbeatIdx> (the S.drumSel pattern). Shift+click a
  pole extends the contiguous downbeat range; a drag on empty grid arms a
  tempo-marquee (the drum editor's deferred-3px `moved` idiom) that box-selects
  downbeats in its swept X range (plain replaces, Shift unions); Ctrl+A in mode
  selects every downbeat. The set is index-based, so it is CLEARED — never
  remapped — in TempoGridCmd exec/rollback and on Tempo Map exit.
- Render: a light amber wash across the selected range, and selected poles read
  amber (the focus keeps its unique halo). Rides the existing draw pass.
- Bulk delete = ONE TempoGridCmd: _tempoDeleteBarlinesPure demotes every selected
  INTERIOR downbeat (never the first/last — the existing guard, generalized to a
  set) + one renumber. Reachable via Del and right-click "Delete N barlines".
- Escape clears the selection, layered UNDER #215's suggest-dismiss (ghosts own
  Escape while showing).

tests/tempo_multiselect.test.mjs (6): marquee hit math, the delete transform
(demote+renumber, first/last guard), the bulk-delete round-trip through the
command, the exec/rollback set-clearing contract, and range selection. Verified
live: Ctrl+A → "105 barlines selected" (amber render), Escape clears, a marquee
box-selects, Ctrl+A+Del → "Deleted 103 barlines" (first/last kept), no errors.
npm test 115 green, lint 0 errors.

Independent of the in-review queue (deps only on merged #215); keep-both seams
with #220 (_tempoMapDraw) and #218/#225 on the shared tempo.js.


Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

Co-authored-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

🤖 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 `@tests/tempo_markers.test.mjs`:
- Around line 37-44: Update the beat fixture in the within-tolerance test around
_tempoMarkersPure so measure 2 has a subsequent downbeat, ensuring its BPM is
calculated from the constructed span rather than using the final-run fallback.
Preserve the 60.005 BPM duration and assert that only the baseline tempo marker
is emitted.
🪄 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: b513aa2f-8c1d-4782-8f55-5ba51b678191

📥 Commits

Reviewing files that changed from the base of the PR and between d194eb0 and 046f09a.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • screen.html
  • src/file-ops.js
  • src/main.js
  • src/state.js
  • src/tempo.js
  • tests/tempo_markers.test.mjs
  • tests/tempo_op_commands.test.mjs
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/file-ops.js
  • screen.html
  • src/state.js
  • CHANGELOG.md
  • tests/tempo_op_commands.test.mjs

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 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 `@tests/tempo_markers.test.mjs`:
- Around line 37-44: Update the beat fixture in the within-tolerance test around
_tempoMarkersPure so measure 2 has a subsequent downbeat, ensuring its BPM is
calculated from the constructed span rather than using the final-run fallback.
Preserve the 60.005 BPM duration and assert that only the baseline tempo marker
is emitted.
🪄 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: b513aa2f-8c1d-4782-8f55-5ba51b678191

📥 Commits

Reviewing files that changed from the base of the PR and between d194eb0 and 046f09a.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • screen.html
  • src/file-ops.js
  • src/main.js
  • src/state.js
  • src/tempo.js
  • tests/tempo_markers.test.mjs
  • tests/tempo_op_commands.test.mjs
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/file-ops.js
  • screen.html
  • src/state.js
  • CHANGELOG.md
  • tests/tempo_op_commands.test.mjs
🛑 Comments failed to post (1)
tests/tempo_markers.test.mjs (1)

37-44: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test doesn't actually exercise the tolerance comparison it claims to.

db here is [0, 4] — measure 2 (index 4) is the last downbeat, so _tempoMarkersPure takes the nextI === null branch and sets bpm = runBpm directly, never computing the ratio from the carefully constructed span. The test would pass identically even if the > tol comparison were broken, since that branch is never reached for this beat set.

🧪 Proposed fix: close measure 2 so its BPM is actually computed
     const span = (4 * 60) / 60.005;   // m2 duration for 60.005 BPM
-    const beats = g([0, 1], [1, -1], [2, -1], [3, -1], [4, 2], [4 + span / 4, -1], [4 + span / 2, -1], [4 + 3 * span / 4, -1]);
+    const beats = g([0, 1], [1, -1], [2, -1], [3, -1], [4, 2], [4 + span / 4, -1], [4 + span / 2, -1], [4 + 3 * span / 4, -1], [4 + span, 3]);
     const mk = _tempoMarkersPure(beats, 0.01);
     assert.deepStrictEqual(mk.filter(m => m.kind === 'tempo').length, 1, 'only the baseline tempo');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

t('a within-tolerance drift does NOT emit a new tempo marker (run behaviour)', () => {
    // m2 spans 4.001s → ~59.985 BPM, inside 0.01 of 60? No — but test the run:
    // make m2 exactly 60.005 BPM (within 0.01 tol → no marker).
    const span = (4 * 60) / 60.005;   // m2 duration for 60.005 BPM
    const beats = g([0, 1], [1, -1], [2, -1], [3, -1], [4, 2], [4 + span / 4, -1], [4 + span / 2, -1], [4 + 3 * span / 4, -1], [4 + span, 3]);
    const mk = _tempoMarkersPure(beats, 0.01);
    assert.deepStrictEqual(mk.filter(m => m.kind === 'tempo').length, 1, 'only the baseline tempo');
});
🤖 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/tempo_markers.test.mjs` around lines 37 - 44, Update the beat fixture
in the within-tolerance test around _tempoMarkersPure so measure 2 has a
subsequent downbeat, ensuring its BPM is calculated from the constructed span
rather than using the final-run fallback. Preserve the 60.005 BPM duration and
assert that only the baseline tempo marker is emitted.

@byrongamatos
byrongamatos merged commit 61d6189 into main Jul 12, 2026
4 checks passed
byrongamatos added a commit that referenced this pull request Jul 12, 2026
…d) (#225)

* fix(editor): route Sync / BPM-rescale / Offset through undoable total-reproject commands

Sync tempo, the BPM box's constant-tempo rescale, and the audio Offset nudge
each mutated the timeline directly with no undo, walking only the current
arrangement's plain notes plus the global beats/sections. Every other
arrangement — and all chords/anchors/handshapes/phrases, plus the drum tab on
the partial paths — was left behind: silent multi-part corruption that couldn't
be undone.

All three now route through one command (TempoMapCmd / TempoOffsetCmd) whose
beat-primary lift->reproject is total (every timed object, every part) and fully
undoable. Sync and rescale pivot the scale at the first downbeat (or the focused
barline) rather than t=0, so a pickup/lead-in song no longer skews. The applied
audio offset moved from the DOM input's dataset.applied onto command-owned
S.appliedOffset so undo restores it; the Sync dialog's duplicate offset field
was removed in favor of the undoable toolbar Offset.

New tests/tempo_op_commands.test.mjs deep-diffs a two-arrangement song with
chords + drums to prove every part moves and undo restores the exact seconds,
plus source guards that the wrappers route through the commands. Removes the
obsolete apply_offset.test.js (sliced the deleted _shiftArrangementTimes) and
updates beat_lock.test.mjs's section case off the deleted inline loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

* feat(editor): flatten dialog names both directions (conform vs rebuild)

Charrette UX P2 / arch 4 (tempo-mapping PR 3). Typing a BPM for a song with a
variable tempo map replaced the bare window.confirm with a small in-app dialog
(new _editorPromptChoice in ui.js, same modal idiom as _editorPromptText) that
NAMES the two genuinely-different edits:

- Conform notes to the new tempo — the previously-missing path. Flattens in the
  TempoMapCmd direction: beats are truth, seconds reproject onto the flat grid,
  so every part (all arrangements, chords, drums, anchors, handshapes) rides to
  the new constant tempo. No hand-scaled seconds (an invariant violation on
  locked/warped grids — the command's lift→reproject is the safe path).
- Rebuild the grid only — today's flatten (TempoGridCmd): seconds hold, beats
  re-lift, for when the notes already sit on the recording.

Both use the same flat grid from _tempoFlattenToBpmPure (anchored at bar 1 =
PR 1's pivot) and are undoable; the status lines name the result + "Undo
restores the map." editorSetBPM's flatten branch becomes async to await the
choice.

tests/flatten_choice.test.mjs: conform moves notes in ALL parts (2-arr song)
and undo restores; rebuild keeps every note's exact seconds while the grid
flattens; _tempoFlattenToBpmPure keeps the beat count + bar-1 anchor.
tests/prompt_choice.test.mjs: the dialog resolves the chosen key, Cancel→null,
and a second dialog settles the first. npm test 116 green, lint 0 errors.

Stacked on #218 (needs its pivot helper) — base this PR on
fix/editor-tempo-op-commands; after #218 squash-merges, rebase onto main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q

* fix(editor): sync the #editor-offset input across TempoOffsetCmd undo/redo

Undo restored S.appliedOffset but left the visible toolbar input stale, and
editorNudgeOffset computes the NEXT offset from el.value — so one +10ms click
after a Ctrl-Z re-applied the undone nudge on top (input 0.010 → passes 0.020,
delta = 0.020 − 0 = 0.020). exec/rollback now both sync the input to
S.appliedOffset (typeof-document guarded for the node suites). Regression test
fails pre-fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(editor): _eachTimed visited phrases via a field they never carry — every phrase was stranded by tempo ops

Phrases anchor on start_time (input.js authoring, routes.py save); _eachTimed
visited ph.time, so lift skipped every phrase and reproject left them on the
old timeline through sync / BPM-rescale / offset — the exact stale-scale bug
class this PR set out to close, hidden because the fixtures (here AND in the
pre-A2 golden's frozen legacy reference) seeded the same wrong field, making
the phrase assertions vacuous. Now visits ('start_time', 'span') so end_time
(present on server-loaded phrases) rides too; fixtures use the real shape and
the tempo_op_commands phrase assertions fail pre-fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(editor): re-validate session + variable-map after the flatten dialog await

The flatten choice dialog awaits across real time. The overlay traps
pointer and keyboard, but an already-in-flight async import can land
while the dialog is open, swapping the session and beat grid — the
user's choice would then flatten the NEW song unprompted. Re-check
S.sessionId and the variable-map precondition after the await, before
either command executes. (window.confirm was synchronous, so this gap
is new with the dialog.)

Also correct the _editorPromptChoice doc: the backdrop re-focuses the
dialog (like the editor's other modals); it does not dismiss.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.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.

2 participants