Skip to content

feat(catalogs): claim-time canonical-artist roster attach + Spotify socials at ingest (chat#1850 P1) - #768

Merged
sweetmantech merged 2 commits into
mainfrom
feat/claim-canonical-artist-attach
Jul 8, 2026
Merged

feat(catalogs): claim-time canonical-artist roster attach + Spotify socials at ingest (chat#1850 P1)#768
sweetmantech merged 2 commits into
mainfrom
feat/claim-canonical-artist-attach

Conversation

@sweetmantech

@sweetmantechsweetmantech commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

What

The api half of chat#1850 P1 — two changes:

  1. Claim-time canonical-artist roster attach.POST /api/catalogs (snapshot path) now resolves the claimed songs' canonical artist through the songs graph — measured ISRCs → song_artists → the dominant artist account (verified deterministic on the repro: 67/67 links to one account) — and inserts it into the claiming account's account_artist_ids when absent. Runs on first claims (createSnapshotCatalog) AND idempotent re-claims (createCatalogHandler), so pre-fix claims heal on the next click. Best-effort: a failed attach never fails the claim. This replaces the marketing funnel's per-signup POST /api/artists as the roster source of truth.
  2. Spotify socials at ingest (forward fix). Capture enrichment (linkSongsToArtists, the api#684 path) has Spotify track.artists ids in hand when auto-creating canonical artist accounts, but attached no socials — making canonicals unfindable by Spotify id. It now attaches https://open.spotify.com/artist/{id} (case preserved — ids are case-sensitive base62; the legacy lowercasing corruption is a different write path and is not repeated here) via the same updateArtistSocials lib path PATCH /api/artists uses, skipping artists that already have a Spotify artist social.

New files (SRP, one exported function each): lib/supabase/song_artists/selectSongArtistsBySongs.ts (chunked batch select), lib/songs/getDominantSongArtist.ts (pure resolution, deterministic tie-break), lib/catalog/attachCanonicalArtistToAccount.ts, lib/songs/attachSpotifySocialsToArtists.ts.

Tests (TDD RED→GREEN)

  • 5 new test files + 3 new cases in the 2 existing catalog test files; all written first and confirmed failing.
  • Touched domains: 19 files / 110 tests green. Full suite: 718 files / 3,966 tests, all green.
  • pnpm exec tsc --noEmit: 200 errors — exactly the pre-existing baseline, none in touched files. pnpm lint clean.

Verification

  • DB layer verified live post-backfill (see the database sibling PR): repro catalog 4b934253… filtered by the canonical roster artist returns 67 songs / 557,513,905 streams via get_catalog_measurements_aggregate.
  • Pending (needs a deploy + authed funnel run): a brand-new funnel signup claiming a snapshot lands the canonical artist on its roster with zero duplicate artist accounts created.

Links

Merge order

  1. docs → 2. database → 3. api (this PR) → 4. marketing. All PRs base main.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved catalog creation and re-claim flows to automatically connect accounts with the most likely canonical artist when matching song data is available.
    • Added automatic Spotify profile linking for artists when artist details are resolved.
  • Bug Fixes

    • Better handles empty or incomplete song data, avoiding failures and keeping catalog processing resilient.
    • Added safer fallback behavior so enrichment steps continue even if one lookup or update fails.

…ocials at ingest (chat#1850 P1)
POST /api/catalogs now resolves the claimed snapshot's canonical artist
through the songs graph (measured ISRCs -> song_artists -> dominant
artist account) and links it to the claiming account's roster, on both
first claims and idempotent re-claims. This replaces the marketing
funnel's per-signup POST /api/artists, which minted a duplicate,
song-less roster artist per signup.
Forward fix: capture enrichment (linkSongsToArtists) now attaches the
Spotify artist profile URL (case preserved) to resolved artist accounts
that lack one, so canonicals are findable by Spotify id going forward.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursorBot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercelBot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
apiReadyReadyPreviewJul 8, 2026 1:46pm

Request Review

@coderabbitai

coderabbitaiBot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d343ba96-8489-4b89-ba21-935f46987818

📥 Commits

Reviewing files that changed from the base of the PR and between ed5ff6c and bac620c.

⛔ Files ignored due to path filters (7)
  • lib/catalog/__tests__/attachCanonicalArtistToAccount.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/catalog/__tests__/createCatalogHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/catalog/__tests__/createSnapshotCatalog.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/songs/__tests__/attachSpotifySocialsToArtists.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/songs/__tests__/getDominantSongArtist.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/songs/__tests__/linkSongsToArtists.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/supabase/song_artists/__tests__/selectSongArtistsBySongs.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (7)
  • lib/catalog/attachCanonicalArtistToAccount.ts
  • lib/catalog/createCatalogHandler.ts
  • lib/catalog/createSnapshotCatalog.ts
  • lib/songs/attachSpotifySocialsToArtists.ts
  • lib/songs/getDominantSongArtist.ts
  • lib/songs/linkSongsToArtists.ts
  • lib/supabase/song_artists/selectSongArtistsBySongs.ts

📝 Walkthrough

Walkthrough

Adds a canonical-artist attachment feature that links accounts to their dominant artist based on claimed song ISRCs, wired into catalog creation and idempotent snapshot re-claim paths. Also adds Spotify social profile enrichment for artists linked during song-to-artist linking.

Changes

Canonical Artist and Spotify Socials Enrichment

Layer / File(s)Summary
Dominant artist selection and song-artist lookup
lib/songs/getDominantSongArtist.ts, lib/supabase/song_artists/selectSongArtistsBySongs.ts
New helpers count artist occurrences to pick a dominant artist (with deterministic tie-break) and fetch song_artists rows in chunks of 200 by ISRC.
Attach canonical artist to account
lib/catalog/attachCanonicalArtistToAccount.ts
New best-effort function resolves the dominant artist from song-artist links and inserts an account↔artist association if missing, returning null on any failure.
Wire attachment into catalog creation
lib/catalog/createSnapshotCatalog.ts, lib/catalog/createCatalogHandler.ts
Catalog creation and the idempotent snapshot re-claim path now derive ISRCs and call the attachment helper, with updated docs/comments.
Spotify socials enrichment
lib/songs/attachSpotifySocialsToArtists.ts, lib/songs/linkSongsToArtists.ts
New function patches artist account socials with a Spotify profile URL when missing; linkSongsToArtists builds a normalized-name-to-Spotify-ID map and invokes it after linking songs.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
participant createCatalogHandler
participant createSnapshotCatalog
participant attachCanonicalArtistToAccount
participant selectSongArtistsBySongs
participant getDominantSongArtist
createCatalogHandler->>createSnapshotCatalog: create catalog with ISRCs
createSnapshotCatalog->>attachCanonicalArtistToAccount: accountId, isrcs
attachCanonicalArtistToAccount->>selectSongArtistsBySongs: fetch song-artist links
attachCanonicalArtistToAccount->>getDominantSongArtist: determine dominant artist
attachCanonicalArtistToAccount->>attachCanonicalArtistToAccount: insert association if absent
Note over createCatalogHandler: Idempotent re-claim path
createCatalogHandler->>createCatalogHandler: select snapshot measurements, dedupe ISRCs
createCatalogHandler->>attachCanonicalArtistToAccount: accountId, isrcs
Loading

Poem

A rabbit hopped through songs and ISRCs bright,
Found the dominant artist, claimed it just right 🐰
Spotify links now sparkle on each roster face,
Best-effort tries, no throw, just grace,
One clean commit, SOLID and light.

✨ 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/claim-canonical-artist-attach

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.

@cubic-dev-aicubic-dev-aiBot 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.

1 issue found across 14 files

Confidence score: 3/5

  • In lib/supabase/song_artists/selectSongArtistsBySongs.ts, the chunked select("*") calls are unpaginated, so larger song batches can hit the Supabase/PostgREST per-request row cap and silently drop song_artists rows; merging as-is risks incomplete artist mappings in user-facing results — add explicit pagination (or reduce chunk/query shape) and verify full-row retrieval before merging.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/supabase/song_artists/selectSongArtistsBySongs.ts">
<violation number="1" location="lib/supabase/song_artists/selectSongArtistsBySongs.ts:20">
P2: Each chunked `select("*")` on `song_artists` is unpaginated. Supabase/PostgREST caps rows per request (commonly 1000), and exceeding that limit silently truncates the result without raising `error`. If a 200-ISRC chunk maps to more rows than the cap — plausible for collaborative catalogs — `getDominantSongArtist` will count an incomplete graph and may attach the wrong canonical artist. Consider bounding the chunk size by expected `song_artists` cardinality, adding `.range()` pagination within each chunk, or moving the resolution to a single SQL query/RPC/CTE that returns only the dominant artist id.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client as Client (UI / API)
participant CreateCat as createCatalogHandler
participant Cat as createSnapshotCatalog
participant AttachArtist as attachCanonicalArtistToAccount
participant SelectSongArt as selectSongArtistsBySongs
participant GetDom as getDominantSongArtist
participant SelAccArt as selectAccountArtistId
participant InsAccArt as insertAccountArtistId
participant SupaSong as song_artists (DB)
participant SupaAcc as account_artist_ids (DB)
participant LinkSongs as linkSongsToArtists
participant AttachSocials as attachSpotifySocialsToArtists
participant SelSoc as selectAccountSocials
participant UpdSoc as updateArtistSocials
participant SupaSoc as account_socials (DB)
Note over Client,UpdSoc: Claim-time canonical-artist roster attach
alt First claim via createSnapshotCatalog
CreateCat->>Cat: Create snapshot catalog with songs
Cat->>AttachArtist: attachCanonicalArtistToAccount(accountId, isrcs)
Note over AttachArtist: Best-effort: never throws
AttachArtist->>SelectSongArt: selectSongArtistsBySongs(isrcs)
SelectSongArt->>SupaSong: SELECT * FROM song_artists WHERE song IN (isrcs)
SupaSupaSong-->>SelectSongArt: song_artist rows
SelectSongArt-->>AttachArtist: artist links
AttachArtist->>GetDom: getDominantSongArtist(links)
Note over GetDom: Deterministic: most links wins, ties by lowest artist id
GetDom-->>AttachArtist: dominant artist id
alt Has dominant artist
AttachArtist->>SelAccArt: selectAccountArtistId(accountId, artistId)
SelAccArt->>SupaAcc: Check if link exists
SupaAcc-->>SelAccArt: existing or null
alt No existing link
AttachArtist->>InsAccArt: insertAccountArtistId(accountId, artistId)
InsAccArt->>SupaAcc: INSERT new roster link
SupaAcc-->>InsAccArt: success
end
AttachArtist-->>Cat: artist id
else No links
AttachArtist-->>Cat: null
end
Cat-->>CreateCat: catalog created with artist on roster
else Re-claim via createCatalogHandler
CreateCat->>CreateCat: Detect existing catalog (idempotent)
CreateCat->>CreateCat: selectSongMeasurements to get ISRCs
CreateCat->>AttachArtist: attachCanonicalArtistToAccount(accountId, isrcs)
Note over AttachArtist: Heals pre-fix claims on next click
AttachArtist->>SelectSongArt: selectSongArtistsBySongs(isrcs)
SelectSongArt->>SupaSong: chunked SELECT
SupaSupaSong-->>SelectSongArt: artist links
AttachArtist->>GetDom: getDominantSongArtist(links)
AttachArtist->>SelAccArt: Check existing
alt Not already linked
AttachArtist->>InsAccArt: Insert roster link
end
AttachArtist-->>CreateCat: artist id
CreateCat-->>Client: existing catalog + artist attached
end
Note over LinkSongs,UpdSoc: Spotify socials at ingest (forward fix)
LinkSongs->>LinkSongs: Enrich songs with Spotify artist names
LinkSongs->>LinkSongs: Collect spotifyArtistId by normalized name
LinkSongs->>AttachSocials: attachSpotifySocialsToArtists([{artistAccountId, spotifyArtistId}])
Note over AttachSocials: Best-effort per artist
loop For each unique artist account
AttachSocials->>SelSoc: selectAccountSocials(accountId)
SelSoc->>SupaSoc: Check existing socials
SupaSoc-->>SelSoc: social rows
alt No existing Spotify artist social
AttachSocials->>UpdSoc: updateArtistSocials(accountId, {SPOTIFY: url})
Note over UpdSoc: Case-preserved id (no lowercasing)
UpdSoc->>SupaSoc: INSERT/UPDATE social
else Already has Spotify artist social
AttachSocials->>AttachSocials: Skip (never clobber)
end
end
AttachSocials-->>LinkSongs: done
LinkSongs-->>LinkSongs: Continue with song-artist linking
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

const rows: Tables<"song_artists">[] = [];
for (let i = 0; i < songs.length; i += CHUNK_SIZE) {
const chunk = songs.slice(i, i + CHUNK_SIZE);
const { data, error } = await supabase.from("song_artists").select("*").in("song", chunk);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Each chunked select("*") on song_artists is unpaginated. Supabase/PostgREST caps rows per request (commonly 1000), and exceeding that limit silently truncates the result without raising error. If a 200-ISRC chunk maps to more rows than the cap — plausible for collaborative catalogs — getDominantSongArtist will count an incomplete graph and may attach the wrong canonical artist. Consider bounding the chunk size by expected song_artists cardinality, adding .range() pagination within each chunk, or moving the resolution to a single SQL query/RPC/CTE that returns only the dominant artist id.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/supabase/song_artists/selectSongArtistsBySongs.ts, line 20:
<comment>Each chunked `select("*")` on `song_artists` is unpaginated. Supabase/PostgREST caps rows per request (commonly 1000), and exceeding that limit silently truncates the result without raising `error`. If a 200-ISRC chunk maps to more rows than the cap — plausible for collaborative catalogs — `getDominantSongArtist` will count an incomplete graph and may attach the wrong canonical artist. Consider bounding the chunk size by expected `song_artists` cardinality, adding `.range()` pagination within each chunk, or moving the resolution to a single SQL query/RPC/CTE that returns only the dominant artist id.</comment>
<file context>
@@ -0,0 +1,31 @@
+ const rows: Tables<"song_artists">[] = [];
+ for (let i = 0; i < songs.length; i += CHUNK_SIZE) {
+ const chunk = songs.slice(i, i + CHUNK_SIZE);
+ const { data, error } = await supabase.from("song_artists").select("*").in("song", chunk);
+
+ if (error) {
</file context>

@sweetmantech

sweetmantech commented Jul 8, 2026

Copy link
Copy Markdown
ContributorAuthor

Preview verification — 2026-07-08, api-git-feat-claim-canonical-artist-attach-recoup.vercel.app

Auth: Privy test-app bearer (previews share the prod DB). Fixture: cloned this morning's Del Water Gap walkthrough snapshot (9e6f9f2c…, 67 measurements — chat#1859 walkthrough) to the caller's account as snapshot bd17eb11…. Songs-graph resolution for its ISRCs is deterministic: canonical Del Water Gap 7647f901… holds 67song_artists links vs 1 each for the three feature artists (Claud, triple j, Holly Humberstone).

Results matrix — documented vs actual

#CheckDocumented / expectedActual (live preview)Status
1First claim: POST /api/catalogs {snapshot}200; catalog created; songs_added = measurement count; canonical artist attached to roster200; catalog 2255ff65…; songs_added: 67; account_artist_ids 17 → 18 rows, added row = canonical 7647f901…
2No duplicate artist mint on claimClaim never creates artist accountsAll 5 "Del Water Gap"-named accounts predate the claim (canonical + 4 known funnel dupes); 0 new rows
3Re-claim idempotency (same snapshot)200; same catalog returned; songs_added: 0; no duplicate roster row200; same id 2255ff65…; songs_added: 0; roster unchanged at 18
4Healing path (pre-fix claims repair on re-claim)Attach re-runs on the snapshot.catalog branchDeleted the account_artist_ids row → re-POST → same catalog + canonical link re-attached (canonical_relinked: 1, roster back to 18)
5Hero's call: GET /api/catalogs/2255ff65…/measurements?artist_account_id=7647f901…&limit=1Artist-scoped aggregate equals unscoped for a fully-linked catalogmeasured_song_count: 67, total_streams: 557,734,241, valuation.mid: $2,058,990, artist_account_id echoed — identical to unscoped (prod repro this morning returned 0/$0 via the funnel-minted duplicate)
6400 — non-UUID snapshotsnapshot must be a valid UUIDHTTP 400, {"missing_fields":["snapshot"],"error":"snapshot must be a valid UUID"}
7401 — no credentialsExactly one of x-api-key / Authorization requiredHTTP 401, {"error":"Exactly one of x-api-key or Authorization must be provided"}
8403 — snapshot owned by another accountSnapshot belongs to a different accountHTTP 403, exact message (probed with walkthrough-owned 9e6f9f2c…; rejected before any write)
9404 — unknown snapshot idSnapshot not foundHTTP 404, exact message (00000000-0000-4000-8000-000000000000)

No secret/env value echoed in any response body.

Not exercised live

  • Spotify socials at ingest (attachSpotifySocialsToArtists): needs a real capture-enrichment run to observe; rests on this PR's unit tests, including the case-preservation assertions.
  • The full fresh-funnel signup (OTP → auto-run → claim) — that's the post-deploy prod walkthrough already planned on chat#1859.

Side observation (pre-existing, not this PR)

For the same Privy bearer, POST /api/accounts resolved account eb2ffb4a… (empty email) while the claim path (validateAuthContextgetOrCreateAccountIdByAuthToken, email-keyed) resolved fb678396… — two different accounts for one credential. Didn't affect these tests once the fixture was pointed at the email-keyed account, but it looks like a latent identity split worth a look outside this PR.

Cleanup

All fixture rows removed after the run (1 roster link, 2 catalogs, 2 account_catalogs, 67 catalog_songs, 67 song_measurements, 1 snapshot); the caller's roster verified back to its 17-artist baseline.

Verdict: docs ↔ API ↔ reality agree on every path tested — good to merge (then deploy before marketing#46).

🤖 Generated with Claude Code

@cursor

cursorBot commented Jul 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Sign up for freeto 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

@sweetmantech