ADR-133: metadata.json vs the processing manifest — purpose split and source of truth¶
- Status: Proposed
- Date: 2026-07-27
- Authors: Marko Dragoljevic
- Related RFCs: RFC-109
- Related ADRs: ADR-132 (manifest schema)
Context & Problem Statement¶
RFC-109 adds a per-episode processing manifest. metadata.json already exists and already carries
some of what the manifest wants (processing.config_snapshot, processing.stage_timings,
content.whisper_model, content.detected_hosts/guests). Two records with overlapping fields is a
recipe for drift and "which one is right?" — exactly what the operator flagged. This ADR draws the
line: what each file is for, which field's source of truth lives where, and how we migrate without
breaking readers.
Decision¶
Two records, two audiences¶
metadata.json= the PRODUCT record. What the episode is, for a consumer (the viewer, search, an agent, a downstream app): feed/episode metadata, the transcript path, the summary, the entities, and the resolved hosts/guests/speakers as a result. It answers "what's in this episode."<base>.manifest.json= the PROCESS record. How the episode was produced, for an operator/analyst: per-stage provenance, quality metrics, method versions, cost, rework flags (ADR-132). It answers "how well did we do, with what, and should we redo it."
A consumer never needs the manifest; an operator introspecting quality never needs to parse the transcript payload. The split is by audience and question, not by convenience.
The dividing line (source of truth per field)¶
| field / block | today | source of truth after this ADR |
|---|---|---|
| feed / episode metadata (title, guid, duration, links) | metadata |
metadata (product) |
transcript path, media, transcript_source |
metadata.content |
metadata (product) |
resolved detected_hosts / detected_guests / speakers |
metadata.content |
metadata (the result is product) |
| summary / GI / KG artifacts + their counts | metadata.{summary,gi,kg} |
metadata (product); the manifest keeps only the quality metrics (counts, gate drops) |
| actual ASR model + speech coverage + failover | .asr.json / nowhere |
manifest (stages.asr) |
config_snapshot |
metadata.processing |
manifest (provenance) |
stage_timings |
metadata.processing |
manifest (stages.*.duration_s) |
run_id / pipeline_version / method_version |
partial in metadata.processing |
manifest (provenance) |
| cost per stage / total | per-run cost JSONL only | manifest (roll-up); JSONL stays the event log |
| naming quality (detected-vs-named, snaps, method) | .speakers.diagnostics.json (deep) |
manifest (stages.naming summary); diagnostics stays the deep per-voice detail |
qa_flags / expectations |
metadata.content |
manifest (quality_flags); migrate + deprecate the metadata copy |
Rule of thumb: a result a consumer uses → metadata; a fact about the processing →
manifest. Where a signal has a deep form and a summary form (naming diagnostics, cost events), the
deep artifact stays (.speakers.diagnostics.json, cost JSONL) and the manifest holds the
queryable summary — the manifest is the index, the sidecars are the detail.
Migration (staged, non-breaking)¶
- Add, don't move (write-both). Introduce the manifest; the migrating fields
(
config_snapshot,stage_timings, actual model) are written to both the manifest (SoT) and their oldmetadatalocation for one release, so no reader breaks. - Back-reference.
metadata.processinggains amanifest_pathpointer, and its provenance fields are marked deprecated in the schema docstring (still populated). - Stop writing the duplicates on a
schema_versionboundary, once readers (viewer, search, any downstream) are confirmed off the deprecatedmetadata.processingprovenance fields — grep + fix referrers first (the doc-vs-code discipline). metadata.processingshrinks to a thin{ run_id, manifest_path, schema_version }link; all provenance/quality lives in the manifest.
No episode is rewritten just to migrate; the split takes effect as episodes are (re)processed, and a one-off backfill can populate manifests for the existing corpus if/when a query needs them.
Consequences¶
- One answer per question. "Which model produced this?" → manifest. "What's the summary?" → metadata. No field is authoritative in two places once migration completes.
- The viewer/search keep reading
metadataunchanged for product data; only tooling that wants provenance/quality reads the manifest — a clean, additive split. .speakers.diagnostics.jsonand the cost JSONL keep their role as the deep detail the manifest summarizes; nothing is deleted, duplication is removed at the summary level.
Non-Goals¶
- Not merging the two files — they have different audiences and lifecycles.
- Not a big-bang rewrite of the existing corpus's metadata — migration is by (re)processing + optional backfill.