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

@sweetmantech sweetmantech 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

cursor Bot 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

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Jul 8, 2026 1:46pm

Request Review

@coderabbitai

coderabbitai Bot 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-ai cubic-dev-ai 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.

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
Contributor Author

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 67 song_artists links vs 1 each for the three feature artists (Claud, triple j, Holly Humberstone).

Results matrix — documented vs actual

# Check Documented / expected Actual (live preview) Status
1 First claim: POST /api/catalogs {snapshot} 200; catalog created; songs_added = measurement count; canonical artist attached to roster 200; catalog 2255ff65…; songs_added: 67; account_artist_ids 17 → 18 rows, added row = canonical 7647f901…
2 No duplicate artist mint on claim Claim never creates artist accounts All 5 "Del Water Gap"-named accounts predate the claim (canonical + 4 known funnel dupes); 0 new rows
3 Re-claim idempotency (same snapshot) 200; same catalog returned; songs_added: 0; no duplicate roster row 200; same id 2255ff65…; songs_added: 0; roster unchanged at 18
4 Healing path (pre-fix claims repair on re-claim) Attach re-runs on the snapshot.catalog branch Deleted the account_artist_ids row → re-POST → same catalog + canonical link re-attached (canonical_relinked: 1, roster back to 18)
5 Hero's call: GET /api/catalogs/2255ff65…/measurements?artist_account_id=7647f901…&limit=1 Artist-scoped aggregate equals unscoped for a fully-linked catalog measured_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)
6 400 — non-UUID snapshot snapshot must be a valid UUID HTTP 400, {"missing_fields":["snapshot"],"error":"snapshot must be a valid UUID"}
7 401 — no credentials Exactly one of x-api-key / Authorization required HTTP 401, {"error":"Exactly one of x-api-key or Authorization must be provided"}
8 403 — snapshot owned by another account Snapshot belongs to a different account HTTP 403, exact message (probed with walkthrough-owned 9e6f9f2c…; rejected before any write)
9 404 — unknown snapshot id Snapshot not found HTTP 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

cursor Bot 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 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