Accept unreleased recordings and planned releases without IDs

SI
5d ago
+428-1

Summary

Adds ingest_unreleased_recording and ingest_planned_release to the shared Context operation, so they are available over both HTTP POST /api/context and the delegated MCP tools import_unreleased_recording_context / import_planned_release_context. An account can now save an unreleased song or a planned album before it has an ISRC, UPC or store listing.

  • Validation: strict Zod schemas in unreleasedMusicOperationSchemas.ts reject isrc, upc, store_ids and spotify_id keys before authorization. They also mirror the database checks, so input the database would reject is a 400 rather than a 409 that tells the client to retry: unknown lifecycle states, control characters in titles and labels, non-lowercase or whitespace-containing http(s) links, more than 20 links, and a recording repeated in one tracklist. An omitted lifecycle is stored as unknown for both entries. The MCP descriptions tell agents identifiers are rejected and that a planned release may carry a tracklist of saved recording subject IDs.
  • Storage: processUnreleasedMusicOperation.ts calls the new service-role RPCs after the usual actor/workspace authorization. The database assigns an internal identity, and ISRC, UPC and store IDs stay unknown (Done-when 1 and 2).
  • No side effects: nothing is dispatched, looked up or charged.
  • Multiple releases: one recording subject can be referenced by several planned releases (Done-when 3, partial).
  • Company assessment (user-visible change): the company_onboarding brief recipe now includes the unreleased_recording and planned_release topics. Missing topics and gaps are computed against every recipe topic, so every company assessment, including Release-review assessments of released Spotify releases, now shows two more "What's missing" entries (unreleased recording, planned release) and two more unavailable gaps when none were submitted. This follows the existing pattern of listing inapplicable topics; chat renders topic strings generically, so nothing breaks. When planned music is submitted, the assessment lists it with its explicit identifier gaps, and recording_metadata / release_metadata stay missing for it.

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

Merge sequencing

docs (recoupable/docs#370) → database (recoupable/database#111) → api (this PR). Deploy only after database#111 is applied; before that, the RPCs do not exist and the actions return 409. This PR depends on no other open PR. api#981 and the related open PRs edit the same shared files (processContextOperation.ts, contextToolOperations.ts, callContextRpc.ts, fullOAuthCatalog.test.ts, lib/context/README.md). The edits here are small and appended at the end of each list, and the MCP tool count goes from 84 to 86. Expect a trivial rebase if those merge first.

Verification

  • Ran at head 8f1a6b64 (0 commits behind main):
    • pnpm exec vitest run lib/context → 68 files passed, 8 skipped; 335 tests passed, 8 skipped, 0 failed.
    • pnpm exec vitest run lib/context lib/mcp/oauth/__tests__/{fullOAuthCatalog,registerFullOAuthTools,contextToolOperations}.test.ts → 71 files passed, 8 skipped; 347 tests passed, 8 skipped, 0 failed.
    • pnpm exec vitest run lib/mcp/oauth lib/supabase/context_requests → 87 tests passed. One file, fullOAuthRouting.test.ts, fails to load because lib/mcp/extension/generated-ui.ts is a gitignored build artifact missing from this worktree (environment only).
    • pnpm exec tsc --noEmit → 199 errors, none in any changed file. An independent review compared against a main-equivalent copy and found them pre-existing.
    • pnpm exec eslint and pnpm exec prettier --check on all changed files → clean.
  • Red → green:
    • Before the implementation, unreleasedMusicOperation.test.ts failed 7 of 7 (unknown action, or path action instead of recording), the new companyAssessment.test.ts case failed with expected [] to deeply equal [...], and fullOAuthCatalog.test.ts failed with "length of 86 but got 84". The rejection tests include positive controls, so they cannot pass vacuously.
    • Review follow-up: with the previous unreleasedMusicOperationSchemas.ts, 3 of the 8 tests fail (control characters accepted, omitted release lifecycle rejected, and uppercase/whitespace links plus duplicate tracklist IDs accepted); all 8 pass with the tightened schemas.
  • Not verified: hosted deployment, the production database (migration not applied anywhere), live providers, real MCP clients and the chat UI.

Out of scope / known gaps

  • Later ISRC, UPC or store-ID assignment and conflicting-ID handling are slice 2 of #2141.
  • plan rejects these entries with Unsupported saved context entry (409) instead of returning a structured unimplemented plan like the songwriter/company/campaign/material entries. No collection module exists for them yet, and nothing is dispatched.
  • The database target reads (list_context_*_request_target) are not allowlisted in the API until a planning or inspector slice needs them.
  • No chat or inspector UI (#2146).
  • No audio or file intake (#2140).
  • No composition, rights, ownership or roster linkage.

🤖 Generated with Claude Code


Summary by cubic

Accounts can now save unreleased recordings and planned releases before they have an ISRC, UPC, or store listing. Adds ingest_unreleased_recording and ingest_planned_release to the shared Context operation, exposed over HTTP POST /api/context and via the MCP tools import_unreleased_recording_context / import_planned_release_context.

  • Strict Zod schemas reject isrc, upc, store_ids, and spotify_id keys before authorization, plus unknown lifecycle states, non-http(s) links, and more than 20 links; the database assigns an internal identity and records identifiers as unknown.
  • Nothing is dispatched, looked up, or charged. The company_onboarding brief recipe now includes both topics, so assessments show planned music with its explicit identifier gaps.

Migration

Deploy only after recoupable/database#111 is applied. Before 20261010214100_context_unreleased_music_entry.sql exists, the RPCs are missing and both operations return 409.