Map Apple Music listings into Context as storefront observations

SI
5d ago
+1394-0

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. The apple_music and youtube_music provider enums are typed; only Apple song/album are produced here.
  • lib/context/providers/mapAppleMusicListings.ts — maps one storefront's result.
    • exact_isrc requires 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 ambiguous and kept: multiple store songs, remixes, and a song whose own ISRC differs.
    • A missing ISRC is not_listed for that storefront only. A hit Apple echoes but does not resolve is unknown for both availability and match (not ambiguous).
    • Title and artist comparison ignores case, punctuation and accents (Beyonce matches Beyoncé).
  • lib/context/providers/compareAppleMusicStorefronts.ts — adds regional_variant only when song or album ids differ between storefronts that list the song. With fewer than two such storefronts, varies stays null.
  • 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 getAppleSongsByIsrc sends (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.

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 eslint and pnpm exec prettier --check on all changed files → clean.
  • First commit only: pnpm exec vitest run lib/apple → 7 files, 46 passed. lib/apple is reused, not modified, and the follow-up does not touch it.
  • Tests inject fixture lookups. They never call generateDeveloperToken or the network; the Apple URL appears only in source-URL assertions.
  • Red → green:
    1. With the implementation files absent, both original test files failed (Cannot find module '../mapAppleMusicListings' / '../collectContextAppleMusicListings').
    2. 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.ts failed with a missing module. For example, compares titles and artists by whole words, not substrings failed because "Songbird" was accepted as a title match.
    3. All 28 passed after the first implementation.
    4. 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, no matchedAgainst in the input), observes several storefronts in one saved result… (input), ignores accent-only differences in titles and artists and keeps an echoed but unresolved hit as unknown… (match was ambiguous). All 30 pass after it.
  • 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 matchedAgainst and keyed into reuse, but whoever wires this collector must supply them from server-side recording metadata.
  • No context_resources writes, identity links, release creation, rights, ownership or roster implications.
  • No cross-provider matching, and no UPC → release mapping beyond retaining Apple's album upc as 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_listed for that storefront only. A hit Apple echoes but does not resolve stays unknown, never listed or ambiguous.
  • exact_isrc requires a single song that echoes the ISRC with no whole-word title/artist contradiction (accents ignored) and no extra version token; anything else stays ambiguous, never dropped or promoted.
  • regional_variant is flagged only when song or album IDs differ between storefronts that list the song; fewer than two listed storefronts leave varies null.
  • 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 the dsp_listings topic) before deployment; the claim raises Invalid enrichment module without it.

Out of scope

  • Not wired into planning, dispatch, HTTP, MCP, or brief recipes.
  • youtube_music, music_video, art_track, and uploader_channel are typed in the schemas but nothing collects them; YouTube Music is tracked in #2131.