Label saved Context catalog valuations as scoped estimates

SI
11d ago
+248-2

Summary

Labels the reused Recoup catalog valuation as a scoped, workspace-private estimate in the content the Context collector saves. computeValuationBand gains an additive, frozen VALUATION_MODEL_ASSUMPTIONS export. Every field reads the band's own constants, including a new shared MIN_AGE_YEARS floor. No numeric value changes, and the marketing/chat mirror is untouched. A new SRP helper, describeCatalogValuationMethodology(modeled, ageSource), returns a structured descriptor with these fields:

  • currency
  • period basis: lifetime-average annual run rate, catalog age, floor flag, age source and measuredThrough: null
  • assumptions
  • methodology id, version and executor
  • source, with freshness unknown
  • revenueBasis: "modeled_estimate" and observedRevenue: { status: "not_collected" }
  • scope: "workspace_private" and shareable: false

collectContextCatalogValuation attaches it as estimate on every result, including not_measured, where the period fields are null. The enrichment reuse key moves to catalog-valuation-v2, so a previously saved result without the label is not reused as-is. This advances Done-when 1 (reuse the existing model, no duplicate calculation) and Done-when 2 (currency, period, assumptions, methodology and source are preserved, and the estimate is labeled separately from observed revenue).

After review, this PR does not add catalog_valuation to the company_onboarding brief recipe. Collection is still policy-blocked, so the topic would have shown as permanently missing in every live assessment. Recipe integration waits until collection is connected (#2123).

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

Merge sequencing

docs → database → api → app: this ticket slice is api only. No docs, database or app PR was opened, and no open PR is a dependency. The already-merged claim_context_enrichment accepts catalog_valuation only with estimate evidence (20260922060000 → 20260927090000). The module key is stored as unconstrained recipe_version text, so the v2 key needs no migration. There is no new HTTP/MCP action, dispatcher change, brief-recipe change, provider call, migration or dependency.

Verification

  • Ran: pnpm exec vitest run lib/context/enrichment/__tests__/catalogCollection.test.ts lib/context/providers/__tests__/catalogValuation.test.ts lib/context/providers/__tests__/describeCatalogValuationMethodology.test.ts lib/catalog → 34 files, 247 tests passed, 0 failed.
  • Ran: pnpm exec vitest run lib/context → 68 files passed, 8 skipped; 332 tests passed, 8 skipped, 0 failed. The baseline on origin/main was 67 files / 326 tests passed, 8 skipped. The 6 added tests are 3 in the new methodology file, 2 in catalogValuation and 1 in catalogCollection.
  • Ran: pnpm exec tsc --noEmit → 199 errors, none in lib/context or lib/catalog. All of them are pre-existing errors in untouched files (lib/tasks/__tests__, lib/trigger/__tests__, other __tests__ fixtures and lib/mcp/extension/registerRecoupExtension.ts), matching origin/main.
  • Ran: pnpm exec eslint on the 7 changed .ts files → clean. pnpm exec prettier --check on the same 7 files plus lib/context/README.md → clean.
  • Red → green (first commit): before implementation, describeCatalogValuationMethodology.test.ts failed to load, and the 2 new catalogValuation tests failed on the missing estimate.
  • Red → green (review fixes):
    • The new catalogCollection test failed while the key was still catalog-valuation-v1.
    • The 2 helper tests failed against the old signature (Cannot read properties of null (reading 'catalogAgeYears')).
    • Run before the fixes, those 3 failed and 12 passed. After the fixes, all pass.
  • The saved-content test now asserts on JSON.stringify(p_result.content), the form in which read_context_documents returns saved content, rather than a hand-written prose fixture.
  • Not verified: hosted deployment, live providers (no Spotify, Chartmetric or Luminate call was made), production or any hosted database, the durable Vercel Workflow path and MCP transport. States reached: implemented and fixture-tested only. Collection is not connected (dispatch stays blocked), not deployed and not live-verified.

Out of scope / known gaps

  • Done-when 3 is not advanced by this PR. No new permission, missing-credential or licensed-analytics test was added. The existing coverage is unchanged: catalogValuation "checks access before reading measurements" and catalogCollection "rechecks catalog access even on reuse" / "does not save when access is revoked".
  • estimate.scope and estimate.shareable are labels inside saved content. Nothing reads or enforces them. Reads stay owner-scoped through selectContextDocuments.
  • Connecting catalog_valuation dispatch: dispatchPlannedContextModule still dispatches only musicbrainz | mlc_recording | spotify_release, so catalog_valuation plan nodes remain blocked (#2123). No brief recipe selects catalog_valuation yet.
  • Luminate (lib/luminate/*) and Chartmetric (providers/searchChartmetricContext.ts) have no planning module or persisting Context collector. Connecting them needs a topic migration and a licensed-analytics scope policy.
  • Some labels appear twice: top-level scope/catalogAgeYears/ageFlooredToOneYear/ageSource and trace.currency already existed before estimate. A test asserts they agree with the estimate block, and both come from the same modeled value in one call. The existing fields were kept to avoid changing their shape for existing readers.
  • collectContextCatalogValuation.ts is 102 lines (100 on main) after the import and the one-line estimate call.
  • measuredThrough is null because the measurement aggregate carries no capture date. Freshness stays unknown.
  • #1864 (featured tracks, NPS band, MLC check, onchain lane) is untouched. This change does not alter the valuation model.

🤖 Generated with Claude Code