Label saved Context catalog valuations as scoped estimates
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"andobservedRevenue: { status: "not_collected" }scope: "workspace_private"andshareable: 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 incatalogValuationand 1 incatalogCollection. - Ran:
pnpm exec tsc --noEmit→ 199 errors, none inlib/contextorlib/catalog. All of them are pre-existing errors in untouched files (lib/tasks/__tests__,lib/trigger/__tests__, other__tests__fixtures andlib/mcp/extension/registerRecoupExtension.ts), matching origin/main. - Ran:
pnpm exec eslinton the 7 changed .ts files → clean.pnpm exec prettier --checkon the same 7 files pluslib/context/README.md→ clean. - Red → green (first commit): before implementation,
describeCatalogValuationMethodology.test.tsfailed to load, and the 2 newcatalogValuationtests failed on the missingestimate. - Red → green (review fixes):
- The new
catalogCollectiontest failed while the key was stillcatalog-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 new
- The saved-content test now asserts on
JSON.stringify(p_result.content), the form in whichread_context_documentsreturns 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" andcatalogCollection"rechecks catalog access even on reuse" / "does not save when access is revoked". estimate.scopeandestimate.shareableare labels inside saved content. Nothing reads or enforces them. Reads stay owner-scoped throughselectContextDocuments.- Connecting
catalog_valuationdispatch:dispatchPlannedContextModulestill dispatches onlymusicbrainz | mlc_recording | spotify_release, socatalog_valuationplan nodes remainblocked(#2123). No brief recipe selectscatalog_valuationyet. - 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/ageSourceandtrace.currencyalready existed beforeestimate. A test asserts they agree with theestimateblock, and both come from the samemodeledvalue in one call. The existing fields were kept to avoid changing their shape for existing readers. collectContextCatalogValuation.tsis 102 lines (100 on main) after the import and the one-lineestimatecall.measuredThroughisnullbecause the measurement aggregate carries no capture date. Freshness staysunknown.- #1864 (featured tracks, NPS band, MLC check, onchain lane) is untouched. This change does not alter the valuation model.
🤖 Generated with Claude Code