Skip to content

refactor(stores): lean setlist shape with reactive catalog overlay#96

Open
silverbucket wants to merge 3 commits into
masterfrom
refactor/lean-setlist-shape
Open

refactor(stores): lean setlist shape with reactive catalog overlay#96
silverbucket wants to merge 3 commits into
masterfrom
refactor/lean-setlist-shape

Conversation

@silverbucket
Copy link
Copy Markdown
Owner

@silverbucket silverbucket commented May 7, 2026

Summary

  • Setlist entries now reference the catalog by id ({ songId, performance }) instead of cloning catalog fields. Display goes through displayedSetlist / displayedSavedSetlists derived, which runs scoreFixedOrder once against the live catalog — RS pulls and local edits flow into every screen via Svelte reactivity, no manual sync.
  • Removes syncSavedSongIntoSetlist, stripEnergy, and the saveSong mirror block. scoreFixedOrder was being called from four places (generator, reorder, remove, add); now it lives inside the derived only. Mutation helpers collapse to array splices.
  • Saved setlists migrate from the fat shape via rs-migrate v2; localStorage current-set normalizes on read. Loading a saved setlist whose songs were deleted from the catalog drops them silently with a toast.

Why

Original report: RS-driven song changes updated the Songs view but not the Roll screen. Initial fix was an imperative mirror (syncCatalogIntoSetlists); on review that was patching the symptom — the live and saved setlists embedded deep clones of catalog fields, so reactivity couldn't propagate. This refactor makes the catalog the single source of truth and lets Svelte's reactive overlay do the work.

Notable

  • Net code change: +221 / -193, with the imperative scoring scattered across mutation paths collapsed into a single derived.
  • No rs-migrate version bump needed for the live setlist (read-side normalizer covers it); v2 covers saved setlists in RS storage.
  • Saved setlists are no longer point-in-time snapshots — renaming a catalog song updates its name everywhere, including the saved viewer.

Test plan

  • npm test — 291/291 pass (existing) + new migration tests.
  • npm run lint — no new warnings.
  • Manual: roll a setlist, edit a song's name in Songs view → Roll screen and Saved viewer reflect immediately.
  • Manual: from a second device, edit a song over RS → live and saved screens update without action.
  • Manual: save a setlist, delete one of its songs from catalog → song disappears from saved view; load shows dropped-count toast.
  • Manual: refresh the app with an old (fat) current-set localStorage entry → normalizer upgrades it on read; setlist renders.

Summary by CodeRabbit

  • Refactor

    • Restructured setlist data storage to use a leaner persisted format with derived display hydration.
    • Updated setlist state management to distinguish between persisted and display-ready formats.
  • Tests

    • Updated test fixtures and migrations tests to align with new setlist data structure.
  • Chores

    • Added data migration for converting existing setlists to the new schema format.

Review Change Stack

Setlist entries now reference the catalog by id instead of cloning
catalog fields. A `displayedSetlist` / `displayedSavedSetlists` derived
runs scoreFixedOrder against the live catalog, so RS pulls and local
edits propagate to every screen via Svelte reactivity — no manual sync
helpers, no mirror blocks. Reorder/remove/add collapse to lean array
splices since the derived handles scoring. Saved setlists migrate from
the fat shape via rs-migrate v2; the localStorage current-set
normalizes on read. loadSavedSetlist drops songs no longer in the
catalog with a toast.
@silverbucket silverbucket self-assigned this May 18, 2026
Setlist entries are now { songId, performance } — fat-shape songs
embedded in fixtures broke every test that opens the modal or checks
song count. Two fixes:

- roll-survives-sync: setlistIds reads s.songId instead of s.id
- saved: setlistFixture songs converted to lean shape; fixtureCatalogSongs /
  tallCatalogSongs helpers seed the catalog so hydrateSetlist can resolve
  them; anxiety assertion updated (score is now computed live, not stored)
…aved modal

confirmOptimizeOrder() was reading generatedSetlist.songs (lean shape)
for cover/instrumental counts and fixedSongIds. All three fields are
undefined on lean entries, producing an all-undefined fixedSongIds array
and zero-capped covers/instrumentals. Switch to displayedSetlist.songs
which is already the hydrated form the generator expects.

SavedScreen viewingSet stored a hydrated snapshot on open, so a catalog
rename or RS sync after opening the modal would not be reflected until
close/reopen. Replace the $state object with a $state id + $derived
lookup against store.displayedSavedSetlists so the modal always shows
live catalog data.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

📝 Walkthrough

Walkthrough

This PR refactors setlist persistence to separate lean (songId + performance) from displayed (scored + hydrated) formats. A new v2 migration converts persisted setlists; hydration derives display-ready setlists from the live song catalog. Components now consume displayedSetlist/displayedSavedSetlists, and all persistence workflows migrate through the new schema.

Changes

Lean Setlist State Architecture

Layer / File(s) Summary
Setlist v2 migration schema
src/lib/migrations.js
Adds setlists v2 transformation: converts embedded fat songs to lean {songId, performance} objects, hoists minimumsRelaxed/openerFilterRelaxed/closerFilterRelaxed from summary to top-level fields, removes summary/songNames/songCount.
Hydration and derivation logic
src/lib/stores/app.svelte.js
Introduces songsById catalog lookup and hydrateSetlist() function; derives displayedSetlist and displayedSavedSetlists by overlaying lean setlist entries with live song data and rescoring via scoreFixedOrder().
Lean format conversion utilities
src/lib/stores/app.svelte.js
Adds leanFromGeneratorResult() to convert generator output to lean schema and normalizeLeanSetlist() for backward-compatible fat-to-lean conversion with flag hoisting.
Setlist state load, save, and mutations
src/lib/stores/app.svelte.js
Refactors loadCurrentSetlist, saveCurrentSetlist, loadSavedSetlist, and mutation helpers (add/remove/reorder songs) to operate on lean {songId, performance} entries while relying on derivation for display scoring.
Data import/export/sync integration
src/lib/stores/app.svelte.js
Removes syncSavedSongIntoSetlist helper; wires migrator.migrateDocument() into snapshot restore, reload, import/export, and localStorage migration; updates saveSong to rely on derivation auto-refresh.
RollScreen component UI updates
src/lib/components/roll/RollScreen.svelte
Switches from store.generatedSetlist to store.displayedSetlist for setlistSongIds, completion detection, anxiety computation, idle/result rendering, and song navigation.
SavedScreen component UI updates
src/lib/components/saved/SavedScreen.svelte
Refactors modal state to track viewingId (deriving viewingSet from store.displayedSavedSetlists); renders saved list from displayedSavedSetlists; uses songs.length instead of removed songCount field.
Unit test migration coverage
src/lib/stores/app.test.js
Replaces syncSavedSongIntoSetlist tests with v2 migration validation: confirms lean song conversion, flag hoisting, legacy field removal, and idempotency of re-migration.
E2E fixture and test updates
tests/e2e/saved.spec.ts, tests/e2e/roll-survives-sync.spec.ts
Refactors fixtures to use shared song catalog and catalog-referenced setlist entries; adds fixtureCatalogSongs() and tallCatalogSongs() helpers; updates all test seeds to match lean schema (songId instead of id field).

🎯 4 (Complex) | ⏱️ ~75 minutes

A rabbit hops through fields of schemas lean,
Where songs and scores now dance unseen—
Hydration glows from catalog seed,
No fat to trim, just what we need. 🐰✨
Migration hoists the flags on high,
And tests now bloom as UI transforms nearby.

🚥 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 'refactor(stores): lean setlist shape with reactive catalog overlay' clearly summarizes the main architectural change: converting setlists to a lean persisted format and adding reactive derived stores for display.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 refactor/lean-setlist-shape

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/e2e/saved.spec.ts (1)

462-462: ⚡ Quick win

Remove legacy songCount from the tall fixture payload.

Line 462 still injects songCount into a v2 lean fixture. Keeping this legacy field in default E2E data weakens schema-contract coverage and can hide accidental dependencies on removed fields.

Suggested diff
-        return setlistFixture({ id: "tall", name: "The Marathon", songs, songCount });
+        return setlistFixture({ id: "tall", name: "The Marathon", songs });
🤖 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/e2e/saved.spec.ts` at line 462, The tall fixture creation is still
passing the legacy songCount property into setlistFixture (the call with id
"tall" and name "The Marathon"); remove the songCount field from that payload so
the v2 lean fixture no longer includes the deprecated property (i.e., update the
setlistFixture invocation for the "tall" fixture to omit songCount).
🤖 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/lib/stores/app.svelte.js`:
- Around line 1382-1383: When persisting setlists, don't save
generatedSetlist.songs verbatim; instead prune out IDs that no longer exist in
the current catalog before cloning and persisting. Update the two save sites
that call clone(generatedSetlist.songs) / write the setlist to first filter
generatedSetlist.songs against the active catalog (e.g., catalog or
catalog.songs / catalogById) so only catalog-valid song references are included,
then clone the filtered array and persist that result.

---

Nitpick comments:
In `@tests/e2e/saved.spec.ts`:
- Line 462: The tall fixture creation is still passing the legacy songCount
property into setlistFixture (the call with id "tall" and name "The Marathon");
remove the songCount field from that payload so the v2 lean fixture no longer
includes the deprecated property (i.e., update the setlistFixture invocation for
the "tall" fixture to omit songCount).
🪄 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: 0d8979ba-52d8-48b0-a6c7-43143b6f7a8f

📥 Commits

Reviewing files that changed from the base of the PR and between 07e5fff and 5d001eb.

📒 Files selected for processing (7)
  • src/lib/components/roll/RollScreen.svelte
  • src/lib/components/saved/SavedScreen.svelte
  • src/lib/migrations.js
  • src/lib/stores/app.svelte.js
  • src/lib/stores/app.test.js
  • tests/e2e/roll-survives-sync.spec.ts
  • tests/e2e/saved.spec.ts

Comment on lines +1382 to 1383
songs: clone(generatedSetlist.songs),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist only catalog-valid song references when saving setlists.

At Line 1382 and Line 1414, saving uses generatedSetlist.songs directly. That can re-persist deleted-song IDs (they’re filtered in display hydration, not always pruned from generatedSetlist), so saved docs keep stale refs and users repeatedly hit dropped-song warnings on load.

Suggested fix
 async function saveCurrentSetlist() {
     if (!generatedSetlist) return;
     const currentSaved = savedSetlists || [];
+    const persistedSongs = (generatedSetlist.songs || [])
+        .filter((e) => songsById.has(e.songId))
+        .map((e) => ({ songId: e.songId, performance: e.performance || {} }));

     // If this setlist was loaded from a saved entry, update that entry in
     // place instead of creating a duplicate with a new id and name.
     if (loadedSavedId) {
         const existing = currentSaved.find((s) => s.id === loadedSavedId);
         if (existing) {
             await updateSavedSetlist(loadedSavedId, {
                 savedAt: nowIso(),
                 seed: generatedSetlist.seed,
                 minimumsRelaxed: !!generatedSetlist.minimumsRelaxed,
                 openerFilterRelaxed: !!generatedSetlist.openerFilterRelaxed,
                 closerFilterRelaxed: !!generatedSetlist.closerFilterRelaxed,
-                songs: clone(generatedSetlist.songs),
+                songs: persistedSongs,
             });
             setlistSaved = true;
             return;
         }
         // Saved entry no longer exists (deleted elsewhere) — fall through.
         loadedSavedId = "";
@@
         const entry = {
             id: uid("set"),
             name: randomName,
             savedAt: nowIso(),
             schemaVersion: 2,
             seed: generatedSetlist.seed,
             minimumsRelaxed: !!generatedSetlist.minimumsRelaxed,
             openerFilterRelaxed: !!generatedSetlist.openerFilterRelaxed,
             closerFilterRelaxed: !!generatedSetlist.closerFilterRelaxed,
-            songs: clone(generatedSetlist.songs),
+            songs: persistedSongs,
         };

Also applies to: 1414-1415

🤖 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 `@src/lib/stores/app.svelte.js` around lines 1382 - 1383, When persisting
setlists, don't save generatedSetlist.songs verbatim; instead prune out IDs that
no longer exist in the current catalog before cloning and persisting. Update the
two save sites that call clone(generatedSetlist.songs) / write the setlist to
first filter generatedSetlist.songs against the active catalog (e.g., catalog or
catalog.songs / catalogById) so only catalog-valid song references are included,
then clone the filtered array and persist that result.

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.

1 participant