Skip to content

feat(editor): tracks become first-class — the persistent track-session tree - #289

Merged
byrongamatos merged 3 commits into
mainfrom
feat/editor-track-session
Jul 16, 2026
Merged

feat(editor): tracks become first-class — the persistent track-session tree#289
byrongamatos merged 3 commits into
mainfrom
feat/editor-track-session

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

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 the editor_track_session manifest 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.stemLinks from #283, partMix keys, the installCreatedTrackSession seam #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)

  • The tree layers over canonical song data, never replaces it. Rows reference sources ('master' / bare manifest stem ids) and chart tracks (_partViewKeyPure keys — the SAME dialect editor_stem_links speaks). Normalization drops unknown references, appends new song parts, and repairs parent cycles — a corrupted tree can never hide a branch or crash a load.
  • Pairing has ONE store. Chart↔stem pairing stays in 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.
  • Sources are derived, not stored — from (audioUrl, S.stems) on demand, so a /stem-op rename/delete can't leave a stale parallel copy.
  • Non-destructive audio removal. Deleting an audio row tombstones the source id in removedSourceIds; the media stays in the pack and restore re-appends the row. The tempo-guide role repairs to the next visible source.
  • A default tree persists as NO key. The save body ships null when 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 as editor_stem_links).
  • The guide fields (tempoGuideSourceId/Locked/Mode) ship in the schema now, inert — so the Metronome Guide PR is frontend-only.
  • installCreatedTrackSession is now real (was the inert feat(editor): unify project creation around imported tracks #286 stub): create/import seeds S.stems from the server's audio_sources (bare ids — the stem: 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.stems was previously only written by loadCDLC and stem-op adoption).

Wiring

Every persistence boundary: /save (parse + manifest apply), save-as, /build (meta + apply), /load (manifest → data.track_sessioninstallTrackSession, after arrangements/stems/drumTab so normalization sees the whole song, with data.audio_url passed explicitly because S.audioUrl still 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 test 181 suites green, python -m pytest 291 passed / 2 skipped, npm run lint 0 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 existing arr.id preference), and no migration of manifest["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

  • New Features
    • Added persistent track sessions that retain an ordered track tree across save, build, and reopen workflows.
    • Added non-destructive folder/track edits, including audio-track removal with restoration.
    • Added tempo-reference tracking for the session (audio vs metronome/guide mode) and persisted it from create-time through imports.
  • Bug Fixes
    • Malformed/absent session data can’t erase an existing saved track session.
    • Explicit clearing now fully removes the saved track-session override.
  • Tests
    • Added regression coverage for persistence, normalization, and save/build wiring.

…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
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a5cd3f5-9400-4d17-bca7-a64dc11f0053

📥 Commits

Reviewing files that changed from the base of the PR and between 54e2eef and 6ec0a2b.

📒 Files selected for processing (2)
  • routes.py
  • tests/test_track_session_persist.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/test_track_session_persist.py
  • routes.py

📝 Walkthrough

Walkthrough

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

Changes

Track session persistence

Layer / File(s) Summary
Track-session model and state
src/state.js, src/track-session.js
Adds canonical track-session state, source and target derivation, normalization, row projection, editing operations, lifecycle installation, default detection, and persistence serialization.
Manifest parsing and persistence
routes.py
Sanitizes editor_track_session, preserves existing values for absent or malformed input, removes the key for explicit null, and wires load, save, save-as, and build flows.
Editor load, create, save, and build wiring
src/file-ops.js, src/create.js, src/main.js
Installs track sessions during load and creation, and sends serialized track-session data through save and build requests.
Persistence and model validation
tests/test_track_session_persist.py, tests/track_session.test.mjs, CHANGELOG.md
Tests manifest coercion, bounds, normalization, editing behavior, lifecycle wiring, and persistence; documents the new manifest extension and behaviors.

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
Loading

Possibly related issues

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 clearly summarizes the main change: making tracks a persistent track-session tree in the editor.
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 feat/editor-track-session

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

📥 Commits

Reviewing files that changed from the base of the PR and between 35b7632 and 3e73a68.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • routes.py
  • src/create.js
  • src/file-ops.js
  • src/main.js
  • src/state.js
  • src/track-session.js
  • tests/test_track_session_persist.py
  • tests/track_session.test.mjs

Comment thread routes.py
Comment thread src/track-session.js
ChrisBeWithYou pushed a commit that referenced this pull request Jul 16, 2026
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
ChrisBeWithYou pushed a commit that referenced this pull request Jul 16, 2026
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
@byrongamatos
byrongamatos merged commit cdd7b8c into main Jul 16, 2026
4 checks passed
@byrongamatos
byrongamatos deleted the feat/editor-track-session branch July 16, 2026 18:28
byrongamatos pushed a commit that referenced this pull request Jul 16, 2026
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
byrongamatos added a commit that referenced this pull request Jul 16, 2026
…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>
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