Run catalog_metadata on saved verified audio with explicit coverage

SI
15d ago
+1017-80

Summary

Runs the existing Music Flamingo catalog_metadata preset against the verified full-song audio already saved as an audio_source document, and persists the validated JSON as a separate catalog_metadata interpretation. Before this, only the preview-URL path (callContextMusicPreset) could run this preset, and nothing on the Context path kept the endpoint response, the validated record and the coverage labels separate.

This slice adds:

  • resolveSongEvidenceCoverage: labels the evidence full, preview, unverified, wrong_recording or missing, each with a schema-valid extent, identity and range. A caller-supplied language label is trimmed and passed through, never guessed; one too long to be a label is dropped rather than throwing.
  • parseCatalogMetadataResponse: turns raw text or an object into a validated record, or into invalid with a reason. It never throws, and it enforces the limits the preset prompt defines.
  • analyzeSavedContextCatalogMetadata and ContextStructuredOutputInvalid: invalid structured output fails the attempt instead of becoming an accepted document.
  • Two shared helpers, now used by both the existing summary/lyric analyzer and the new module: loadAcceptedContextAudioSource (looks up the accepted audio and checks the workspace storage key) and requestSavedContextAudioAnalysis (re-checks checksum and duration, signs a 15-minute URL, makes one production call).
  • scripts/analyzeContextCatalogMetadata.ts: an operator runner that mirrors the lyric runner. It makes a paid call and was not run.

The trace records the local preset prompt and params, a provenance note, the media manifest (label, range, checksum, no URL), the full endpoint response body, validation status, timing and a billing note. The signed URL and API key never enter rpc payloads.

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

Design notes (checked against the code)

  • The production /api/songs/analyze endpoint already applies the preset's parseJsonLike. For catalog_metadata it returns an object, or the raw string when parsing fails (processAnalyzeMusicRequest). So the parser accepts unknown, and the response schema is a loose object that keeps every key the endpoint returns, including preset.
  • The validator enforces the preset's own contract: a positive integer tempo_bpm; energy_level and danceability from 1 to 10; at most 3 subgenres, 5 mood, 3 lyrical_themes and 3 similar_artists; non-empty trimmed strings. Every field is optional. Output that breaks these limits fails the attempt.
  • Audio without waveform verification gets the label unverified (identity uncertain). That keeps it separate from wrong_recording, which requires positive evidence: a duration disagreement greater than 2 s (identity mismatch).
  • The analyzer refuses anything that isn't full evidence before it claims an attempt or calls the provider, so no paid call is made on unverified audio. A subject with no accepted audio is refused the same way.
  • presetVersion is the sha256 of the local preset prompt and params, and a test pins the formula. A changed recipe therefore makes a new paid call, while a compatible accepted result is reused.
  • Moving analyzeSavedContextAudio.ts onto the shared loader and request helper changes no behaviour: its existing tests, the full lib/sites suite (its only other caller is the Sites analyzer) and the lyric path all pass unchanged. No open api PR touches these files.

Merge sequencing

api only. No docs change: the OpenAPI topics enum already lists catalog_metadata, and no new action is exposed. No database change: claim_context_enrichment has accepted topic catalog_metadata since migrations 20260923070000 and 20260927090000. No app change. This PR depends on no open PR, and no other open api PR touches analyzeSavedContextAudio.ts, the new files or scripts/analyzeContext*.

Verification

All commands were run in the PR worktree at commit 3520bc4c (base origin/main c72887a).

  • Ran: pnpm exec vitest run lib/context: 71 files passed, 8 skipped; 355 tests passed, 8 skipped. The baseline on origin/main was 326 passed / 8 skipped, so this PR adds 29 tests with no regressions.
  • Ran: pnpm exec vitest run lib/sites (covers the Sites caller of analyzeSavedContextAudio): 50 files passed, 3 skipped; 226 tests passed, 4 skipped.
  • Ran: pnpm exec tsc --noEmit: 199 pre-existing error TS lines across 66 files, none in any file this PR touches and none in lib/context. An independent reviewer checked a copy without this PR's files and got the same 199.
  • Ran: pnpm exec eslint on the 12 changed or added .ts files: clean. pnpm exec prettier --check on those files plus AUDIO-SOURCE.md: all formatted.
  • Red → green: all three original test files first failed with Cannot find module, then passed 22/22. The review follow-up was also written test-first. These tests failed before the fix: out-of-limit values were accepted (energy 99, danceability -4, tempo -20, 7 moods, empty genre); the response was not trimmed; unverified audio was labeled wrong_recording; a 101-character language label threw; the trace dropped endpoint keys; the shared request helper did not exist. All pass now.
  • Behaviour covered by fixtures:
    • Invalid structured output calls fail_context_enrichment and never complete_context_enrichment.
    • Reuse makes no provider, signing or verification call.
    • Changed bytes or duration are rejected before signing or fetching.
    • Unverified audio is refused before any claim.
    • The signed URL and API key never appear in any rpc payload.
  • Not verified: hosted deployment; the live Music Flamingo / production analyze endpoint (no paid call was made, every provider response is a fixture, and the operator script was not run); production or any hosted database (no migration, no RPC executed); the hosted Sites path.

Out of scope / known gaps

  • Nothing dispatches this recipe over HTTP, MCP or a workflow, because paid dispatch is gated by #2123 and #2125. The module is server-only. Its only entry point is the operator script, which mirrors scripts/analyzeContextLyrics.ts.

  • Other parts of the ticket are not built here:

    • the versioned neutral song-summary topic (the existing song_summary / saved-audio-summary-themes-v3 path is unchanged)
    • audio and lyrics acquisition adapters (resolving accessible audio or lyrics through providers)
    • lyrics source, version and language attribution

    resolveSongEvidenceCoverage already provides the preview, missing and language outputs, but no acquisition path calls it yet; the analyzer only runs on saved full audio.

  • CE07 cost references: costUsd: null and costStatus: "unknown", exactly as in the sibling modules.

  • Failed-attempt traces are not persisted, including the endpoint response of an attempt whose output failed validation, because fail_context_enrichment(p_owner, p_attempt) accepts no reason. The thrown ContextStructuredOutputInvalid carries reason and rawLength for logs. Persisting the trace needs a database migration.

  • The existing shared parseJsonLike helper replaces every ' with ", so a Python-style dict whose text contains an apostrophe (for example don't) fails to parse and that attempt fails. This behaviour is pre-existing in lib/flamingo/presets/parseJsonLike.ts, which production also uses. It is not changed here.

  • The Sites hosted analyzer rejects non-string provider responses, so it cannot run JSON presets through this module as-is.

  • No Records data and no new dependencies.

🤖 Generated with Claude Code