Skip to content

fix(valuation): prefer an artist the account already rosters for a Spotify id (chat#1889 row 8 follow-up) - #792

Closed
sweetmantech wants to merge 1 commit into
mainfrom
fix/valuation-prefer-rostered-artist
Closed

fix(valuation): prefer an artist the account already rosters for a Spotify id (chat#1889 row 8 follow-up)#792
sweetmantech wants to merge 1 commit into
mainfrom
fix/valuation-prefer-rostered-artist

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Follow-up to api#791 — chat#1889 row 8. Found live by @sweetmantech during chat#1900's post-merge re-verification: the artist sidebar showed doubles again.

Why #791 wasn't enough

My "exactly 1 roster entry" assertion on the re-test raced the fire-and-forget valuation — it passed at +8s, and the valuation linked a second entry at ~+20s. Server truth after it completed:

0b91f38a…   ← the row the chat client created (POST /api/artists + PATCH social)
7647f901…   ← an old canonical, linked by the valuation's fallback

#791 stops the fallback minting a new row. But the chat flow creates its own artist row before the valuation runs — so when the global lookup resolves a different canonical for the same Spotify id, insertAccountArtistId links that one too. No new row minted, two roster entries anyway. My #791 verification called POST /api/valuation directly, with no client-created row in the picture, which is exactly why it passed.

What changed

findCanonicalArtistBySpotifyId(spotifyArtistId, accountId?) now collects every artist linked to the Spotify id and prefers one the requesting account already rosters — i.e. the row the client just created. Resolution stays global when the account rosters none of them (the cross-account reuse #791 verified is unchanged; accountId is used only for preference, never to scope the search).

Verification

TDD red → green:

# RED — global lookup resolves the old canonical over the account's own row
FAIL findCanonicalArtistBySpotifyId.test.ts > prefers an artist the requesting account already rosters

# GREEN — full api suite
Test Files  785 passed · Tests  4255 passed

The new preference test reproduces the live failure shape exactly (two socials, newest resolving to the client row, older to the old canonical, account rostering the client row). A second new test pins that global resolution survives when the account rosters nothing for the id. tsc at the 236 baseline; eslint clean.

Live verification after merge + test-sync: I'll re-run the full chat#1900 cold-start walk and this time assert the roster count after the valuation completes, not before — with a DB cross-check.

Tracked in chat#1889 (matrix row 8, reopening).


Summary by cubic

Fixes duplicate artist roster entries by making the Spotify canonical lookup prefer an artist the requesting account already rosters. Global reuse stays the same when the account has none.

  • Bug Fixes
    • Updated findCanonicalArtistBySpotifyId to collect all artists linked to the Spotify id and return one already rostered by the requesting account; otherwise falls back to the first global match.
    • linkSearchedArtistToAccount now passes accountId to the lookup to avoid linking a second, different artist.
    • Added tests to cover preference for the account’s artist and to ensure global resolution when the account rosters none.

Written for commit 18dbf01. Summary will update on new commits.

Review in cubic

…otify id

api#791 stopped the valuation minting a new artist row, but the chat add
flow creates its own row (+ Spotify social) BEFORE the fire-and-forget
valuation runs. When the global lookup then resolved a DIFFERENT
canonical, insertAccountArtistId linked that one too and the account
showed two roster entries again -- reproduced live on chat#1900's
preview after api#791 merged, caught by Sweets in the artist sidebar.

findCanonicalArtistBySpotifyId now collects every artist linked to the
Spotify id and prefers one the requesting account already rosters; the
global resolution is unchanged when the account rosters none of them.

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

vercel Bot commented Jul 29, 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 29, 2026 3:09am

Request Review

@cursor

cursor Bot commented Jul 29, 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.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@sweetmantech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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

How do review 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 refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 82b72f34-53e9-4f2a-80bc-44dab7bde885

📥 Commits

Reviewing files that changed from the base of the PR and between 0b222d1 and 18dbf01.

⛔ Files ignored due to path filters (1)
  • lib/valuation/__tests__/findCanonicalArtistBySpotifyId.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • lib/valuation/findCanonicalArtistBySpotifyId.ts
  • lib/valuation/linkSearchedArtistToAccount.ts

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 3 files

Confidence score: 4/5

  • In lib/valuation/findCanonicalArtistBySpotifyId.ts, loading the full account roster just to check candidate membership introduces O(n) behavior as rosters grow, which can slow valuation requests and increase DB/load pressure for larger accounts — switch to a targeted account_artist_ids lookup filtered by both account and linked artist to keep this path constant-time.
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/valuation/findCanonicalArtistBySpotifyId.ts">

<violation number="1" location="lib/valuation/findCanonicalArtistBySpotifyId.ts:44">
P2: Valuation now loads the requesting account's entire roster just to test candidate membership, making this path grow linearly with roster size. A targeted `account_artist_ids` query filtered by both account and linked artist IDs would avoid repeated full-roster reads.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client as Client (Chat UI)
    participant ChatAPI as POST /api/artists
    participant Valuation as Valuation Job (fire-and-forget)
    participant linkArtist as linkSearchedArtistToAccount
    participant findCanonical as findCanonicalArtistBySpotifyId
    participant DB as Supabase DB
    
    Note over Client,DB: Before Fix – Duplicate Roster Entries
    
    Client->>ChatAPI: Create artist row + Spotify social
    ChatAPI->>DB: Insert artist, insert social, link via account_artist_ids
    ChatAPI-->>Client: Row created
    
    Note over Valuation: Runs ~20s later (async)
    Valuation->>linkArtist: linkSearchedArtistToAccount(accountId, spotifyId)
    linkArtist->>findCanonical: findCanonicalArtistBySpotifyId(spotifyId) <br/> (no accountId passed)
    findCanonical->>DB: selectSocials(spotifyId)
    DB-->>findCanonical: two socials (new + old)
    findCanonical->>DB: selectAccountSocials(newSocialId)
    DB-->>findCanonical: account_id of new artist
    findCanonical->>DB: selectAccountSocials(oldSocialId)
    DB-->>findCanonical: account_id of old canonical
    Note over findCanonical: Returns first found (old canonical)
    findCanonical-->>linkArtist: old-canonical-id
    linkArtist->>DB: insertAccountArtistId(accountId, old-canonical-id)
    Note over DB: Now account has two roster entries
    
    Note over Client,DB: After Fix – Prefer Already-Rostered Artist
    
    Client->>ChatAPI: Create artist row + Spotify social (same)
    ChatAPI->>DB: Insert artist, insert social, link via account_artist_ids
    ChatAPI-->>Client: Row created
    
    Valuation->>linkArtist: linkSearchedArtistToAccount(accountId, spotifyId)
    linkArtist->>findCanonical: CHANGED: findCanonicalArtistBySpotifyId(spotifyId, accountId)
    findCanonical->>DB: selectSocials(spotifyId)
    DB-->>findCanonical: two socials (new + old)
    findCanonical->>DB: selectAccountSocials(newSocialId)
    DB-->>findCanonical: account_id of new artist
    findCanonical->>DB: selectAccountSocials(oldSocialId)
    DB-->>findCanonical: account_id of old canonical
    findCanonical->>DB: NEW: selectAccountArtistIds([accountId])
    DB-->>findCanonical: array of already-rostered artist ids
    alt accountId provided AND account already rosters one of the linked artists
        Note over findCanonical: NEW: Prefer that artist (client-created row)
        findCanonical-->>linkArtist: client-created artist id
    else account rosters none of them (global reuse case)
        Note over findCanonical: Fallback to first global (as before)
        findCanonical-->>linkArtist: old-canonical-id
    end
    linkArtist->>DB: insertAccountArtistId (only if not already rostered)
    Note over DB: Single roster entry preserved
Loading

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

Re-trigger cubic


return null;
if (accountId) {
const rostered = await selectAccountArtistIds([accountId]);

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: Valuation now loads the requesting account's entire roster just to test candidate membership, making this path grow linearly with roster size. A targeted account_artist_ids query filtered by both account and linked artist IDs would avoid repeated full-roster reads.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/valuation/findCanonicalArtistBySpotifyId.ts, line 44:

<comment>Valuation now loads the requesting account's entire roster just to test candidate membership, making this path grow linearly with roster size. A targeted `account_artist_ids` query filtered by both account and linked artist IDs would avoid repeated full-roster reads.</comment>

<file context>
@@ -1,36 +1,53 @@
 
-    return null;
+    if (accountId) {
+      const rostered = await selectAccountArtistIds([accountId]);
+      const rosteredIds = new Set(rostered.map(row => row.artist_id));
+      const alreadyRostered = linkedArtistIds.find(id => rosteredIds.has(id));
</file context>

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Closing unmerged — superseded by the creation-side fix. Decision (Sweets, 2026-07-29): a canonical lookup must not need account context; the ambiguity this patch navigated exists because the chat add path still creates a row per signup. The real slice: (1) POST /api/artists accepts spotify_artist_id and resolves-or-creates the canonical server-side, (2) a data migration relinks existing duplicates to the oldest canonical, (3) findCanonicalArtistBySpotifyId reverts to account-free with a deterministic oldest-first pick. Tracked as the top three rows on chat#1889.

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