Skip to content

fix(valuation): deterministic oldest-first canonical pick (chat#1889 row 10) - #794

Merged
sweetmantech merged 1 commit into
mainfrom
fix/canonical-pick-oldest-first
Jul 29, 2026
Merged

fix(valuation): deterministic oldest-first canonical pick (chat#1889 row 10)#794
sweetmantech merged 1 commit into
mainfrom
fix/canonical-pick-oldest-first

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Row 10 of the canonical-artist trilogy on chat#1889.

findCanonicalArtistBySpotifyId iterated socials newest-first, and enrichSearchedArtistProfile bumps socials.updated_at mid-flow — so the pick could flip between two lookups inside one add. Oldest-first is stable, and the oldest social is the true canonical (prod anchor: Ana Bárbara's 2025-05-30 row, shared by OneRPM org accounts + sweetmantech@gmail.com). No account context — per the 2026-07-29 decision that closed api#792.

TDD red→green: the new test stages a newest social resolving to a fresh duplicate and an older one resolving to the true canonical, and asserts the old one wins. 145 valuation+artists tests pass; tsc at the 236 baseline; eslint clean.

Sequencing: correctness is complete once database#49 (row 9) has collapsed existing duplicates — until then oldest-first is still a better pick than newest-first (stable + favors the true canonical) with no downside. Code-independent of api#793; no merge conflict (different files).

Tracked in chat#1889 (matrix row 10).

🤖 Generated with Claude Code


Summary by cubic

Make canonical artist selection deterministic by iterating socials oldest-first in findCanonicalArtistBySpotifyId to prevent flips when enrichSearchedArtistProfile updates socials.updated_at. Aligns with chat#1889 (row 10); no account context required.

  • Bug Fixes
    • Sort socials by updated_at ascending and traverse oldest-first to select the stable canonical.
    • Add a test where the newest social maps to a duplicate and the oldest maps to the canonical; asserts the oldest wins.

Written for commit c0f5efb. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved consistency when identifying canonical artists by checking linked social accounts in a stable, oldest-first order.
    • Preserved graceful handling when no linked account is found or an error occurs.

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

@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 4:45pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

findCanonicalArtistBySpotifyId now sorts retrieved socials by ascending updated_at before selecting the first linked account. Missing timestamps are treated as zero; existing error handling and null fallback behavior remain unchanged.

Changes

Canonical artist lookup

Layer / File(s) Summary
Order socials before selection
lib/valuation/findCanonicalArtistBySpotifyId.ts
Creates an oldest-first copy of the socials, sorts by updated_at with a zero fallback, and uses it to find the first linked account.

Estimated code review effort: 1 (Trivial) | ~3 minutes

Possibly related PRs

  • recoupable/api#791: Modifies the same canonical artist lookup and its linked-social selection.
  • recoupable/api#793: Uses the canonical artist selection logic affected by this ordering change.

Poem

Oldest links now lead the way,
In steady order, day by day.
Missing dates fall back to zero,
A calmer path for every hero.
The first true match comes into view.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Solid & Clean Code ✅ Passed The file keeps one primary export matching the filename, and the logic stays focused and straightforward with no added duplication or extra responsibilities.
✨ 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 fix/canonical-pick-oldest-first

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

Confidence score: 4/5

  • In lib/valuation/findCanonicalArtistBySpotifyId.ts, ties on equal updated_at are left to Supabase’s unspecified row order, so canonical artist selection can vary across runs and cause inconsistent valuation results; add a deterministic secondary sort key (for example id) to make tie-breaking stable.
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:31">
P2: Equal `updated_at` values are not deterministically resolved: this comparator returns `0`, preserving the unspecified order returned by Supabase. Adding a stable secondary key (such as `id`) would prevent the canonical pick from flipping when socials share the same timestamp.</violation>
</file>

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

Re-trigger cubic

// pick can flip between two lookups in the same add (chat#1889 row 10).
// The oldest social is the stable, true canonical.
const oldestFirst = [...socials].sort(
(a, b) => new Date(a.updated_at ?? 0).getTime() - new Date(b.updated_at ?? 0).getTime(),

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: Equal updated_at values are not deterministically resolved: this comparator returns 0, preserving the unspecified order returned by Supabase. Adding a stable secondary key (such as id) would prevent the canonical pick from flipping when socials share the same timestamp.

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

<comment>Equal `updated_at` values are not deterministically resolved: this comparator returns `0`, preserving the unspecified order returned by Supabase. Adding a stable secondary key (such as `id`) would prevent the canonical pick from flipping when socials share the same timestamp.</comment>

<file context>
@@ -24,7 +24,14 @@ export async function findCanonicalArtistBySpotifyId(
+    // pick can flip between two lookups in the same add (chat#1889 row 10).
+    // The oldest social is the stable, true canonical.
+    const oldestFirst = [...socials].sort(
+      (a, b) => new Date(a.updated_at ?? 0).getTime() - new Date(b.updated_at ?? 0).getTime(),
+    );
+
</file context>
Suggested change
(a, b) => new Date(a.updated_at ?? 0).getTime() - new Date(b.updated_at ?? 0).getTime(),
(a, b) =>
new Date(a.updated_at ?? 0).getTime() - new Date(b.updated_at ?? 0).getTime() ||
a.id.localeCompare(b.id),

…row 10)

findCanonicalArtistBySpotifyId iterated socials newest-first, and
enrichSearchedArtistProfile bumps updated_at mid-flow -- so the pick
could flip between the creation call and the valuation fallback within
one add. Oldest-first is stable, and the oldest social is the true
canonical (verified on prod: Ana Barbara's is the 2025-05-30 row that
OneRPM org accounts + sweetmantech@gmail.com already share). No account
context, per the 2026-07-29 decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sweetmantech
sweetmantech force-pushed the fix/canonical-pick-oldest-first branch from 475ec6b to c0f5efb Compare July 29, 2026 16:43
@sweetmantech

Copy link
Copy Markdown
Contributor Author

Rebased onto main (post-#793) as c0f5efbd — clean, 154 valuation+artists tests pass, tsc at the 236 baseline.

Status update after the #49/#50 data cleanups: prod currently has zero Spotify ids resolving to >1 artist, so this pick-order change has no behavioral effect on today's data. It stays worth merging as the code-level guarantee of that invariant: one live regrowth vector remains until chat#1900 lands (the onboarding form still client-mints a socialed row when adding an artist another account has), and if a duplicate reappears, newest-first picks the newest dupe — the exact failure mode that doubled rosters on 07-28 — while oldest-first picks the established canonical. One sort + one test; belt-and-suspenders by design.

@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: 2

🧹 Nitpick comments (1)
lib/valuation/findCanonicalArtistBySpotifyId.ts (1)

27-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the lookup function under 20 lines.

findCanonicalArtistBySpotifyId now spans 26 lines. Extract timestamp normalization and/or oldest-first ordering into a focused helper so the lookup remains small and single-purpose.

As per coding guidelines: “Flag functions longer than 20 lines” and “Keep functions small and focused.”

🤖 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 `@lib/valuation/findCanonicalArtistBySpotifyId.ts` around lines 27 - 34, Reduce
findCanonicalArtistBySpotifyId below 20 lines by extracting timestamp
normalization and/or the oldest-first social ordering into a focused helper.
Keep the lookup function responsible only for canonical artist lookup and
preserve the existing stable oldest-first ordering behavior.

Source: Coding guidelines

🤖 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 `@lib/valuation/findCanonicalArtistBySpotifyId.ts`:
- Around line 27-29: Update the ordering comment near
findCanonicalArtistBySpotifyId to describe the prior database-returned order as
unspecified, rather than claiming it was newest-first; retain the rationale that
selecting the oldest social record provides a stable canonical result.
- Around line 30-32: Update the oldestFirst sorting comparator in
findCanonicalArtistBySpotifyId to normalize malformed updated_at values to the
existing missing-timestamp fallback before subtraction. Ensure both compared
timestamps produce finite numeric values so sorting and oldest canonical social
selection remain deterministic.

---

Nitpick comments:
In `@lib/valuation/findCanonicalArtistBySpotifyId.ts`:
- Around line 27-34: Reduce findCanonicalArtistBySpotifyId below 20 lines by
extracting timestamp normalization and/or the oldest-first social ordering into
a focused helper. Keep the lookup function responsible only for canonical artist
lookup and preserve the existing stable oldest-first ordering behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 159026e1-bb30-49f6-bf53-1b5138dd85fa

📥 Commits

Reviewing files that changed from the base of the PR and between 42cfedf and c0f5efb.

⛔ 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 (1)
  • lib/valuation/findCanonicalArtistBySpotifyId.ts

Comment on lines +27 to +29
// Oldest first: enrichment bumps updated_at mid-flow, so a newest-first
// pick can flip between two lookups in the same add (chat#1889 row 10).
// The oldest social is the stable, true canonical.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify the ordering comment.

The previous implementation was not guaranteed to be newest-first; it used the database-returned order. Describe that order as unspecified rather than “newest-first” to keep the rationale accurate.

🤖 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 `@lib/valuation/findCanonicalArtistBySpotifyId.ts` around lines 27 - 29, Update
the ordering comment near findCanonicalArtistBySpotifyId to describe the prior
database-returned order as unspecified, rather than claiming it was
newest-first; retain the rationale that selecting the oldest social record
provides a stable canonical result.

Comment on lines +30 to +32
const oldestFirst = [...socials].sort(
(a, b) => new Date(a.updated_at ?? 0).getTime() - new Date(b.updated_at ?? 0).getTime(),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline =="
ast-grep outline lib/valuation/findCanonicalArtistBySpotifyId.ts --view expanded || true

echo "== file contents =="
cat -n lib/valuation/findCanonicalArtistBySpotifyId.ts

echo "== related test files =="
fd -a '.*canonical.*|.*findCanonical.*|spotify' . | sed 's#^\./##' | head -200

echo "== behavioral Date comparator probe =="
node - <<'JS'
function compare(a,b) {
  return new Date(a.updated_at ?? 0).getTime() - new Date(b.updated_at ?? 0).getTime();
}
const cases = [
  {updated_at: '2024-01-02T00:00:00Z'},
  {updated_at: '2024-01-03T00:00:00Z'},
  {updated_at: 'invalid'},
  {updated_at: '2024-01-05T00:00:00Z'},
];
for (let i = 1; i <= 10; i++) {
  const sorted = [...cases].sort(compare).map(c => c.updated_at);
  console.log(JSON.stringify(sorted));
}
console.log(new Date('invalid').getTime());
console.log(new Date('2024-01-03T00:00:00Z').getTime() - new Date('invalid').getTime());
JS

Repository: recoupable/api

Length of output: 3846


Normalize invalid timestamps before sorting.

new Date(value).getTime() returns NaN for malformed timestamps, and NaN - anything makes Array.sort’s comparator ineffective, so the oldest canonical social may not be selected reliably. Normalizing invalid values to the missing-timestamp fallback keeps canonical selection deterministic and consistent with the intended best-effort behavior.

🤖 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 `@lib/valuation/findCanonicalArtistBySpotifyId.ts` around lines 30 - 32, Update
the oldestFirst sorting comparator in findCanonicalArtistBySpotifyId to
normalize malformed updated_at values to the existing missing-timestamp fallback
before subtraction. Ensure both compared timestamps produce finite numeric
values so sorting and oldest canonical social selection remain deterministic.

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Deployment testing — A/B against a controlled fixture proves the pick flip

Prod now holds zero multi-candidate Spotify ids (post database#49/#50), so the pick order is unobservable on live data. The honest test was a controlled fixture: one synthetic Spotify id (zztestpickorder0000001), two artist rows, the "canonical" social backdated to 2025-01-01 and the "dupe" social stamped today — the exact shape a regrown duplicate takes.

Same POST /api/artists {name, spotify_artist_id} request, same data, two deployments:

Deployment Code Picked
test-recoup-api (pre-#794, newest-first) main …0002 "ZZ Pick Dupe New" — the newest dupe, i.e. the failure mode that doubled rosters on 07-28
api-5nzv425wk (this PR, sha confirmed c0f5efbd) #794 …0001 "ZZ Pick Canonical Old" — the established canonical

Both returned 200 with the artist linked; only the pick differed. That's the entire behavioral claim of this PR, demonstrated live.

Fixture fully cleaned up — residue query returns 0 across accounts/socials/links, and the global invariant re-verified after cleanup: still 0 Spotify ids resolving to >1 artist.

Two incidental findings while building the fixture, both good news: socials.profile_url now carries a UNIQUE constraint and the lowercasing trigger normalizes variants — so the case-variant duplicate socials rows that seeded this whole mess can't recur either; the surviving ambiguity vector is only ?si=-style query strings, which this PR's oldest-first pick now handles deterministically.

Ready to merge — closes the trilogy (rows 8–10 on chat#1889).

@sweetmantech
sweetmantech merged commit 3bb0ca2 into main Jul 29, 2026
6 checks passed
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