Add Context contract decision modules and acceptance fixtures

SI
5d ago
+1519-0

Summary

Adds the Context Engine's contract decision modules as pure, single-purpose functions in a new lib/context/contracts/ folder, plus a synthetic acceptance fixture catalogue. The dispatcher, README and schema.ts are not touched, because open sibling PRs edit them. Nothing in production imports these modules yet. They are implemented and fixture-tested contracts for later tickets to adopt, not live behaviour.

  • contextContractVersion.ts + contextContractEnvelopeSchema.ts: context-contract-v1, a versioned URL-only envelope built on the existing contextIngestSchema. scope (single_track | single_video) is checked against the URL kind. direction is optional. assets is reserved and accepts only an empty list; asset bodies are rejected until file intake has storage and access checks. Caller-supplied actor, owner or payer is still rejected.
  • resolveContextCorrectionAuthority.ts: the correction decision table.
    • Shared canonical identity (spotify_track_id, isrc, credited_artist_order): denied for an ordinary customer, requires_review for a workspace admin.
    • The service may change a field only as a source observation that cites a source_version_id. Without one it is denied (source_observation_requires_source_version).
    • Model estimates and interpretations never become observations by relabelling, even with a cited source (derived_output_cannot_become_observation). A new observation enters through source ingestion with its own source version.
    • Creative proposals and customer assertions never become any factual kind.
    • Customer-side corrections are recorded as private assertions, never on the shared row.
    • The persistence layer (#2124) must still confirm that the cited source version exists and is in scope.
  • contextPilotConfigSchema.ts + evaluateContextPilotActivation.ts: five pilot fields (spend ceiling, paid-but-unusable output, retention/withdrawal, freshness, readiness behaviour). Each is either a concrete value or "unresolved". Any unresolved field gives paid_activation: "blocked" and names the field; unpaid operations are always allowed. configured is a prerequisite, not permission to spend (#2123/#2105 own reservation). No environment reads.
  • contextObservedDateSchema.ts: an observed date with explicit day/month/year/unknown precision. unknown carries no value, and a value cannot claim more precision than it has. Day precision must be a real calendar date, so leap years are checked and 1999-02-31 is rejected. Year 0000 is rejected.
  • __tests__/fixtures/contextAcceptanceFixtures.ts: 69 synthetic decision-table fixtures.
    • 39 exercise the new modules: authority 14, contract 10, observed date 9, pilot 6.
    • 30 pin existing behaviour that this ticket's scenarios depend on: 12 run the live parseContextUrl/parseContextReleaseUrl routing boundary, and 18 cover contextCoverageSchema, createContextReuseKey and selectContextDocuments.
    • Scenario families: same-name artists, collaborations and credit order, reissues, wrong or partial audio, unavailable/full/multilingual lyrics, throwbacks, source injection, private evidence, and two songs by one artist.
    • No real artists, store IDs or ISRCs. Identity stays null or unknown.

Album/playlist routing: this PR deliberately does not add its own URL router. api#1001 (ctx-2126) gives the live parseContextUrl explicit album → ingest_release, playlist and artist messages, and api#1019 (ctx-2131) adds ingest_video for one YouTube video. A second, unused routing table would drift from both. The routing fixtures pin the live boundary instead: a Spotify album is accepted only by the release locator, while playlists, artist pages, channels, a bare /watch, non-HTTPS, credentialed and unknown URLs are rejected by both. These fixtures stay green whether or not #1001 and #1019 merge first.

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

Merge sequencing

api only. No database or app PR for this ticket. The sibling docs PR recoupable/docs#358 is now a draft and redundant: docs#357, #360, #365 and #366 already edit the same context.json lines (see that PR). No dependency on open PRs. This PR does not edit lib/context/README.md, processContextOperation.ts, schema.ts or parseContextUrl.ts, which api#981/#983/#985/#1001/#1019 touch. Wiring the authority table and envelope into the dispatcher is left to #2120/#2139/#2124.

Verification

All runs are on head 33421cf (base origin/main c72887a).

  • pnpm exec vitest run lib/context/__tests__/contextContracts.test.ts: 1 file, 80 tests passed, 0 failed.
  • pnpm exec vitest run lib/context: 76 files (68 passed, 8 skipped); 414 tests (406 passed, 8 skipped, 0 failed).
  • pnpm exec tsc --noEmit: 199 errors in 66 files, all pre-existing on origin/main (lib/tasks/__tests__, lib/trigger/__tests__, lib/chats/__tests__, …). 0 errors under lib/context.
  • pnpm exec eslint lib/context/contracts lib/context/__tests__/contextContracts.test.ts lib/context/__tests__/fixtures/contextAcceptanceFixtures.ts: exit 0. pnpm exec prettier --check on the same paths: clean.
  • Grep of added lines for fetch(, process.env, Supabase/serverClient, Arweave and ISRC-shaped strings: no matches.
  • Red → green:
    • Before the module changes, 8 tests failed: the estimate/interpretation relabel fixtures, observation with and without a cited source, the impossible-date and year-0000 fixtures, and the "never records relabelled derived output" sweep. All 80 passed after.
    • The original commit's suite failed to load without lib/context/contracts.
  • Mutation checks (each reverted afterwards):
    • Removing the derived→observation rule fails 3 tests.
    • Removing the source-version requirement fails 1.
    • Removing the calendar check fails 2.
    • Removing the year-zero check fails 1.
    • Removing the source sort in createContextReuseKey fails the reissue fixture.
  • Not verified: hosted deployment, live providers (no provider calls are made; every module is a pure function), production or hosted database (no migrations), HTTP/MCP wiring (the modules are not wired), pnpm build.

Out of scope / known gaps

  • Not wired: no production code imports lib/context/contracts. The ticket's Done-when item 4 ("every proposed persisted concept has a concrete initial use and fixture") is not met by this PR: it proposes no persisted concept, and the decision modules have fixtures but no production caller yet.
  • Ticket scope not addressed here:
    • artist/recording/release/video/era associations
    • recording-to-release membership
    • credited artist roles and order beyond the authority table's credited_artist_order field
    • lifecycle states
    • cancellation and idempotency semantics beyond the existing idempotency_key
    • consumer topic, size and media requirements
    • an explicit "HTTP success does not imply accepted context" contract
  • Correction persistence and source-version existence checks (#2124), spend reservation and settlement (#2123/#2105), YouTube enablement (api#1019), and identity resolution in the database (#2117/#2119).
  • workspace_admin is an actor class in the decision table only; today's authorization layer distinguishes only membership. Mapping it is #2120's job.
  • contextCoverageSchema cannot tell that a multilingual lyric transcript is missing a language. The lyric adapter must record it as partial, and the fixture documents that as a requirement rather than a schema guarantee.
  • No fixture proves hosted schema behaviour. SCENARIOS.md entries S08/S10/S12/S27/S28/S35 still need dedicated runs.

🤖 Generated with Claude Code