Map Apple Music listings into Context as storefront observations
Summary
Adds a server-side Context collector that records Apple Music listings for one recording ISRC as a storefront-scoped dsp_listings observation. It reuses the existing lib/apple/getAppleSongsByIsrc lookup and the claim/complete/fail enrichment chain. It covers the ticket's first slice:
- provider, resource type, storefront, version tokens and match evidence are retained;
- songs and albums are separate store objects, and album ids never become song or recording IDs;
- music video, art track and uploader channel are typed but not collected (YouTube Music listing collection is still open in #2143);
- availability is kept per storefront;
- regional variants, remixes and ambiguous matches are covered by fixtures.
New files:
lib/context/providers/dspListingTypes.ts— zod schemas for song, album, music video, art track and uploader channel listings, plus availability and match evidence. Theapple_musicandyoutube_musicprovider enums are typed; only Apple song/album are produced here.lib/context/providers/mapAppleMusicListings.ts— maps one storefront's result.exact_isrcrequires a single song that echoes the ISRC and has no whole-word title or artist contradiction and no extra version token (remix, live, edit and similar).- Everything else is
ambiguousand kept: multiple store songs, remixes, and a song whose own ISRC differs. - A missing ISRC is
not_listedfor that storefront only. A hit Apple echoes but does not resolve isunknownfor both availability and match (notambiguous). - Title and artist comparison ignores case, punctuation and accents (
BeyoncematchesBeyoncé).
lib/context/providers/compareAppleMusicStorefronts.ts— addsregional_variantonly when song or album ids differ between storefronts that list the song. With fewer than two such storefronts,variesstaysnull.lib/context/enrichment/collectContextAppleMusicListings.ts:- Takes 1–10 storefronts (default
us), deduplicated and sorted for a stable fingerprint, and saves them in one observation with one declared and observed source per storefront. - The reuse fingerprint covers the storefront set, the collection version, and the title, artists and duration the listing is matched against (
matchedAgainst), so different match inputs never reuse an earlier match status. - Each declared and observed source URL is the exact request
getAppleSongsByIsrcsends (filter[isrc],include=albums,extend=composerName,audioVariants). - Rechecks that the recording's ISRC matches before claiming and after the lookup.
- Records no cost claim (
costUsd: null). - If any storefront lookup fails, nothing is saved.
- Takes 1–10 storefronts (default
Design note: the scoping plan proposed one module per storefront. A Context document holds one current result per subject and topic, so separate storefront modules would replace each other's current result, and re-claiming the earlier storefront would return unknown (needs reconciliation). Saving all observed storefronts in one result avoids both. The plan also derived regional_variant from multiple albums inside one storefront. In practice that is usually a single plus an album, so it is now multiple_store_songs / multiple_album_contexts, and regional_variant needs a cross-storefront difference.
Refs recoupable/app#2143 (child of epic recoupable/app#2116).
Merge sequencing
docs (no change) → database recoupable/database#114 (20261010214300_context_dsp_listings.sql, must be applied first, or the claim raises Invalid enrichment module) → this api PR → app (no change).
- No dependency on other open PRs.
- Shared planner, dispatcher, operation and MCP files are untouched (
processContextOperation,planContextModules,dispatchPlannedContextModule,getContextModuleInput,contextToolOperations). The README edit is an append-only subsection.
Verification
Results on the current head (after the review follow-up commit 05c80e08):
- Ran:
pnpm exec vitest run lib/context/providers/__tests__/appleMusicListings.test.ts lib/context/providers/__tests__/appleMusicStorefronts.test.ts lib/context/enrichment/__tests__/appleMusicListingsCollection.test.ts→ 3 files, 30 passed, 0 failed. - Ran:
pnpm exec vitest run lib/context→ 70 files passed, 8 skipped; 356 tests passed, 8 skipped, 0 failed. - Ran:
pnpm exec tsc --noEmit→ 199 errors, all in files this PR doesn't touch; the error set is identical to the first commit's run, and none is in this PR's files. - Ran:
pnpm exec eslintandpnpm exec prettier --checkon all changed files → clean. - First commit only:
pnpm exec vitest run lib/apple→ 7 files, 46 passed.lib/appleis reused, not modified, and the follow-up does not touch it. - Tests inject fixture lookups. They never call
generateDeveloperTokenor the network; the Apple URL appears only in source-URL assertions. - Red → green:
- With the implementation files absent, both original test files failed (
Cannot find module '../mapAppleMusicListings'/'../collectContextAppleMusicListings'). - After rewriting the tests for multi-storefront observation, the regional-variant rule, version tokens and whole-word matching, 12 tests failed against the previous partial implementation, and
appleMusicStorefronts.test.tsfailed with a missing module. For example,compares titles and artists by whole words, not substringsfailed because"Songbird"was accepted as a title match. - All 28 passed after the first implementation.
- Review follow-up: 5 new or tightened tests failed before the fix —
isolates reuse by the title, artists and duration the listing is matched against(title/artists/duration changes produced the same fingerprint),claims an observation module keyed by ISRC, storefronts and version(inexact source URL, nomatchedAgainstin the input),observes several storefronts in one saved result…(input),ignores accent-only differences in titles and artistsandkeeps an echoed but unresolved hit as unknown…(match wasambiguous). All 30 pass after it.
- With the implementation files absent, both original test files failed (
- Not verified: hosted deployment, live Apple Music calls (no developer-token or provider spend was used), any hosted or production database, and whether the migration is applied anywhere. State: implemented and fixture-tested only. Not connected, not deployed, not live-verified.
Out of scope / known gaps
- Not wired into
plan, the dispatcher, HTTP, MCP or any brief recipe. Nothing calls the collector yet. - No YouTube Music, music video, art track or uploader channel collection. Those values are only reserved in the enums. YouTube Music store listings remain an open #2143 gap (#2131 covers single-video YouTube context, not store listings), and because every listing storefront field validates Apple storefront codes, a YouTube Music collector needs a provider-aware storefront schema first.
- Title and artists are caller-supplied; only the ISRC is checked against the recording subject. They are recorded as
matchedAgainstand keyed into reuse, but whoever wires this collector must supply them from server-side recording metadata. - No
context_resourceswrites, identity links, release creation, rights, ownership or roster implications. - No cross-provider matching, and no UPC → release mapping beyond retaining Apple's album
upcas a field.
🤖 Generated with Claude Code
Summary by cubic
Adds a Context enrichment collector that records Apple Music listings for one recording ISRC as storefront-scoped dsp_listings observations, reusing the existing Apple lookup and the claim/complete/fail chain.
- The module's reuse fingerprint includes the ISRC, storefronts, collection version, and the title, artists and duration the listing is matched against; any change invalidates reuse.
- Songs and albums stay separate store objects; an album ID never becomes a song, recording, or release identifier.
- Availability is observed per storefront at one time; a missing ISRC is
not_listedfor that storefront only. A hit Apple echoes but does not resolve staysunknown, neverlistedorambiguous. exact_isrcrequires a single song that echoes the ISRC with no whole-word title/artist contradiction (accents ignored) and no extra version token; anything else staysambiguous, never dropped or promoted.regional_variantis flagged only when song or album IDs differ between storefronts that list the song; fewer than two listed storefronts leavevariesnull.- Storefronts are deduplicated and saved in one observation; separate storefront modules would replace each other's current result in the Context document.
- Each declared source URL is the exact request sent to Apple, and any failed storefront lookup saves nothing. Records no cost claim.
Migration
- Requires database migration
20261010214300_context_dsp_listings.sql(adds thedsp_listingstopic) before deployment; the claim raisesInvalid enrichment modulewithout it.
Out of scope
- Not wired into planning, dispatch, HTTP, MCP, or brief recipes.
youtube_music,music_video,art_track, anduploader_channelare typed in the schemas but nothing collects them; YouTube Music is tracked in #2131.