Add resolve_songwriter_identity Context action

SI
5d ago
+401-3

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: calls resolve_context_songwriter_identity after the shared authorizeContextOwner check. It lowercases request_id and professional_id before 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, like confirm_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, plan would throw for resolved requests. Songwriter research is still reported as not_implemented and 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 artifact lib/mcp/extension/generated-ui.ts is 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 on origin/main (pre-existing test-file typings and the missing generated-ui). None are in the changed files.
  • Ran (after review fixes): pnpm exec eslint on the 9 changed TS files (0 problems) and pnpm exec prettier --check on them plus the README (clean).
  • Red → green: run against origin/main production code, the new test file had 12 failures. processContextOperation rejected the action with a Zod invalid_union error, HTTP returned 400 instead of 200/409, and plan rejected a resolved target with identityConfirmed: true. With the implementation, all 28 pass. Review fixes: the 3 new cases (uppercase IDs on first call and replay; a plan target with identityConfirmed: true and no professionalId, or false with 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-valued professional_id, extra name / 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_id are 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_context roster 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) and confirmed: true; names never select identity.
  • Reachable through POST /api/context and the MCP context tool, sharing the same authorization and storage path.
  • Nothing enrolls a roster member, creates a professional or login account, or dispatches enrichment; plan still reports songwriter research as not implemented.
  • Requires database migration 20261010213600_context_songwriter_identity_resolution.sql to be applied before this deploys.
  • Kept out of the delegated OAuth catalog until the roster-tool delegation audit.