Add resolve_songwriter_identity Context action
Summary
Adds the resolve_songwriter_identity action to the shared Context operation. It is reachable through POST /api/context and the MCP context tool via the same processContextOperation. A saved ingest_songwriter_name request used to stay identityConfirmed: false indefinitely. Now an authorized operator can link it to one organization professional they explicitly selected (professional_id from /api/organizations/professionals) and confirmed (confirmed: true). This advances the #2136 items "resolve ambiguous names before enrichment" and "test new, existing, ambiguous and revoked-access requests through shared REST/MCP logic". Names never select identity. Nothing enrolls a roster member, creates a professional or login account, or dispatches enrichment.
lib/context/songwriterIdentitySchema.ts: a strict request schema that reuses the Context idempotency-key rules, plus the receipt schema.lib/context/processSongwriterIdentityOperation.ts: callsresolve_context_songwriter_identityafter the sharedauthorizeContextOwnercheck. It lowercasesrequest_idandprofessional_idbefore the RPC (Postgres returns canonical lowercase UUIDs), validates the receipt, and rejects any receipt whose request or professional differs from the confirmed selection. Uppercase IDs therefore resolve and replay like lowercase ones.processContextOperation.ts: adds the union member (after the release-case schemas) and a one-line dispatch.callContextRpc.ts: adds the RPC to the allowlist.contextToolOperations.ts: adds MCP metadata (write, not read-only).registerFullOAuthTools.ts: keeps the action out of the delegated OAuth catalog, likeconfirm_professional_roster, until the roster-tool delegation audit.planning/planStoredContextModules.ts: accepts a resolved songwriter target (identityConfirmed: true+professionalId) and rejects a target where the two disagree. Without this,planwould throw for resolved requests. Songwriter research is still reported asnot_implementedand nothing is dispatched.lib/context/README.md: documents the action and its limits.
Refs recoupable/app#2136 (child of epic recoupable/app#2116).
Merge sequencing
docs recoupable/docs#369 ā database recoupable/database#110 (20261010213600_context_songwriter_identity_resolution.sql, must be applied before this deploys) ā api (this PR). There is no app change in this slice. Does not depend on any other open PR. It touches the same shared dispatcher files as api#981/#983, but the hunks are kept separate (union entry after ...releaseCaseOperationSchemas, dispatch after the release-case block, allowlist entry next to list_context_songwriter_request_target, a separate OAuth continue).
Conflict with open api#1011 (ownership parity): its lib/context/__tests__/oauthContextParity.test.ts expects every contextOperationSchema option to be registered as a delegated OAuth tool (registered.size === contextOperationSchema.options.length + 1). This PR deliberately skips resolve_songwriter_identity in registerFullOAuthTools.ts. Whichever PR merges second must reconcile: either the parity test excludes this action (as fullOAuthCatalog.test.ts does here) and adds a fixture for it in the shared-path parity cases, or the delegated-grant audit approves exposing it and the continue is removed. Both PRs pass on their own; they fail together until that one change is made.
Verification
- Ran (after review fixes, HEAD 4e24f391):
pnpm exec vitest run lib/context/__tests__/songwriterIdentityResolution.test.ts: 31 passed, 0 failed. - Ran (after review fixes):
pnpm exec vitest run lib/context lib/mcp/oauth/__tests__/fullOAuthCatalog.test.ts lib/professionals: 70 files passed, 8 skipped; 375 tests passed, 8 skipped, 0 failed. - Ran (before review fixes; those touch only
lib/context):pnpm exec vitest run lib/mcp app/api/context lib/supabase/context_requests: 192 tests passed, 0 failed. Two files (registerRecoupExtension.test.ts,fullOAuthRouting.test.ts) cannot load in a fresh worktree because the gitignored build artifactlib/mcp/extension/generated-ui.tsis missing. This is unrelated to this change. - Ran (after review fixes):
pnpm exec tsc --noEmit: 199 diagnostics, identical line-for-line to the same run onorigin/main(pre-existing test-file typings and the missinggenerated-ui). None are in the changed files. - Ran (after review fixes):
pnpm exec eslinton the 9 changed TS files (0 problems) andpnpm exec prettier --checkon them plus the README (clean). - Red ā green: run against
origin/mainproduction code, the new test file had 12 failures.processContextOperationrejected the action with a Zodinvalid_unionerror, HTTP returned 400 instead of 200/409, andplanrejected a resolved target withidentityConfirmed: true. With the implementation, all 28 pass. Review fixes: the 3 new cases (uppercase IDs on first call and replay; a plan target withidentityConfirmed: trueand noprofessionalId, orfalsewith one) failed against the previous commit d05db175 (3 failed, 28 passed) and pass now (31/31). - Covered cases:
- A new resolution sends exact RPC params and never dispatches.
- An exact replay returns
created: false. - Missing or false
confirmed, a missing or name-valuedprofessional_id, extraname/account_id/roster_intent, or an empty key are rejected before authorization or storage. - Revoked access never reaches storage.
- Database denials and conflicts propagate once, and HTTP does not leak their text.
- A mismatched or malformed receipt is rejected.
- Uppercase
request_id/professional_idare sent lowercase and match the stored lowercase receipt, including on replay. - HTTP and MCP reach the same storage call with the same actor.
- The delegated OAuth catalog omits the action.
- Not verified: hosted deployment (the Vercel integration builds a preview automatically on push; it was not exercised), live providers, the production database, and an end-to-end call against a migrated database.
Out of scope / known gaps
- Chat UI to pick a professional for a saved songwriter name (follow-up once this is merged).
- Enrichment or research for resolved songwriters. Company-name resolution. Un-resolving a resolution.
- Delegated (OAuth) exposure of this action, which waits for the same audit as
confirm_professional_roster. - The
commit_spotify_contextroster side effects (#2147/#2140). No rights, mandates or roster changes.
š¤ Generated with Claude Code
Summary by cubic
Adds the resolve_songwriter_identity action so an authorized operator can link a saved songwriter-name request to one explicitly selected organization professional. Previously a saved ingest_songwriter_name request stayed identityConfirmed: false forever.
- Requires the
professional_id(from/api/organizations/professionals) andconfirmed: true; names never select identity. - Reachable through
POST /api/contextand the MCPcontexttool, sharing the same authorization and storage path. - Nothing enrolls a roster member, creates a professional or login account, or dispatches enrichment;
planstill reports songwriter research as not implemented. - Requires database migration
20261010213600_context_songwriter_identity_resolution.sqlto be applied before this deploys. - Kept out of the delegated OAuth catalog until the roster-tool delegation audit.