feat(editor): beat-primary note model (Phase A2) - #135
Conversation
…d (Phase A2) Promote a float note.beat to runtime truth; note.time (seconds) becomes a cache regenerated by timeOf(beat) on every grid change. On load every timed object (notes, chords + chord notes, anchors, hand-shapes, phrases, sections, drum hits) lifts a beat from its seconds (_liftAllBeats); a tempo flex reprojects every part from its beat (_reprojectAll); the save body strips the beat cache so the wire stays seconds-only (_stripBeatsFromSaveBody) — no spec change. Snapped notes keep their exact subdivision, off-grid notes their fractional beat; both follow the beat on a flex, so a tempo edit can no longer silently corrupt note timing. Retires the whole tempoRideScope machinery (the drum/all/per-part "which notes ride the grid" picker, its Ctrl+T toggle, and the _applyTempoRemap/_captureScopedTimes remap walk): under total reprojection there is no ride choice left to get wrong. Golden-gated: a test proves lift+reproject reproduces the pre-A2 all-parts _applyTempoRemap to 3 dp on a multi-part drifting-grid fixture before any legacy code was deleted. Full JS suite green (71/0). Tests: tests/beat_primary.test.js (9 groups incl. golden gate, exact undo, load/save identity, 1 ms invariant, no beat leak to wire). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013JgxKh99UAeQqmhzSc73tv
|
Important Review skippedNo new commits to review since the last review. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR replaces ride-scope-based tempo retiming with a beat-primary model, updates load/save and undo paths to lift and reproject beats, removes ride-scope UI and shortcut wiring, and adds tests plus changelog entries for the new behavior. ChangesBeat-primary refactor
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Loader
participant ScreenJS
participant TempoMapCmd
Loader->>ScreenJS: load song seconds
ScreenJS->>ScreenJS: _liftAllBeats(S.beats)
TempoMapCmd->>ScreenJS: exec() tempo grid edit
ScreenJS->>ScreenJS: _reprojectAll(beats -> seconds)
TempoMapCmd->>ScreenJS: rollback()
ScreenJS->>ScreenJS: restore exact seconds snapshot
ScreenJS->>ScreenJS: _stripBeatsFromSaveBody(body) on save
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
TempoMapCmd.rollback reprojected note seconds through _r3 on undo, silently quantizing sub-millisecond placement (a note at 1.23456 came back 1.235), so edit->undo->save diverged from the original save. exec now snapshots the exact pre-edit seconds of every timed object and rollback restores them verbatim; beat indexing is untouched (a flex preserves it). Redo re-snapshots on each exec. Adds a sub-ms regression test and a _reprojectAll invariant guard comment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
Phase A2 — beat-primary note model
Second step of the workspace-shell time-model foundation (after A1's
beatOf/timeOfconverter, #133). Promotes a note's musical beat to runtime truth; seconds become a derived cache.What changes
note.beat(float) is the truth;note.time(seconds) is a cache regenerated bytimeOf(beat)whenever the grid changes.beatlifted from its seconds via_liftAllBeats— beat is the truth from load onward._reprojectAll), replacing the old scoped remap.TempoGridCmd(re-indexing edits) re-lifts beats from the unchanged seconds._stripBeatsFromSaveBody) so.sloppak/archive stay seconds-only — no feedpak-spec change.Why (the corruption class this deletes)
A snapped note keeps its exact subdivision, an off-grid note its fractional beat, and both follow the beat on a flex — so a tempo edit can no longer silently corrupt note timing. This retires the entire
tempoRideScopemachinery: the drum/all/per-part "which notes ride the grid" picker, its Ctrl+T toggle,_applyTempoRemap,_captureScopedTimes/_restoreScopedTimes, and the ride-resolver pures. Under total reprojection there is no ride choice left to get wrong. Netscreen.jsis −402/+150 — it removes more than it adds.Trust-critical testing
This is the trust-critical migration, so it's over-tested. Golden gate: a test embeds the pre-A2
_applyTempoRemapand proves lift+reproject reproduces it to 3 dp on a multi-part drifting-grid fixture — the gate that had to pass before any legacy remap/ride-snapshot code was deleted.tests/beat_primary.test.js— 9 groups: golden gate · load→save identity on an unedited grid · flex keeps everybeatwhile seconds reproject · snapped-keeps-subdivision vs off-grid-keeps-fraction · reproject is total (old ride-corruption impossible) · <2-beat grid degrades to seconds-primary ·|time − timeOf(beat)| < 1 msinvariant · exact undo (TempoMapCmd exec→rollback restores times exactly) · save body never leaksbeat/beatEndto the wire. All new fns (_liftAllBeats/_reprojectAll/_eachTimed/_stripBeatsFromSaveBody) are absent on main, so the suite fails on main.Also updated:
eof_shortcuts.test.js(Ctrl+T now falls through tot → toggleTapsince the ride toggle is gone),loop_undo_mode.test.js/midi_tempo_import.test.js(harness wiring to the new helpers), and removedtempo_ride_parts.test.js(tested the deleted ride machinery).Verification
node --check screen.jscleanroutes.pychange (no pytest needed); wire format unchangedOrdering
Requires A1 (#133, merged). Phase A is parallel-safe with the feature backlog. Phase B (the shell chrome) intentionally waits for the open feature PRs to merge — not part of this PR.
Design source:
SONG-EDITOR-WORKSPACE-SHELL-CHARRETTE.md§1.3/§1.4/§1.10.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests