feat: manual canonical cleanup — targeted merges, name-keyed collapse, safe orphan deletion (chat#1889)

Merged
SW
3mo ago
+276-0

The manual cleanup pass agreed on chat#1889 (decisions Sweets, 2026-07-29). Follows #49; uses its persisted manifest.

What it does

  1. Stick Figure — igor@'s roster moves onto the 195-song graph owner (his weekly reports go from 1 measured song to 195).
  2. Mac Miller — everyone consolidated on Rostrum's 223-song row; the artist-rosters-artist edge deleted (3 more such edges exist globally, listed on the issue).
  3. Ed Sheeran / Disclosure split — the 0-song "Disclosure" row carried Ed Sheeran's Spotify id. The two accounts that added Ed Sheeran get a new true canonical (6eUKZXaKkcviH0Ku9w2n3V); the Disclosure row keeps patrick@methodmusic's roster (he added Disclosure — Method is Disclosure's management) and gains its real Spotify social (6nS5roXSAGhTGr34W6n7Et).
  4. Name-keyed collapse — rostered zero-song rows sharing a name fold onto the group's canonical (graph owner first), with an identity guard: a row whose Spotify id conflicts with the canonical's is never folded. 542 folds: Luh Tyler 77→1, sweetman-family 235→handful, Chillpill's rosters land on the 17-song graph owner.
  5. Orphan deletion — 663 fully-orphaned duplicate accounts deleted (socials/org/info/email rows included). Wallet-bearing and any still-referenced rows are excluded by guards.

Two hazards found by transactional dry-runs against prod (both handled)

  • Soft references: rooms, scheduled_actions, files, sessions, campaigns, artist_segments, funnel_analytics reference artist ids without FKs — 3,038 rooms + 4,573 memories + 30 scheduled weekly-report tasks on the spotify-orphans alone. All seven tables are repointed through a transitively-resolved canonical map (yesterday's collapse can target a canonical that today's fold moves again — Chillpill chains X→Y→Z) before any deletion, and the delete set carries NOT-EXISTS guards on every soft-ref table.
  • artist_segments timestamps trigger is broken in prod — it assigns new.created_at on a table with no such column, so every UPDATE errors. User triggers are disabled around the single repoint statement. Tracked as its own item on chat#1889.

Dry-run evidence (full body executed on prod inside BEGIN…ROLLBACK)

AssertionExpectedActual
Name folds—542
Accounts deleted—663
Rooms / tasks still dangling after repoint0 / 00 / 0
igor on the 195-song Stick Figure11
New Ed Sheeran rosters22
Luh Tyler rows left11
Unique-constraint violations00

Iterations that got here: v1 left 736 rooms dangling (chains — fixed with the recursive map); v2 hit the artist_segments trigger bug (worked around). Post-apply, zz_dupe_manifest_20260729 and zz_cleanup_manifest_20260729b both get dropped per the agreed protocol — a follow-up migration will do that after verification.

🤖 Generated with Claude Code


Summary by cubic

Manual canonical cleanup to merge duplicates, fix the Ed Sheeran/Disclosure mislink, repoint soft references, and safely delete orphans. Implements chat#1889, removes 663 duplicates, and adds guards so preview/local environments no‑op on prod-only steps.

  • Migration
    • Targeted merges: moved Stick Figure roster to the 195-song owner; consolidated Mac Miller and removed the artist-rosters-artist link; split Ed Sheeran from Disclosure and set correct Spotify socials.
    • Name-keyed collapse of rostered zero-song rows with an identity guard; 542 folds (e.g., Luh Tyler 77→1, Chillpill to the 17-song owner).
    • Repointed soft refs in rooms, scheduled tasks, files, sessions, campaigns, segments, and funnel analytics via a transitively resolved canonical map; temporarily disabled artist_segments user triggers to bypass a prod trigger bug.
    • Deleted 663 fully orphaned duplicates (socials/org/info/email included), excluding wallet-bearing or referenced rows; logged in zz_cleanup_manifest_20260729b; idempotent.
    • Env-compat: stubbed zz_dupe_manifest_20260729; guarded prod-specific writes (Disclosure attach, Ed Sheeran canonical mint) so Supabase Preview/local runs no-op instead of failing.