feat(editor): tracks become first-class — the persistent track-session tree - #289
Conversation
…n tree
A song's tracks (master recording, studio stems, transcription parts,
optional folders) are now one ordered, persistent tree — the
editor_track_session manifest extension key — instead of surfaces
re-deriving ad-hoc lists from whatever the song contains.
The tree LAYERS OVER canonical song data, never replacing it:
- rows reference sources ('master' / bare manifest stem ids) and chart
tracks (_partViewKeyPure keys — the same dialect editor_stem_links
speaks); unknown references drop on normalize, new parts append,
parent cycles repair;
- pairing is NOT stored: rows project S.stemLinks, so the two stores
can never disagree (a link to a tombstoned source projects unpaired);
- sources are derived on demand from (audioUrl, S.stems) — no parallel
copy to go stale under /stem-op renames;
- removing an audio track is non-destructive: a removedSourceIds
tombstone hides the row, the media stays in the pack, restore
re-appends it;
- the tempo-guide role (which source is the timing reference, locked or
not, audio vs metronome) persists on the tree; its behavior ships
separately;
- a fully-default tree saves as null and the backend removes the key,
so untouched packs stay byte-identical (absent ≠ null: an older
client's absence has no authority to erase a persisted tree).
Wired at every persistence boundary (save, save-as, build, load) and
through the create/import seam #286 reserved (installCreatedTrackSession
now real: seeds S.stems from the server's audio_sources — bare ids,
unconditional reset so a previous song's stems can't leak into a fresh
import). This slice is model + persistence; the unified Tracks surface
that renders the tree ships separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
|
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 (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds a persistent editor track-session model with normalized track trees, folder and source operations, tempo-guide state, manifest load/save/build wiring, create/import initialization, and Python/JavaScript regression tests. ChangesTrack session persistence
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Editor
participant file_ops
participant routes
participant Sloppak
Editor->>file_ops: load track_session
file_ops->>routes: request persisted session
routes->>Sloppak: read editor_track_session
Sloppak-->>routes: return manifest payload
routes-->>file_ops: return sanitized track_session
file_ops-->>Editor: install normalized session
Editor->>file_ops: save track_session
file_ops->>routes: send serialized session
routes->>Sloppak: write editor_track_session
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@routes.py`:
- Around line 484-491: Update the removedSourceIds handling near
_track_session_id to bound the number of processed entries and deduplicate IDs
with a set for linear-time membership checks, while preserving the existing
filtering and output order in removed_sources. Use the established request limit
or an appropriate fixed maximum, and stop processing once that bound is reached.
In `@src/track-session.js`:
- Around line 124-130: Update the persisted-row normalization before the
canonical loops in track-session handling so folder or mismatched leaf records
cannot occupy IDs returned by audioTrackId or transcriptionTrackId. Reserve
canonical source and target IDs while processing persisted rows, or assign
collision-free IDs to conflicting leaf rows, ensuring the loops over
visibleSources and targets always append every canonical track.
🪄 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: 63d75665-aeab-4c37-9adf-3481bf812ef4
📒 Files selected for processing (9)
CHANGELOG.mdroutes.pysrc/create.jssrc/file-ops.jssrc/main.jssrc/state.jssrc/track-session.jstests/test_track_session_persist.pytests/track_session.test.mjs
A click/reference stem can be locked as the session's TEMPO GUIDE (the ♩ button in the Audio tracks manager; the role persists on the track-session tree). Assisted mapping (G) then analyzes THAT source — decoded on demand into an analysis-only cache, playback and the visible waveform untouched — with the click-track contract: consolidated transients are beat pulses walked by the chart's authored beats-per-measure, so tempo changes in the click are followed directly instead of rejected as drift. Pulse dropout continues on the recent median gap at low confidence, honestly flagged. Ordinary (non-guide) G fits now carry a low-confidence completion tail to the final authored barline (never auto-committed — proposals only), and a new Accept Whole Fit verb (tempo toolbar button + registry command) commits every proposal as ONE undoable TempoMapCmd. Accepting through the final downbeat rescales the open last measure's interior beats onto the accepted tempo (equal-length invariant preserved). Anchor semantics: the focused barline always anchors the fit — locked or not — and a stale multi-selection no longer resets analysis toward the beginning or caps the march (it is only an anchor fallback when nothing has focus). In the metronome engine a locked barline keeps its authored time without disturbing the pulse cursor, so one stale lock cannot phase-shift every later suggestion. Guide-routing discipline: G awaits the guide decode and revalidates its preconditions after the await (mode still on, guide unchanged); a new-song boundary resets the guide cache and orphans in-flight decodes. Design-doc and user-guide sections updated to match. Stacked on feat/editor-track-session (#289) — the guide fields ride the persistent track-session tree it introduced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
A click/reference stem can be locked as the session's TEMPO GUIDE (the ♩ button in the Audio tracks manager; the role persists on the track-session tree). Assisted mapping (G) then analyzes THAT source — decoded on demand into an analysis-only cache, playback and the visible waveform untouched — with the click-track contract: consolidated transients are beat pulses walked by the chart's authored beats-per-measure, so tempo changes in the click are followed directly instead of rejected as drift. Pulse dropout continues on the recent median gap at low confidence, honestly flagged. Ordinary (non-guide) G fits now carry a low-confidence completion tail to the final authored barline (never auto-committed — proposals only), and a new Accept Whole Fit verb (tempo toolbar button + registry command) commits every proposal as ONE undoable TempoMapCmd. Accepting through the final downbeat rescales the open last measure's interior beats onto the accepted tempo (equal-length invariant preserved). Anchor semantics: the focused barline always anchors the fit — locked or not — and a stale multi-selection no longer resets analysis toward the beginning or caps the march (it is only an anchor fallback when nothing has focus). In the metronome engine a locked barline keeps its authored time without disturbing the pulse cursor, so one stale lock cannot phase-shift every later suggestion. Guide-routing discipline: G awaits the guide decode and revalidates its preconditions after the await (mode still on, guide unchanged); a new-song boundary resets the guide cache and orphans in-flight decodes. Design-doc and user-guide sections updated to match. Stacked on feat/editor-track-session (#289) — the guide fields ride the persistent track-session tree it introduced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
A click/reference stem can be locked as the session's TEMPO GUIDE (the ♩ button in the Audio tracks manager; the role persists on the track-session tree). Assisted mapping (G) then analyzes THAT source — decoded on demand into an analysis-only cache, playback and the visible waveform untouched — with the click-track contract: consolidated transients are beat pulses walked by the chart's authored beats-per-measure, so tempo changes in the click are followed directly instead of rejected as drift. Pulse dropout continues on the recent median gap at low confidence, honestly flagged. Ordinary (non-guide) G fits now carry a low-confidence completion tail to the final authored barline (never auto-committed — proposals only), and a new Accept Whole Fit verb (tempo toolbar button + registry command) commits every proposal as ONE undoable TempoMapCmd. Accepting through the final downbeat rescales the open last measure's interior beats onto the accepted tempo (equal-length invariant preserved). Anchor semantics: the focused barline always anchors the fit — locked or not — and a stale multi-selection no longer resets analysis toward the beginning or caps the march (it is only an anchor fallback when nothing has focus). In the metronome engine a locked barline keeps its authored time without disturbing the pulse cursor, so one stale lock cannot phase-shift every later suggestion. Guide-routing discipline: G awaits the guide decode and revalidates its preconditions after the await (mode still on, guide unchanged); a new-song boundary resets the guide cache and orphans in-flight decodes. Design-doc and user-guide sections updated to match. Stacked on feat/editor-track-session (#289) — the guide fields ride the persistent track-session tree it introduced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
…it) (#290) * feat(editor): metronome guide + whole-song tempo fits (Accept Whole Fit) A click/reference stem can be locked as the session's TEMPO GUIDE (the ♩ button in the Audio tracks manager; the role persists on the track-session tree). Assisted mapping (G) then analyzes THAT source — decoded on demand into an analysis-only cache, playback and the visible waveform untouched — with the click-track contract: consolidated transients are beat pulses walked by the chart's authored beats-per-measure, so tempo changes in the click are followed directly instead of rejected as drift. Pulse dropout continues on the recent median gap at low confidence, honestly flagged. Ordinary (non-guide) G fits now carry a low-confidence completion tail to the final authored barline (never auto-committed — proposals only), and a new Accept Whole Fit verb (tempo toolbar button + registry command) commits every proposal as ONE undoable TempoMapCmd. Accepting through the final downbeat rescales the open last measure's interior beats onto the accepted tempo (equal-length invariant preserved). Anchor semantics: the focused barline always anchors the fit — locked or not — and a stale multi-selection no longer resets analysis toward the beginning or caps the march (it is only an anchor fallback when nothing has focus). In the metronome engine a locked barline keeps its authored time without disturbing the pulse cursor, so one stale lock cannot phase-shift every later suggestion. Guide-routing discipline: G awaits the guide decode and revalidates its preconditions after the await (mode still on, guide unchanged); a new-song boundary resets the guide cache and orphans in-flight decodes. Design-doc and user-guide sections updated to match. Stacked on feat/editor-track-session (#289) — the guide fields ride the persistent track-session tree it introduced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q * Revalidate asynchronous tempo guide analysis * fix(editor): unlock the tempo guide when its stem is renamed/deleted Locking a stem as the metronome guide records its source id in the track session, but the tracks-manager rename/delete controls rewrite S.stems (via _adopt) without touching the guide role, so tempoGuideSourceId can dangle. A dangling LOCKED guide is not harmless: assisted mapping (G) can't find the source live, and the save-time normalize silently repoints the still-locked role onto the first surviving source (usually the master recording) — so a reopened song would analyze the wrong track as a click. Reconcile at the choke point every stem-list mutation flows through (_adopt): if the locked guide's source vanished, unlock back to the default instead of transferring the lock. Reorder keeps ids, so it is a no-op there. Regression test drives the delete/rename path and pins that a live guide is left untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(editor): address CodeRabbit review on tempo guide (PR #290) Three still-valid findings from the CodeRabbit pass: - audio.js: coalesce concurrent guide analyses. Two rapid G presses for the same (sourceId, url) each bumped _guideGeneration, so the older request superseded itself and returned null, briefly overwriting the newer success with an analysis error. Reuse a keyed in-flight promise instead; different guides / song switches still supersede via the token. - tempo-suggest.js: stop proposal generation when an authoritative lock conflicts with the proposed chronology. The completion tail no longer demotes a lock behind the last emitted time into an unlocked inferred proposal at a fabricated time, and the metronome walk no longer emits an equal/decreasing locked downbeat time when the pulse walk has run ahead — both terminate. - track-session.js: unlock a stale locked tempo guide at load. A persisted tempoGuideSourceId that no longer exists was silently repointed onto master while staying locked (reconcile then bailed because the id resolved); the load seam now clears the lock/mode, mirroring the stem-op reconcile. Skipped: the "account for missing pulses" finding (heavy heuristic lift) — inserting inferred pulse slots on oversized gaps can't distinguish a missed transient from a genuine tempo change, which the metronome engine is designed to follow; it would fight the module's stated contract and needs tuning against real audio. Tracked as a follow-up, not a minimal root-cause fix. Regression tests added to tests/metronome_guide.test.mjs (all fail pre-fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: ChrisBeWithYou <chris@rifflarr.local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
What this is
The foundation slice of the DAW Tracks re-cut: a song's tracks — the master recording, studio stems, and every transcription part, optionally grouped into folders — become one ordered, persistent tree (
src/track-session.js, persisted as theeditor_track_sessionmanifest extension key), instead of each surface re-deriving its own ad-hoc list.This supersedes the data-model portion of #285, rebuilt on the current main so it speaks the landed vocabulary (
S.stems/S.stemLinksfrom #283,partMixkeys, theinstallCreatedTrackSessionseam #286 reserved). The rest of #285 (Metronome Guide, tempo-suggest continuation, marquee/bulk locks, the Tracks lanes UI) follows as separate PRs on top of this model.Design (the load-bearing choices)
'master'/ bare manifest stem ids) and chart tracks (_partViewKeyPurekeys — the SAME dialecteditor_stem_linksspeaks). Normalization drops unknown references, appends new song parts, and repairs parent cycles — a corrupted tree can never hide a branch or crash a load.editor_stem_links; tree rows project it (_trackSessionRowsPure), so the two stores cannot disagree. A link pointing at a tombstoned source projects as unpaired instead of resurrecting it.(audioUrl, S.stems)on demand, so a/stem-oprename/delete can't leave a stale parallel copy.removedSourceIds; the media stays in the pack and restore re-appends the row. The tempo-guide role repairs to the next visible source.nullwhen the tree carries nothing the song doesn't already express, and the backend removes the key — untouched packs stay byte-identical. Absent ≠ null: an older client's absence has no authority to erase a persisted tree (_FIELD_ABSENT, same contract aseditor_stem_links).tempoGuideSourceId/Locked/Mode) ship in the schema now, inert — so the Metronome Guide PR is frontend-only.installCreatedTrackSessionis now real (was the inert feat(editor): unify project creation around imported tracks #286 stub): create/import seedsS.stemsfrom the server'saudio_sources(bare ids — thestem:prefix is create-payload namespace, not persisted identity), with an unconditional reset so a previous song's stems can't leak into a fresh import (S.stemswas previously only written byloadCDLCand stem-op adoption).Wiring
Every persistence boundary:
/save(parse + manifest apply), save-as,/build(meta + apply),/load(manifest →data.track_session→installTrackSession, after arrangements/stems/drumTab so normalization sees the whole song, withdata.audio_urlpassed explicitly becauseS.audioUrlstill points at the previous song at that line).Tests
tests/track_session.test.mjs(13, real-import ESM): normalization/canonical order, key dialects, pairing projection incl. tombstone case, folder ops + descendant guard + cycle repair, non-destructive remove/restore round-trip, is-default matrix, the create-seam stems reset, and the save/build wire checks.tests/test_track_session_persist.py(7): absent/null/malformed authority contract, YAML round-trip, id bounds (names-with-spaces pass — chart keys are arrangement names today — control chars and unbounded ids don't), shape filtering, 300-track/120-char caps, and no-pairing-field-on-rows.Gates:
npm test181 suites green,python -m pytest291 passed / 2 skipped,npm run lint0 errors (3 pre-existing baseline warnings).What this deliberately does NOT do
No new UI surface (the unified Tracks lanes/header column is the follow-up), no engine changes, no stable arrangement ids (that lands with the instrument-identity track and slots in via
_partViewKeyPure's existingarr.idpreference), and no migration ofmanifest["stems"]order — the manifest list stays ingest-order-authoritative for the stem manager until the unified surface supersedes it.🤖 Generated with Claude Code
https://claude.ai/code/session_01EBQCHCNA81E9tHmSDHSe2Q
Summary by CodeRabbit