Quarantine conflicting Spotify track identity evidence
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_identifiersmaps the track to another ISRC, the observation is saved as acontext_resource_linksrow (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 inconflict. - Accepted conflict. When an accepted identity link points to another recording, that link is reported through
linkIdsand never downgraded. - Request outcome. The request becomes
failedwith erroridentity_conflict: …, using the same code prefix as recoupable/api#1001. Its output carriesoutput.identityConflictsand emptysubjectIds. 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 underresolvedConflict. - The track's other open conflict links become
rejected, withresolution,acceptedIsrcandresolvedAtadded to their evidence. - A
candidate/rejectedlink that would still block acceptance raises an error. Previously the request could complete without an accepted track identity.
- A conflict link to that recording is promoted to
- Guests. A claimed guest still becomes
readybecause its payload was extracted, and adoption returnsclaimed. 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 extendstest_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 identityraised at commit, and acontext_one_accepted_identityduplicate key). - Review fixes: against the previous version of this migration, 7 of 37 failed.
test_later_observation_of_the_quarantined_isrc_promotes_the_linkfailed with'conflict' != 'accepted'because no accepted identity was written.test_reviewed_link_blocking_acceptance_raises_instead_of_completingfailed with "Expected SQL failure". The rest failed on the evidence and error-prefix changes. After the fix: 37/37.
- Initial slice: with the migration absent, 4 of 24 failed (
- 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 intosongs. - 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 areacceptedorrejected.
🤖 Generated with Claude Code