Quarantine conflicting Spotify track identity evidence

SI
5d ago
+438-0

Summary

commit_spotify_context raised Conflicting recording identity when a submitted Spotify track was already mapped to a different ISRC. The API then recorded only a generic extraction failure, and the observed ISRC, existing mapping and source attribution were lost. That left the recoupable/app#2119 acceptance item "conflicting track-to-recording mappings are visible and excluded from accepted context pending resolution" unmet.

Migration 20261010211900_context_identity_conflict_quarantine.sql replaces the function. Only the identity check and the accepted-identity insert change:

  • Legacy conflict. When song_identifiers maps the track to another ISRC, the observation is saved as a context_resource_links row (relation='identity', status='conflict'). The link points from the track to the existing recording subject, which is created if absent. Its evidence holds both ISRCs, the provider ID, source URL, retrieval time and observed title. The workspace-private request ID is not copied into this shared evidence; the request's own output references the link. Retries refresh evidence only on rows still in conflict.
  • Accepted conflict. When an accepted identity link points to another recording, that link is reported through linkIds and never downgraded.
  • Request outcome. The request becomes failed with error identity_conflict: …, using the same code prefix as recoupable/api#1001. Its output carries output.identityConflicts and empty subjectIds. No songs, identifiers, observed-recording/release/artist subjects, artists or documents are written.
  • Resolution. A later verified observation accepted for the same track handles the quarantined links:
    • A conflict link to that recording is promoted to accepted, with its prior evidence kept under resolvedConflict.
    • The track's other open conflict links become rejected, with resolution, acceptedIsrc and resolvedAt added to their evidence.
    • A candidate/rejected link that would still block acceptance raises an error. Previously the request could complete without an accepted track identity.
  • Guests. A claimed guest still becomes ready because its payload was extracted, and adoption returns claimed. In both cases the destination request reports the conflict. Before this migration both calls raised and rolled back.

The rest of the non-conflict path is unchanged from the 20261009190000 body. Explicit service-role-only grants are restated.

Refs recoupable/app#2119 (child of epic recoupable/app#2116).

Merge sequencing

docs recoupable/docs#363 → database (this PR) → api recoupable/api#1007. Apply this migration before deploying API consumers that read output.identityConflicts. This PR depends on no OPEN PR. It does not touch the tables or functions changed by api#981/database#89, database#90/#92/#94/#95 or database#88.

Verification

All runs used disposable local PostgreSQL 17.7 with export PATH="/opt/homebrew/opt/postgresql@17/bin:$PATH" LC_ALL=C. macOS needs the locale export, or the postmaster refuses to start.

  • Ran: python3 -m unittest discover -s tests -p 'test_identity_conflicts.py' → Ran 37 tests, OK. The suite now extends test_research_roster.ResearchRoster, so the 9 research-roster and 18 onboarding regressions also run against the new function in CI. The 10 own tests cover:
    • legacy quarantine
    • retry idempotence
    • resolution that closes the superseded conflict
    • a later observation that promotes the quarantined link
    • the blocking-link guard
    • an accepted identity never downgraded
    • claimed guest completion
    • ready guest adoption
    • unchanged non-conflict research
    • anon/authenticated denial
  • Ran: python3 -m unittest discover -s tests -p 'test_*roster.py' → Ran 38 tests, OK. These suites load migrations only up to 20261009190000.
  • Ran: a diff of the function body against 20261009190000_context_research_without_roster.sql. Only these parts differ: declared variables, the identity-check block, the accepted-identity insert with its promotion/guard/closure, and the grant lines.
  • Red → green:
    • Initial slice: with the migration absent, 4 of 24 failed (Conflicting recording identity raised at commit, and a context_one_accepted_identity duplicate key).
    • Review fixes: against the previous version of this migration, 7 of 37 failed. test_later_observation_of_the_quarantined_isrc_promotes_the_link failed with 'conflict' != 'accepted' because no accepted identity was written. test_reviewed_link_blocking_acceptance_raises_instead_of_completing failed with "Expected SQL failure". The rest failed on the evidence and error-prefix changes. After the fix: 37/37.
  • CI: the discovery line is in .github/workflows/artist-onboarding.yml.
  • Not verified: hosted deployment, live providers, production database, PostgreSQL 15/16 locally (CI covers 16), concurrent-process stress on the conflict path.

Out of scope / known gaps

  • No review operation (accept/correct/withdraw is recoupable/app#2124), merge, split or cleanup. Promotion and closure happen only when a verified observation is accepted for the same track.
  • When the contested mapping is an accepted link, the new observation (observed ISRC, source, time) is recorded only in that request's output.identityConflicts. No shared conflict row is written, because the accepted row occupies that key and the observed ISRC is deliberately not inserted into songs.
  • Conflict evidence is last-writer-wins per row: a later conflicting observation replaces the earlier one.
  • Artist-identity ambiguity (Conflicting artist identity) still rejects transactionally and is not quarantined.
  • Existing rows are not migrated or inventoried (recoupable/app#2117). An inventory counting status='conflict' now sees only open conflicts; resolved ones are accepted or rejected.

🤖 Generated with Claude Code