Attribute evidence freshness in compiled Context briefs
Summary
Adds evidence-freshness attribution to compiled Context briefs and a fixture-tested two-song checkpoint. There is no recollection, model call, provider spend or database change. read_context_documents already returns sources[].retrievedAt per document, but compileContextBrief dropped it. The compiler now adds an additive input_manifest.freshness object:
oldestRetrievedAt/newestRetrievedAtacross every dated source of the selected documentsdocumentsWithoutRetrievalDate- per-document
oldestRetrievedAt/newestRetrievedAt
Every document section of the Markdown text now ends with a Retrieved: line: one timestamp, <oldest> to <newest> when that document's sources differ, or unknown. Source locations never enter the text or the manifest.
The new twoSongBriefCheckpoint.test.ts drives the shared processContextOperation surface, the same one HTTP and MCP call per transports.test.ts. Its fixture has two same-artist track requests, an out-of-recipe customer supporting-text document and a withdrawn revision. It asserts:
- artist reuse and separate per-song documents
- task-specific creative-direction vs playlist-pitch output
- honest lyric gaps
- exclusion of withdrawn, other-workspace and out-of-recipe evidence
- freshness bounds
- identical
brief/save_brief/read_briefsnapshots with no dispatch
Because compiled output changed, the Sites save_brief idempotency suffix moves from brief-v3 to brief-v4 (saveSiteContextBrief.ts). save_context_brief raises Brief key already used for different output when a key is replayed with different output. Without the bump, a site whose current revision already had a v3 brief would conflict after deploy on concepts → generate and on every retry. The workflow then reports "stopped during context brief" and the revision does not change. albumContextKeys.test.ts now locks the exact keys.
Plan deviation (validated against the deployed RPC): the scoping plan proposed bumping compilerVersion to context-brief-v2 and adding retrievedAt inside each input_manifest.documents[] entry. The live save_context_brief function (20261010010000_context_company_assessment.sql) raises Invalid compiled brief unless compilerVersion is exactly context-brief-v1. It raises Brief manifest does not match evidence unless input_manifest.documents equals a six-key shape it rebuilds itself. This PR keeps both pinned values stable and carries freshness in a new manifest key that the RPC tolerates. The checkpoint test asserts the six-key shape and v1, so a regression would fail the test.
Refs recoupable/app#2130 (child of epic recoupable/app#2116).
Merge sequencing
docs → api. Merge the companion docs PR recoupable/docs#360 first; it now documents the freshness contract only. The company_onboarding docs catch-up belongs to docs#365/#358. There is no database or app PR in this slice. This PR does not touch processContextOperation.ts or the MCP registration files, so it does not overlap the open api#981/#983/#985 dispatcher PRs. The one-line saveSiteContextBrief.ts change is not touched by any other open api PR. api#1018 edits only the company_onboarding recipe in compileContextBrief.ts, which is a different hunk.
Verification
Final pass, head 393dcb91:
- Ran:
pnpm exec vitest run lib/context→ 68 files passed, 8 skipped (live/local suites skip withoutCONTEXT_LIVE_TEST/CONTEXT_PAID_TEST); 332 tests passed, 8 skipped, 0 failed. - Ran:
pnpm exec vitest run lib/sites→ 50 files passed, 3 skipped; 226 tests passed, 4 skipped, 0 failed. The ticket'slib/sites/productionpath has no test files; Sites tests live inlib/sites/__tests__. - Ran:
pnpm exec tsc --noEmit→ 199 errors, all pre-existing and none in changed files orlib/context. The two inlib/sitesare in untouchedbuildProgress.test.ts/publishAction.test.ts. An untouchedorigin/maincheckout at c72887a reported 202, with 0 inlib/context. - Ran:
pnpm exec eslinton the 7 changed TS files → clean.pnpm exec prettier --checkon them plusREADME.md→ clean. - Red → green, first commit: before implementation the two new files reported 4 failed / 6 passed. Two failures were genuine RED,
input_manifest.freshnessbeingundefined. The other two were fixture mistakes corrected alongside the implementation. - Red → green, review fixes: the updated freshness assertions failed against the old newest-only summary, 2 failed / 8 passed: the oldest bound was masked and per-document entries had no range. Both pass after the fix (10/10). The new exact Sites key assertion failed with the
brief-v3suffix (1 failed / 1 passed) and passes withbrief-v4. - Not verified: hosted deployment, live providers (no Music Flamingo, Perplexity, GPT/Astra, Chartmetric, Luminate or Spotify calls), production database, RPC behavior executed against PostgreSQL (read from migration SQL), a real two-track same-artist run, Sites workflow end to end.
Out of scope / known gaps
- Freshness is reported, not enforced. No age limit drops stale evidence yet.
- There is no evidence-kind private-input rule in the compiler. A
customer_assertionon a recipe topic for a request subject would be selected.company_onboardingintentionally keeps labeled assertions. The Sites handoff already drops customer assertions inreadSiteContextBrief, butplaylist_pitchhas no consumer-side filter. The checkpoint test's supporting-text exclusion proves topic scoping only and is named accordingly. - Source locations stay out of
textandinput_manifestonly.brief.documents[].sources[].urlis still returned and saved, which is pre-existing behavior that Sites relies on. - Old
save_briefkeys conflict after deploy. Replaying a pre-deploy key from another caller returns the documented conflict, and callers use a new key. The chat company-assessment Save mints a fresh random key after each preview. - Acceptance items 4–6 of #2130 are not advanced here. The live two-track evaluation, injected failure/resume with provider-expense reconciliation, and the human-review pass/fail dossier are gated on #2127/#2128/#2129 and approval.
- Other tickets own the remaining changes. This PR adds no DB migration or chat UI. Withdrawal logic belongs to #2124, and provider and coverage changes belong to #2127/#2128/#2129.
🤖 Generated with Claude Code