Add ingest_video Context action for one YouTube video locator

SI
5d ago
+315-3

Summary

Adds the provider-independent, zero-spend ingest_video action to the shared Context operation used by HTTP POST /api/context, the MCP context tool and the OAuth catalog (import_music_video_context). parseContextVideoUrl canonicalizes exactly one YouTube video URL (youtu.be, watch?v=, shorts, embed; tracking parameters dropped) and points Spotify tracks to ingest. The strict Zod schema rejects Spotify tracks and albums, playlists, channels, multi-v, http and lookalike hosts before authorization or storage. It accepts no topics or direction, because nothing is collected. After the existing authorizeContextOwner step, processVideoOperation calls create_context_video_request once with the workspace owner and returns the saved partial request (HTTP 200). It dispatches no workflow, calls no provider and spends no credits. The saved record keeps uploader/channel, performers, credited artists, recording, captions, language and media version as explicit unresolved/uncollected/unknown gaps. It never attaches to a song or artist document. ingest stays Spotify-only; its rejection message now points YouTube URLs at ingest_video.

Guard tests (videoLocatorBoundaries.test.ts) pin two boundaries. First, plan rejects a saved video entry before reading targets, so no collection is planned. Second, creative-direction, playlist-pitch and company-onboarding briefs leave the video_locator document out of song and artist evidence: it is listed only in input_manifest.excludedTopics, and the video request is reported as partial.

Plan adjustments found while validating against the code:

  • callContextRpc.ts keeps an RPC allowlist, so the function name is added there.
  • contextToolOperations.ts is satisfies Record<Action, …>, and contextToolOperations.test.ts asserts every schema action has a tool, so import_music_video_context is added.
  • fullOAuthCatalog.test.ts pins the delegated tool count (84 → 85).

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

Merge sequencing

docs → database → api → app. Depends on recoupable/database#107 (migration 20261010213100_context_video_entry.sql) being released first; until then, ingest_video fails with the generic 409. Sibling recoupable/docs#366 documents the action. All three use branch claude/ctx-2131-youtube-video-entry. No dependency on open API PRs (#981/#983/#985/#986–#991/#996).

Expected overlap on rebase:

  • api#1001 and api#1014 (both open) also edit lib/context/README.md; this PR inserts one bullet after ingest_release.
  • The ingest rejection string in processContextOperation.ts changed on one line (the ingest branch belongs to #2126).
  • lib/mcp/oauth/__tests__/fullOAuthCatalog.test.ts pins the delegated tool count. This PR, api#1017 and api#1004 each change it from 84 to 85, and api#1023 changes it to 86. Whichever merges later must bump the count again to the real total.

No chat change.

Verification

  • Ran: pnpm exec vitest run lib/context/__tests__/videoOperation.test.ts lib/context/__tests__/videoLocatorBoundaries.test.ts → 2 files, 22 passed, 0 failed.
    • videoOperation.test.ts (18): canonicalization; eight rejected non-video inputs; one create_context_video_request RPC with the authorized owner (organization and personal scope); no dispatch; rejection before authorize/RPC with the exact url refinement issue; strict rejection of topics/direction with the exact unrecognized_keys issue; ingest still rejects YouTube and names ingest_video.
    • videoLocatorBoundaries.test.ts (4): plan fails closed; three brief purposes exclude video_locator.
  • pnpm exec vitest run lib/context → Test Files 69 passed | 8 skipped (77); Tests 348 passed | 8 skipped (356); 0 failed.
  • pnpm build:extension && pnpm exec vitest run lib/mcp/oauth lib/mcp/tools/context lib/mcp/extension lib/supabase/context_requests → 23 files, 91 passed, 0 failed. build:extension is the esbuild step pnpm test already runs. It generates the gitignored generated-ui.ts; without it, fullOAuthRouting.test.ts cannot load.
  • pnpm exec tsc --noEmit → 198 pre-existing errors, all in unrelated test files (trigger, chat, credits, composio, …). With this PR's changes reverse-applied, origin/main produces the identical sorted error list, so this PR adds 0 errors.
  • pnpm exec eslint and pnpm exec prettier --check on the ten changed/new TypeScript files and lib/context/README.md → clean.
  • Red → green, two steps:
    • videoOperation.test.ts failed before implementation with Cannot find module '../parseContextVideoUrl'.
    • With origin/main's processContextOperation.ts restored (no schema spread, no routing), 5 of 18 fail on head ce28c44: organization-scoped save, personal-scope save, rejection of non-video URLs, rejection of topics/direction, and ingest naming ingest_video. All 18 pass with the change. (Before ce28c44 the two rejection tests only asserted that something was thrown, so they also passed without the action; only 3 of 18 failed.)
    • The boundary tests characterize existing behavior, so they were green from the start. A mutation adding video_locator to the creative-direction recipe makes the creative-direction case fail.
  • grep -n create_context_video_request lib/context/processVideoOperation.ts → present; no yt-dlp/apify/googleapis reference in the new files.
  • Re-run on head ce28c44 (finalizer pass): pnpm exec vitest run lib/context → Test Files 69 passed | 8 skipped (77); Tests 348 passed | 8 skipped (356); 0 failed. pnpm exec tsc --noEmit → 198 errors, none in lib/context, lib/mcp/oauth, lib/mcp/tools/context, lib/supabase/context_requests or any changed file. eslint and prettier on the ten changed TypeScript files and lib/context/README.md → clean. The MCP/OAuth suites above were not re-run, because only videoOperation.test.ts changed.
  • Not verified: hosted deployment, live providers (none are called by design), production database, and an authenticated HTTP/MCP round trip against a deployed service.

Out of scope / known gaps

  • Not built here:
    • a default-off verify_video metadata step (candidate zero-cost source: public YouTube oEmbed for title/channel only) and its commit RPC
    • captions, description and language retention
    • plan support for kind: 'video' (today it throws "Unsupported saved context entry")
    • artist-research reuse via createContextReuseKey
    • attribution review that links a related video to an existing artist or recording
    • chat UI
  • The ingest redirect to ingest_video reaches callers only in-process. contextIngestSchema still accepts a YouTube URL, so ingest with one passes validation and authorization and then throws. contextOperationHandler.ts maps every thrown error to a generic 409, and registerContextTool.ts returns a generic tool error, so HTTP and MCP clients do not see the message. Typed transport errors are pre-existing scope (api#1005 / recoupable/app#2125). This PR does not change the ingest schema, which belongs to #2126.
  • Briefs do not select video_locator evidence for any purpose. Video narrative, lyrics and brand content do not exist yet, so there is nothing to merge.
  • recoupable/app#2130 (CE14 checkpoint) still gates ticket completion.
  • Implemented and fixture-tested only; not connected, deployed or live-verified.

🤖 Generated with Claude Code