Per channel: fetch Atom XML → parse → drop Shorts → keep within window → dedup vs state file (new-since last-seen video id) → write one RECORD per new video. Stamps the roster name onto each record so it resolves against the registry (the Atom <title> drifts and would otherwise quarantine). One dead feed never kills the run.
Load inbox → resolve each record against registry.json (unknown → quarantined, not dropped) → classify against 4 relevance anchors. Any layer hits → relevant + theme-tagged.
Builds theme-grouped sections[] from clean video titles + theme tags — not the noisy description (that leaks promo lines). EN narrative. Optional --signal prepends the ViskaStrat book-positioning section as the lead.
The state file makes it a feed, not a dump.youtube-state.json holds last-seen video id per channel; each run emits only what's new. Cron it daily → genuinely incremental delivery.
Decoupled by file contract. The pipeline reads adapter output files, never imports the adapter. YouTube ships even while RSS/email is parked — exactly the current priority split.
The fragile seam is resolve (roster name ↔ registry name). Fixture tests pass even when it's wrong; only a live run catches it. That's why Step 1 stamps roster identity onto every record.