Add Context contract decision modules and acceptance fixtures
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 existingcontextIngestSchema.scope(single_track|single_video) is checked against the URL kind.directionis optional.assetsis 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):deniedfor an ordinary customer,requires_reviewfor a workspace admin. - The service may change a field only as a source
observationthat cites asource_version_id. Without one it isdenied(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.
- Shared canonical identity (
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 givespaid_activation: "blocked"and names the field; unpaid operations are alwaysallowed.configuredis a prerequisite, not permission to spend (#2123/#2105 own reservation). No environment reads.contextObservedDateSchema.ts: an observed date with explicitday/month/year/unknownprecision.unknowncarries 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 and1999-02-31is rejected. Year0000is 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/parseContextReleaseUrlrouting boundary, and 18 covercontextCoverageSchema,createContextReuseKeyandselectContextDocuments. - 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
nullorunknown.
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 underlib/context.pnpm exec eslint lib/context/contracts lib/context/__tests__/contextContracts.test.ts lib/context/__tests__/fixtures/contextAcceptanceFixtures.ts: exit 0.pnpm exec prettier --checkon 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
createContextReuseKeyfails 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_orderfield - 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_adminis an actor class in the decision table only; today's authorization layer distinguishes only membership. Mapping it is #2120's job.contextCoverageSchemacannot tell that a multilingual lyric transcript is missing a language. The lyric adapter must record it aspartial, 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