Skip to content

feat(enrichment): write the master track store at request time — enrich once, reuse everywhere (#541) - #550

Merged
thewrz merged 14 commits into
mainfrom
feat/541-request-time-enrichment
Jun 24, 2026
Merged

feat(enrichment): write the master track store at request time — enrich once, reuse everywhere (#541)#550
thewrz merged 14 commits into
mainfrom
feat/541-request-time-enrichment

Conversation

@thewrz

@thewrz thewrz commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #549 (auto-closed when its stacked base branch was deleted by the #548 merge). Same branch, now rebased onto main; PR1's foundation (#548) is already merged.

Why

enrich_request_metadata wrote genre/bpm/key onto each Request row, so the same recording requested at two events was enriched twice, and energy/duration were never enriched at request time. #540 shipped the master tracks store; #548 added the writer foundation. This wires enrichment to populate the global store once per unique recording and reuse it — the next request for the same song costs zero provider calls.

What

  • Cache-aside dual-write in enrich_request_metadata: dedupe_signatureget_track(); if the store row is complete and trusted, copy fields onto the Request and skip all providers (the dedupe win). Otherwise run the existing cascade, thread per-field provenance, and upsert_track() while still writing Request.genre/bpm/musical_key for the current UI.
  • ISRC captured (was discarded) so a request and a later pool-import of the same recording collapse to one row.
  • Soundcharts audio-features (energy/danceability/valence/…) behind the dark gate; bpm/key/genre stay with the existing cascade.
  • legacy provenance source (precedence 30) + idempotent backfill script (python -m app.scripts.backfill_tracks).
  • No schema migration (FK + read-repoint deferred to feat(setbuilder): pool reads global store + pool→builder contract & build coverage gate #542+).

Hardening from two adversarial review passes

Workflow review (3 fixed): pre-supplied metadata now reaches the store; store-upsert failure no longer poisons the Request commit (commit-first recovery); energy backfills onto a complete cached row.
Codex review (3 fixed): legacy-only rows are no longer authoritative cache hits (real providers upgrade them); Spotify ISRC capture decoupled from the Tidal token (feeds Soundcharts tokenless); already-complete search-result submissions now seed the store.

Testing

  • Headline regression: same song at two events → one enrichment, zero provider calls on the repeat
  • Legacy-not-authoritative, complete-submission-seeds-store, tokenless-Spotify-ISRC, provenance, dual-write, energy gate on/off, store-failure-preserves-request, backfill idempotency
  • Full backend suite: 3232 passed, coverage 89.26% (≥ 85% gate); ruff + bandit clean
  • CI green (now runs against main)

🤖 Co-authored by Claude Opus 4.8. Closes #541. Built via TDD + multi-agent + Codex adversarial review.

Summary by CodeRabbit

  • New Features
    • Added a one-time backfill to populate the master track store from legacy request metadata (with legacy provenance).
    • Added ISRC support across request/collect flows end-to-end (API schema, validation, database, and UI forwarding from search results).
    • Expanded enrichment with provenance-aware “trusted cache” reuse and master-store seeding for already-complete submissions.
  • Bug Fixes
    • Prevented master-store upserts from overwriting an existing recording when the signature matches but ISRC conflicts.
    • Improved enrichment reliability and background scheduling by always refreshing metadata via a safe background task.
  • Tests
    • Added/expanded coverage for backfill correctness, idempotency, per-row failure isolation, dual-write behavior, trusted cache rules, and fresh-task scheduling.

thewrz and others added 4 commits June 24, 2026 05:43
…ackfill (#541)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… time (#541)

Rewire enrich_request_metadata to populate the global tracks store once per
unique recording and reuse it: a complete store row (genre+bpm+musical_key)
short-circuits ALL providers and copies down, so the same song requested at a
second event costs zero API calls (the dedupe win).

Thread per-field provenance through a resolved accumulator so the store
dual-write carries accurate sources (beatport/tidal/musicbrainz) with no
post-hoc guessing; capture the ISRC that was previously discarded into the
track identity. Soundcharts audio-features (energy/danceability/...) write
behind the dark-by-default gate when an ISRC is in hand and the row lacks
energy; bpm/key/genre stay with the existing cascade to avoid equal-precedence
churn. The Request's genre/bpm/musical_key columns are still written for the
current UI, and the store upsert is wrapped so a store failure never regresses
the request's own commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#541)

Three review fixes to the cache-aside dual-write so the store dedupe win is
actually realized and the request commit is never regressed:

1. Pre-supplied request metadata now reaches the store. Requests legitimately
   arrive with partial metadata (RequestCreate accepts genre/bpm/musical_key;
   the frontend submits search-result fields), but _apply_enrichment_result only
   recorded a field into the store payload when the Request was MISSING it — so a
   request that already carried a field wrote an incomplete store row that could
   never satisfy the cache-aside gate (genre AND bpm AND musical_key) and re-hit
   every provider forever. Seed each core field the Request holds but the
   accumulator lacks, attributed to the lowest 'legacy' provenance so real later
   enrichment cleanly overrides it.

2. Energy now backfills onto a trio-complete cached row on a repeat request. The
   short-circuit returned unconditionally on a complete-trio row, making the
   Soundcharts gate's cached.energy-is-None arm dead for exactly the rows it
   serves (contradicting spec §2/§4/§5.4/§7, incl. the dark-rollout-then-enable
   plan). When the gate is on and the cached row lacks energy, fall through to a
   Soundcharts-only backfill, still skipping the core cascade so the
   zero-extra-core-API-calls dedupe win is preserved.

3. A store upsert_track failure no longer poisons the Request commit. A flush-time
   DB error left the session in PendingRollback so the trailing db.commit() raised
   PendingRollbackError and the Request's freshly enriched bpm/genre/key were lost
   — the opposite of the "store is strictly additive, never regress the request
   commit" guarantee. _safe_upsert_track commits the Request enrichment first, then
   runs the store write under its own commit/rollback recovery.

Adds three regression tests (pre-supplied-field completeness + reuse, energy
backfill-on-repeat without re-running the cascade, store-failure preserves request
enrichment). Full gate green: ruff/bandit/pytest, coverage 89.20% (>=85%).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)

Three functional gaps in the new cache-aside behavior:

- Legacy rows were treated as authoritative cache hits: a complete trio sourced
  only from 'legacy' (backfill/seed) short-circuited forever, so real providers
  could never upgrade it. _trio_trusted now requires a non-legacy trio; a legacy
  row falls through to the cascade and is upgraded, then cached.
- Spotify ISRC extraction was nested under the Tidal-token guard, starving the
  Soundcharts audio-features lookup for DJs without a Tidal token. ISRC is now
  captured independently (and when the Soundcharts gate is on).
- Already-complete submissions never seeded the store (early return + the guest
  submit path skipped enrichment for full-metadata requests), so search-result
  submissions never populated the store. _seed_complete_request seeds the trio
  as 'legacy' on the complete path, and the submit path now enqueues enrichment
  for complete requests too.

Regression tests added for all three; pre-existing presupplied test split to
match the corrected legacy-upgrade behavior. Full gate green, 89.26%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@thewrz, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 33 minutes and 28 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d27145d-8098-4179-8ada-83c6b030fc84

📥 Commits

Reviewing files that changed from the base of the PR and between 6aa5d57 and 12ac998.

📒 Files selected for processing (2)
  • server/app/services/tracks/store.py
  • server/tests/test_track_store.py
📝 Walkthrough

Walkthrough

The PR adds ISRC support to request submission and storage, switches enrichment background work to fresh sessions, updates enrichment to write and reuse shared track-store records with provenance rules, and adds a legacy backfill script.

Changes

ISRC request plumbing and track-store enrichment

Layer / File(s) Summary
ISRC request payload plumbing
server/app/..., dashboard/app/..., dashboard/lib/..., server/openapi.json
Request schemas, models, APIs, clients, validation, and generated contracts carry isrc through request creation and collect submission.
Fresh-session background task wiring
server/app/api/..., server/app/services/sync/orchestrator.py, server/tests/test_bg_fresh_sessions.py, server/tests/test_collect_public.py
Background enrichment helpers are centralized in the orchestrator, and request submission paths enqueue fresh-session tasks.
Provenance-aware enrichment and cache rules
server/app/services/tracks/provenance.py, server/app/services/sync/enrichment_pipeline.py, server/app/services/tracks/store.py, server/tests/test_track_store.py, server/tests/test_enrichment_store_writes.py
Legacy precedence, cache-authoritative checks, dual-write enrichment, BPM context handling, ISRC conflict handling, and enrichment-store tests are updated together.
Legacy track backfill script
server/app/scripts/backfill_tracks.py, server/tests/test_backfill_tracks.py
A one-shot script streams eligible requests, upserts track rows with legacy provenance, isolates per-row failures, and is covered by backfill tests.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related issues

Possibly related PRs

  • wrzonance/WrzDJ#519: Closely related because it changes how background enrichment work is scheduled without carrying request-scoped sessions.
  • wrzonance/WrzDJ#547: Closely related because it extends the tracks-store provenance and upsert behavior used here.
  • wrzonance/WrzDJ#548: Closely related because it touches the same tracks-store and provider-enrichment path.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.34% 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: request-time enrichment now writes to the shared track store and reuses it on later requests.
Linked Issues check ✅ Passed The PR implements request-time global-track upserts, cache-aside reuse, ISRC-based identity, and request-level denormalized copies as required by #541.
Out of Scope Changes check ✅ Passed The changes stay focused on request-time track caching and supporting ISRC/provenance/backfill work; no unrelated feature appears in the diff.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/541-request-time-enrichment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

🤖 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 `@server/app/scripts/backfill_tracks.py`:
- Around line 47-56: The backfill in backfill_tracks.py currently calls Request
query .all(), which loads every matching row into memory at once. Update the
existing request-fetching logic in the backfill flow to use batched iteration or
streaming with the Request query instead of materializing the full result set,
and keep the processing loop working over chunks so memory usage stays bounded
as the table grows.
- Around line 61-93: Wrap each request in a per-row savepoint inside
backfill_tracks() so one failing upsert does not poison the shared Session. Use
db.begin_nested() around the values/signature/upsert_track path, and keep the
existing exception logging for request.id; if you choose not to use a nested
transaction, you must explicitly rollback after a failed flush before
continuing.

In `@server/app/services/sync/enrichment_pipeline.py`:
- Line 402: Move ISRC/source extraction in the enrichment flow before the cache
lookup so `get_track` can use the resolved Spotify ISRC as well as the
signature. In `enrichment_pipeline.py`, update the logic around the `get_track`
call to resolve `resolved_isrc` first, then query with `get_track(db,
isrc=resolved_isrc, signature=sig)` instead of signature-only. Make the same
ordering change in the related block around `get_track` usage so existing rows
matched by ISRC are found before providers are re-run.
- Around line 330-342: The _trio_trusted helper currently treats any
non-"legacy" source as trusted, which incorrectly includes missing provenance
and "unknown" entries. Update the trust check in _trio_trusted so only explicit
real-provider sources are considered authoritative for genre, bpm, and
musical_key, and make sure rows without provenance do not short-circuit the
cache-aside path.
- Around line 354-362: The seeding path in
enrichment_pipeline._safe_upsert_track currently writes request.musical_key
directly for already-complete requests, which can cache a non-normalized value
before normalize_key() runs. Normalize the complete-request musical_key before
building values/sources in this block (using the same key-normalization logic as
the later normalization path) so cached track data is stored in Camelot form and
reused consistently for future requests.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c703debd-d254-44fe-95cc-6d1f82f78e2c

📥 Commits

Reviewing files that changed from the base of the PR and between fc8748d and dc22ede.

📒 Files selected for processing (7)
  • server/app/api/events.py
  • server/app/scripts/backfill_tracks.py
  • server/app/services/sync/enrichment_pipeline.py
  • server/app/services/tracks/provenance.py
  • server/tests/test_backfill_tracks.py
  • server/tests/test_enrichment_store_writes.py
  • server/tests/test_track_store.py

Comment thread server/app/scripts/backfill_tracks.py
Comment thread server/app/scripts/backfill_tracks.py
Comment thread server/app/services/sync/enrichment_pipeline.py Outdated
Comment thread server/app/services/sync/enrichment_pipeline.py Outdated
Comment thread server/app/services/sync/enrichment_pipeline.py Outdated
…ime store)

Second-order issues surfaced by CodeRabbit and Codex on the rebased PR:

- Cache trust: _trio_trusted now requires real-provider provenance (precedence
  >= 50) via is_cache_authoritative, not merely source != 'legacy'. Missing/
  unknown/low-trust (community/llm) trios no longer short-circuit provider
  upgrades.
- Complete-request path now runs the Soundcharts energy backfill (gate on, row
  lacks energy) instead of returning before it.
- Spotify ISRC is fetched only when a consumer exists (Tidal exact-match needs a
  token; Soundcharts needs the gate) — no wasted external call otherwise.
- _seed_complete_request normalizes the key to Camelot before seeding.
- backfill_tracks streams rows (yield_per) and wraps each row in a SAVEPOINT so a
  poisoned-session error on one row no longer aborts the whole run.

Declined CodeRabbit's 'resolve ISRC before the cache lookup': it would add a
Spotify API call to every request before the signature cache check, defeating the
zero-call dedupe win, to catch a rare different-signature/same-ISRC case that
upsert_track's ISRC backfill already heals on the next miss.

Regression tests added for each fix. Full gate green, 3237 passed, 89.20%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewrz

thewrz commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

🔍 Codex (GPT-5.5, xhigh) round-2 on #550 — 3 confirmed [P2]s, all fixed in 22c6067 with regression tests:

  1. Energy backfill skipped on the complete-request path — the seed-and-return branch ran before the cached.energy is None backfill. The complete path now runs the Soundcharts energy backfill too (gate on), without the core cascade. (test_complete_request_backfills_energy_when_gate_on)
  2. Backfill script poisoned the session on a bad row — overlaps CodeRabbit's savepoint Major; fixed via per-row begin_nested(). (test_backfill_isolates_a_failing_row)
  3. Unused Spotify ISRC lookups — the ISRC is now fetched only when a consumer exists (Tidal exact-match token, or Soundcharts gate). (test_no_spotify_isrc_fetch_when_no_consumer)
    Full gate green: 3237 passed, 89.20%.

@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 `@server/tests/test_backfill_tracks.py`:
- Around line 211-212: The test in test_backfill_tracks.py only asserts that no
row exists for bad_sig, but it should also directly verify the rolled-back
poison insert with a NULL-signature check. Update the backfill regression test
near the existing assert on Track.query so it includes an explicit assertion for
Track.signature IS NULL, using the same Track model and db.query path, so the
test fails if a NULL-signature row leaks.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 10999f56-1182-4a54-b25f-01ee91a9dd53

📥 Commits

Reviewing files that changed from the base of the PR and between dc22ede and 22c6067.

📒 Files selected for processing (5)
  • server/app/scripts/backfill_tracks.py
  • server/app/services/sync/enrichment_pipeline.py
  • server/app/services/tracks/provenance.py
  • server/tests/test_backfill_tracks.py
  • server/tests/test_enrichment_store_writes.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/app/scripts/backfill_tracks.py
  • server/app/services/sync/enrichment_pipeline.py

Comment thread server/tests/test_backfill_tracks.py Outdated
…ackfill test (#541)

Codex convergence finding (P2): the cache-aside fast path returned before the
event-context BPM correction, so a trusted cached BPM (e.g. 66) was served raw
into a 128-132 BPM event instead of being half/double-time corrected like the
miss path does. Extracted the correction into _apply_bpm_context_correction and
call it on BOTH paths — the canonical store value is left unchanged (correction
is per-event, request-only).

Also: backfill bad-row test now asserts the NULL-signature poison row was rolled
back directly (IS NULL), per CodeRabbit — matching the test's stated intent.

Regression test test_cache_hit_applies_bpm_context_correction added. Full gate
green, 3238 passed, 89.21%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewrz

thewrz commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

🔍 Codex (GPT-5.5, xhigh) round-3 (convergence) on #550 — 1 confirmed [P2], fixed in a0788db: the cache-aside fast path returned before the event-context BPM correction, so a trusted cached BPM (e.g. 66) was served raw into a 128–132 BPM event instead of being half/double-time corrected like the miss path. Extracted _apply_bpm_context_correction and applied it on both paths; the canonical store value is unchanged (correction is per-event). Pinned by test_cache_hit_applies_bpm_context_correction. Full gate green: 3238 passed, 89.21%.

…#505)

Codex finding (P2): broadening the submit enqueue to complete submissions (#541)
scheduled enrich_request_metadata with the request-scoped `db`, which — with
Soundcharts enabled — pins a pool connection through Spotify/Soundcharts network
calls. Switch to the existing _enrich_with_fresh_session(id) helper so the task
runs in its own session and releases the connection promptly (the #505 pattern;
also closes the pre-existing submit-path gap). Pinned by
test_submit_request_schedules_fresh_session_not_db.

Full gate green, 3239 passed, 89.21%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewrz

thewrz commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

🔍 Codex round-4 (verification) on #550 — 1 confirmed [P2], fixed in 0100e5c: broadening the submit enqueue to complete submissions (#541) scheduled enrichment with the request-scoped db, pinning a pool connection through Spotify/Soundcharts network calls. Switched to the existing _enrich_with_fresh_session(id) helper (the #505 fresh-session pattern; also closes the pre-existing submit-path gap). Pinned by test_submit_request_schedules_fresh_session_not_db. Full gate green: 3239 passed, 89.21%.

thewrz and others added 2 commits June 24, 2026 08:18
…-BPM store (#541)

From the search→queue consistency audit + final Codex pass:

- Codex P2: the global store now keeps the CANONICAL provider BPM. Per-event
  half/double-time correction applies to the Request only (never written to the
  tracks row), so a cache hit at another event re-derives its own correction
  instead of inheriting this event's tempo.
- MEDIUM-1: collect submit now enqueues enrichment UNCONDITIONALLY (matches
  submit_request) — the divergent completeness guard was the pre-#541 pattern,
  inert only because CollectSubmitRequest lacks trio fields; a latent silent-skip
  the moment it gains them.
- MEDIUM-2 (structural): extracted the verbatim-triplicated _enrich_with_fresh_session
  into a single shared helper in services/sync/orchestrator.py; events/collect/
  requests all import it. Now a new entry point physically cannot drift — importing
  the one helper gets the master-store write + fresh-session hygiene for free.
- Refreshed the stale enrichment_pipeline docstrings to document the #541 master-
  store dual-write / cache-aside / energy-backfill (they only described the old
  genre/BPM/key cascade).

Tests: canonical-BPM-in-store pin, submit/refresh-metadata/enrich-all fresh-session
scheduling, helper-unit-tests + collect test repointed to the shared helper. Full
gate green, 3242 passed, 89.25%. MEDIUM-3 (duplicate recommendation matcher) filed
as #551.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…store write (#541)

Codex P2: the canonical-BPM-in-store rule was enforced on the provider-resolved
path but NOT the pre-supplied legacy-seed path. A partial request arriving with a
pre-supplied BPM has it event-context-corrected (request.bpm 66->132) before the
legacy-seed loop runs; since that BPM isn't in , the seed wrote the
event-corrected value to the global tracks row. Stash the pre-correction value
(canonical_bpm) and seed THAT. Regression test added.

Declining Codex's companion 're-check cache by ISRC before the cascade' finding:
its proper resolution is capturing the search-result ISRC at submit (it's already
returned by Tidal/Beatport search) — tracked as a follow-up rather than adding a
redundant pre-cascade lookup. Data already converges via upsert_track's ISRC
backfill, so it is an efficiency gap, not corruption.

Full gate green, 3243 passed, 89.25%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewrz

thewrz commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

🔍 Codex r6 (final verification) — 2 [P2]s, both the ISRC-first theme already deferred to #552 (no new #550-scope issue):

  1. Resolve Spotify ISRC before seeding complete requests — rare duplicate-row risk for credit-variant resubmissions of an already-stored recording.
  2. Re-check cache by ISRC after resolving it (incomplete path) — the efficiency gap from r5.

Disposition: declining both for #550, deferred to #552 ("ISRC-first from submit"). The proper fix is capturing the search-result ISRC at submit (Tidal/Beatport/Spotify search already return it) and going ISRC-first on both the cache lookup AND _seed_complete_request. Adding a pre-cascade/pre-seed Spotify lookup inside #550 would add an external call per request as a band-aid; #552 reuses the ISRC we already have. Data converges correctly today via upsert_track's ISRC backfill; the only residual is a rare duplicate row (correct data, both rows), fixed by #552.

This was the 6th Codex round; rounds 1–5 fixed all substantive #550-scope issues (transaction poisoning, dedupe-defeat, legacy-not-authoritative, energy-backfill paths, BPM canonical-store on both paths, fresh-session scheduling). r6 surfaced only the deferred theme → converged.

Capture the search-result ISRC at submit and use it as the master-store cache
key + identity, instead of re-deriving it during the background cascade. This
makes the store genuinely ISRC-first from the moment of submission and closes the
duplicate-row edge Codex flagged on #550 (a complete credit-variant submission
under a different normalized signature no longer inserts a second row).

Backend:
- migration 063 adds requests.isrc; Request.isrc model column.
- RequestCreate.isrc + CollectSubmitRequest.isrc; create_request and the collect
  submit store normalize_isrc(isrc).
- enrich_request_metadata: cache lookup is now get_track(isrc=request.isrc, sig),
  resolved_isrc seeds from request.isrc, and _seed_complete_request seeds by ISRC.
  Manual/typed requests (no ISRC) keep the signature fallback — purely additive.

Frontend:
- submitRequest + submitCollectRequest forward isrc; the four search->submit
  surfaces (DJ search modal, guest join, kiosk request modal, collect) pass the
  chosen result's isrc. DJ accept-recommended is unchanged (RecommendedTrack has
  no ISRC). Regenerated openapi.json + api-types.generated.ts.

Tested: backend ISRC-first cache hit across variant signature + no-duplicate
complete-submission collapse (3245 passed, 89.25%); frontend submitRequest /
submitCollectRequest forward-isrc unit tests (tsc + eslint + vitest 1393 green);
alembic check clean.

Closes #552.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewrz

thewrz commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

#552 folded in (ISRC-first from submit). Per maintainer call (zero residual edges before merge), the ISRC-at-submit slice is now part of this PR: requests.isrc (migration 063), RequestCreate/CollectSubmitRequest isrc, enrichment cache lookup + seed are ISRC-first, and the four search→submit frontends pass the chosen result's ISRC. This eliminates the rare duplicate-row edge Codex flagged (a complete credit-variant submission now collapses onto the existing ISRC row). Backend 3245 passed @ 89.25%; frontend tsc + eslint + vitest 1393 green; alembic check clean. This PR now Closes #541 AND #552.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/app/services/sync/enrichment_pipeline.py (1)

365-397: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve canonical BPM when reseeding already-complete requests.

A request that was previously served from cache can have request.bpm context-corrected and committed on the request row, then hit this early-return path on /refresh-metadata or bulk re-enrich. _seed_complete_request() will reseed the global track row from that event-specific BPM, which breaks the later invariant that only canonical BPM is stored in tracks.

Please carry a canonical BPM into this path as well (for example, reuse the existing track row’s BPM when one exists, or thread a pre-correction BPM into _seed_complete_request) and add a regression test covering refresh of a cache-hit request.

Also applies to: 465-477

🤖 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 `@server/app/services/sync/enrichment_pipeline.py` around lines 365 - 397, The
`_seed_complete_request` path is reseeding `tracks` with the request’s
event-specific BPM instead of the canonical track BPM, which can overwrite the
stored canonical value during refresh/re-enrich flows. Update
`_seed_complete_request` to prefer an existing canonical BPM from the track row
when available, or pass a pre-correction BPM into this helper before the request
row is committed, and keep the same treatment in the other reseed path that uses
this helper. Add a regression test around a cache-hit request refreshed through
`/refresh-metadata` or bulk re-enrich to verify the canonical BPM in `tracks` is
preserved.
🤖 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 `@server/tests/test_enrichment_store_writes.py`:
- Around line 917-957: The ISRC identity tests are coupled to the Soundcharts
feature flag and can become flaky when enrich_request_metadata() performs energy
backfill unexpectedly. In the affected ISRC-focused tests, stub get_settings()
so soundcharts_audio_features_enabled is pinned to the intended value before
calling enrich_request_metadata(), keeping the assertions deterministic and
isolated from external adapter behavior.

---

Outside diff comments:
In `@server/app/services/sync/enrichment_pipeline.py`:
- Around line 365-397: The `_seed_complete_request` path is reseeding `tracks`
with the request’s event-specific BPM instead of the canonical track BPM, which
can overwrite the stored canonical value during refresh/re-enrich flows. Update
`_seed_complete_request` to prefer an existing canonical BPM from the track row
when available, or pass a pre-correction BPM into this helper before the request
row is committed, and keep the same treatment in the other reseed path that uses
this helper. Add a regression test around a cache-hit request refreshed through
`/refresh-metadata` or bulk re-enrich to verify the canonical BPM in `tracks` is
preserved.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8fd55c2f-236a-49f6-a4b1-77fa1ddffa3b

📥 Commits

Reviewing files that changed from the base of the PR and between 22c6067 and 2188bfd.

📒 Files selected for processing (23)
  • dashboard/app/(dj)/events/[code]/components/DjSongSearchModal.tsx
  • dashboard/app/collect/[code]/page.tsx
  • dashboard/app/e/[code]/display/components/RequestModal.tsx
  • dashboard/app/join/[code]/page.tsx
  • dashboard/lib/__tests__/api.test.ts
  • dashboard/lib/__tests__/collect-api.test.ts
  • dashboard/lib/api-types.generated.ts
  • dashboard/lib/api.ts
  • server/alembic/versions/063_add_request_isrc.py
  • server/app/api/collect.py
  • server/app/api/events.py
  • server/app/api/requests.py
  • server/app/models/request.py
  • server/app/schemas/collect.py
  • server/app/schemas/request.py
  • server/app/services/request.py
  • server/app/services/sync/enrichment_pipeline.py
  • server/app/services/sync/orchestrator.py
  • server/openapi.json
  • server/tests/test_backfill_tracks.py
  • server/tests/test_bg_fresh_sessions.py
  • server/tests/test_collect_public.py
  • server/tests/test_enrichment_store_writes.py
✅ Files skipped from review due to trivial changes (3)
  • server/alembic/versions/063_add_request_isrc.py
  • server/app/schemas/request.py
  • dashboard/lib/api-types.generated.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/tests/test_backfill_tracks.py
  • server/app/api/events.py

Comment thread server/tests/test_enrichment_store_writes.py
…sts (#552)

Codex P2: the ISRC-first cache lookup get_track(isrc, sig) falls back to the
signature when the ISRC misses, so a request carrying ISRC_A could short-circuit
on a signature-matched row whose ISRC is a DIFFERENT recording (ISRC_B, same
artist/title, different release/remaster) — serving wrong metadata and skipping
providers. The fast path now only trusts a row whose ISRC is empty or matches the
request ISRC; a mismatch falls through to providers. An ISRC-less trusted row is
still used and gets the request ISRC backfilled (the ISRC lookup missed, so no
collision). Deeper note: signature is UNIQUE, so two distinct recordings sharing
a normalized artist/title still cannot both persist — that is a #540/#542 design
matter, not this fix.

CodeRabbit Major: pin the Soundcharts gate OFF in the two ISRC identity tests so
the cache-hit energy-backfill arm can't make a real API call if the env enables
the gate (config-coupled flakiness).

Regression tests: different-ISRC row not trusted (providers run); ISRC-less row
trusted + ISRC backfilled. Full gate green, 3247 passed, 89.26%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewrz

thewrz commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

🔍 Codex r7 (reviewed the full PR incl. #552) — 1 [P2], fixed in bc7f7ff: the ISRC-first cache lookup's signature fallback could serve a DIFFERENT recording's metadata when the request's ISRC missed but the signature matched a row with a different non-null ISRC (a different release/remaster of the same artist/title). The fast path now only trusts a row whose ISRC is empty or matches the request's; a mismatch falls through to providers, and an ISRC-less trusted row gets the request ISRC backfilled. (Deeper signature-uniqueness limitation — two distinct recordings with an identical normalized artist/title — is a #540/#542 design matter, noted, not in scope here.) Regression tests added; full gate 3247 @ 89.26%.

…ding (#552)

Codex P2 (store-level ISRC conflict): upsert_track's get_track(isrc, sig) falls
back to the signature when the ISRC misses, so a complete submission / provider
refresh carrying ISRC_B could land on the signature row of a DIFFERENT recording
(ISRC_A, same normalized artist/title) and overwrite A's metadata with B's, never
storing B. upsert_track now detects a non-null ISRC mismatch on a signature-
fallback match and returns the existing row UNCHANGED (logged), so the store can
never be corrupted by a different recording. The recording's data still lives on
its Request row. Full multi-recording-per-signature support (ISRC-primary identity
instead of unique signature) is a #542/#540 schema matter, noted, out of scope.

This complements the cache-READ guard (bc7f7ff); the WRITE guard in upsert_track
protects every store writer (seed, dual-write, energy backfill, backfill script).

Regression test: same signature + different ISRC does not clobber the existing
row. Full gate green, 3248 passed, 89.26%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewrz

thewrz commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

🔍 Codex r8 — 1 [P2], fixed in 4a13c32: the store-level counterpart of r7. upsert_track's signature fallback (when the ISRC misses) could overwrite a DIFFERENT recording's row (same normalized artist/title, different ISRC) — corrupting it and never storing the new recording. upsert_track now detects a non-null ISRC mismatch on a signature-fallback match and returns the existing row UNCHANGED (logged), so the store can't be corrupted; this protects EVERY writer (seed, dual-write, energy backfill, backfill script), not just the flagged path. The recording's data still lives on its Request. The residual — two distinct recordings can't share one signature row — is the signature-uniqueness identity model of #540 (now non-corrupting); ISRC-primary identity is tracked for #542. Regression test added; full gate 3248 @ 89.26%.

…ty (#552)

Codex P2: RequestCreate.isrc / CollectSubmitRequest.isrc accepted any <=15-char
string, but enrichment treats the stored value as authoritative recording identity
(cache key) and sends it to provider by-ISRC endpoints. A malformed value would
defeat the ISRC-first cache (no row matches) and drive invalid external calls.

Added valid_isrc(): normalize + validate the ISO 3901 shape
(^[A-Z]{2}[A-Z0-9]{3}[0-9]{7}$, 12 chars). Both submit schemas now drop a
malformed ISRC to None (lenient: an optional metadata field must not 422 the whole
request) so only a well-formed ISRC is ever stored or used as a key. Provider-
sourced ISRCs are trusted.

Tests: valid normalize, hyphenated normalize, malformed->None, absent->None, for
both RequestCreate and CollectSubmitRequest. Full gate green, 3254 passed, 89.27%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewrz

thewrz commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

🔍 Codex r9 — 1 [P2], fixed in 6aa5d57: the submitted ISRC (RequestCreate/CollectSubmitRequest) was unvalidated but used as authoritative identity (cache key) + sent to provider by-ISRC endpoints. Added valid_isrc() (normalize + ISO 3901 shape check); both submit schemas now drop a malformed ISRC to None (lenient — an optional field shouldn't 422 the request), so only well-formed ISRCs are stored or used as keys. Validation tests added; full gate 3254 @ 89.27%.

@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 `@server/app/services/tracks/store.py`:
- Around line 109-127: The ISRC conflict guard in upsert_track is only reachable
through the initial get_track path, so reconciled rows returned from
_insert_identity_reconciling can still fall through and overwrite a different
recording. Move the ISRC mismatch check out of the current elif chain into a
standalone if that runs after the insert/reconcile branch, using the existing
upsert_track, _insert_identity_reconciling, and get_track flow so any row
resolved by signature fallback is rejected before backfill/overwrite.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 61771f4f-5966-4b86-8751-76d080fb427c

📥 Commits

Reviewing files that changed from the base of the PR and between 2188bfd and 6aa5d57.

📒 Files selected for processing (8)
  • server/app/schemas/collect.py
  • server/app/schemas/request.py
  • server/app/services/sync/enrichment_pipeline.py
  • server/app/services/track_normalizer.py
  • server/app/services/tracks/store.py
  • server/tests/test_enrichment_store_writes.py
  • server/tests/test_input_validation.py
  • server/tests/test_track_store.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • server/app/schemas/collect.py
  • server/app/schemas/request.py
  • server/tests/test_enrichment_store_writes.py
  • server/app/services/sync/enrichment_pipeline.py

Comment thread server/app/services/tracks/store.py
…#552)

CodeRabbit Major: the r8 ISRC-conflict guard was an elif on the INITIAL get_track,
so it was bypassed when the conflict surfaced via the reconcile race — insert hits
the unique-signature constraint, _insert_identity_reconciling re-reads a different-
ISRC row, and that returns through the 'track is None' branch, skipping the elif.
Moved the guard to a standalone check AFTER resolution so it covers both the
initial signature fallback AND the reconcile re-read. A freshly inserted row has
isrc==norm_isrc so it never trips on the insert path.

Regression test: a reconcile-race landing on a different-ISRC row does not
overwrite it. Full gate green, 3255 passed, 89.27%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewrz
thewrz merged commit 2bd047c into main Jun 24, 2026
10 checks passed
@thewrz
thewrz deleted the feat/541-request-time-enrichment branch June 24, 2026 18:04
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.

feat(enrichment): write the global track store at request time (enrich once, reuse everywhere)

1 participant