Skip to content

Architecture Decision Records (ADRs)

Purpose

Architecture Decision Records (ADRs) capture the what and why of decisions taken after technical discussion (often in an RFC or issue). RFCs hold the full design, alternatives, and implementation journey; Accepted ADRs are the short, immutable record of what we chose.

How ADRs Work

  1. Immutable Records: Once an ADR is accepted, it remains unchanged unless superseded by a new ADR.
  2. Context Driven: They explain the trade-offs and rationale behind a decision (not the whole solution document).
  3. Reference for Developers: They provide onboarding context for why certain patterns (like the Provider Protocol) were chosen.

ADR Index

Code (last column): Yes = reflected in the codebase; Partial = incomplete or still rolling out; No = not started (including accepted ADRs waiting on implementation).

ADR Title Status Related RFC Description Code
ADR-001 Hybrid Concurrency Strategy Accepted RFC-001 IO-bound threading, sequential CPU/GPU tasks Yes
ADR-002 Security-First XML Processing Accepted RFC-002 Mandated use of defusedxml for RSS parsing Yes
ADR-003 Deterministic Feed Storage Accepted RFC-004 Hash-based output directory derivation Yes
ADR-004 Flat Filesystem Archive Layout Accepted RFC-004 Flat directory structure per feed run Yes
ADR-005 Lazy ML Dependency Loading Accepted RFC-005 Function-level imports for heavy ML libraries Yes
ADR-006 Context-Aware Model Selection Accepted RFC-010 Automatic English model promotion (.en) Yes
ADR-007 Universal Episode Identity Accepted RFC-011 GUID-first deterministic episode ID generation Yes
ADR-008 Database-Agnostic Metadata Schema Accepted RFC-011 Unified JSON format for SQL/NoSQL Yes
ADR-009 Privacy-First Local Summarization Accepted RFC-012 Local Transformers over Cloud APIs Yes
ADR-010 Hierarchical Summarization Pattern Accepted RFC-012 Map-reduce chunking for long transcripts Yes
ADR-011 Secure Credential Injection Accepted RFC-013 Environment-based secret management Yes
ADR-012 Provider-Agnostic Preprocessing Accepted RFC-013 Shared pre-inference cleaning pipeline Yes
ADR-013 Standalone Experiment Configuration Accepted RFC-015 Separation of research params from code Yes
ADR-014 Codified Comparison Baselines Accepted RFC-015, RFC-041 Objective delta measurement vs baseline artifacts Yes
ADR-015 Deep Provider Fingerprinting Accepted RFC-016 Hardware and environment tracking for reproducibility Yes
ADR-016 Typed Provider Parameter Models Accepted RFC-016 Pydantic validation for backend parameters Yes
ADR-017 Registered Preprocessing Profiles Accepted RFC-016 Versioned cleaning logic tracking Yes
ADR-018 Externalized Prompt Management Accepted RFC-017 Versioned Jinja2 templates in prompts/ Yes
ADR-019 Standardized Test Pyramid Accepted RFC-018, RFC-024 Strict unit/integration/e2e tiering Yes
ADR-020 Protocol-Based Provider Discovery Accepted RFC-021 Decoupling via PEP 544 Protocols Yes
ADR-021 Acceptance Test Tier as Final CI Gate Accepted RFC-023 Fourth test tier for README/documentation accuracy; runs last in CI Yes
ADR-022 Flaky Test Defense Accepted RFC-025 Automated retries and health reporting Yes
ADR-023 Public Operational Metrics Accepted RFC-026 Transparency via GitHub Pages dashboards Yes
ADR-024 Unified Provider Pattern Accepted RFC-029 Type-based unified provider classes Yes
ADR-025 Technology-Based Provider Naming Accepted RFC-029 Clear library-based option naming Yes
ADR-026 Per-Capability Provider Selection Accepted RFC-032, RFC-033, RFC-034, RFC-035, RFC-036, RFC-037 Independent provider choice per capability; partial-protocol providers allowed Yes
ADR-027 Unified Provider Metrics Contract Accepted - Standardized ProviderCallMetrics pattern for all providers Yes
ADR-028 Unified Retry Policy with Metrics Accepted - Centralized retry for LLM/API providers with backoff and metrics (not RSS/media HTTP; see CONFIGURATION — Download resilience) Yes
ADR-029 Grouped Dependency Automation Accepted RFC-038 Balanced Dependabot updates via grouping Yes
ADR-030 Periodic Module Coupling Analysis Accepted RFC-038 Nightly visualization of architecture health Yes
ADR-031 Mandatory Pre-Release Validation Accepted RFC-038 Standardized checklist script for releases Partial
ADR-032 Git Worktree-Based Development Accepted RFC-039 Parallel stable dev environments Yes
ADR-033 Stratified CI Execution Accepted RFC-039 Fast push checks vs. full PR validation Yes
ADR-034 Isolated Runtime Environments Accepted RFC-039 Independent venv per worktree Yes
ADR-035 Linear History via Squash-Merge Accepted RFC-039 Clean, revertible main branch history Yes
ADR-036 Standardized Pre-Provider Audio Stage Accepted RFC-040 Mandatory optimization before any transcription Yes
ADR-037 Content-Hash Based Audio Caching Accepted RFC-040 Shared optimized artifacts in .cache/ Yes
ADR-038 FFmpeg-First Audio Manipulation Accepted RFC-040 System-level performance for audio pipelines Yes
ADR-039 Speech-Optimized Codec (Opus) Accepted RFC-040 MP3 (libmp3lame @ 64 kbps) for preprocessed audio; Opus rejected (see ADR) Yes
ADR-040 Explicit Golden Dataset Versioning Accepted RFC-041 Approved, frozen ground truth data versions Yes
ADR-041 Multi-Tiered Benchmarking Strategy Accepted RFC-041 Fast PR smoke tests vs nightly full benchmarks Yes
ADR-042 Heuristic-Based Quality Gates Accepted RFC-041 Regex-based detection of common AI failure modes Yes
ADR-043 Hybrid MAP-REDUCE Summarization Accepted RFC-042 Compression (Classic) + Abstraction (Instruct LLM) Yes
ADR-044 Local LLM Backend Abstraction Accepted RFC-042 Support for llama.cpp, ollama, and transformers Yes
ADR-045 Strict REDUCE Prompt Contract Accepted RFC-042 Mandatory markdown structure for LLM outputs Yes
ADR-046 MPS Exclusive Mode for Apple Silicon Accepted RFC-042 Serialize GPU work on MPS to prevent memory contention; default on Yes
ADR-047 Proactive Metric Regression Alerting Accepted RFC-043 Automated PR comments and webhook notifications Partial
ADR-048 Centralized Model Registry Accepted RFC-044, RFC-029 Single source of truth for model architecture limits Yes
ADR-049 Materialization Boundary for Evaluation Inputs Accepted RFC-046 Preprocessing becomes dataset definition via materialization_id; chunking stays in run config Yes
ADR-050 Single Code Path for Evaluation and Application Accepted RFC-048 Eval and app share identical execution path; scorers are read-only observers Yes
ADR-051 Per-Episode JSON Artifacts with Logical Union Accepted RFC-049, RFC-055, RFC-061 Shard by episode (gi.json, kg.json); union at query time; optional materialization Yes
ADR-052 Separate GIL and KG Artifact Layers Accepted RFC-049, RFC-055 Independent schemas, feature flags, CLI namespaces, and evolution paths Yes
ADR-053 Grounding Contract for Evidence-Backed Insights Accepted RFC-049, RFC-050 Explicit grounded boolean, verbatim quotes with spans, evidence chain Yes
ADR-054 Relational Postgres Projection for GIL and KG Accepted RFC-051 Files canonical, Postgres is derived; separate GIL/KG tables; provenance on every row No
ADR-055 Adaptive Summarization Routing Proposed RFC-053 Rule-based routing with episode profiling for summarization strategies No
ADR-056 Composable E2E Mock Response Strategy Superseded RFC-054 Centralized response-profile router not built; per-provider mock clients + per-concern unit suites shipped instead. RFC-054 closed via redirect. Yes (different shape)
ADR-057 AutoResearch Thin Harness with Credential Isolation Accepted RFC-057 Thin control layer reusing existing eval; immutable score.py; AUTORESEARCH_* credential vars Yes
ADR-058 Additive pyannote Diarization Accepted RFC-058 pyannote additive second pass; [ml]/[dev] deps; default on for local Whisper Partial
ADR-059 Confidence-Scored Multi-Signal Commercial Detection Accepted RFC-060 Confidence-scored candidates; Phase 1 (#486) + Phase 2 diarization signals (#488) both shipped Yes
ADR-060 VectorStore Protocol with Backend Abstraction Superseded by ADR-099 RFC-061 PEP 544 protocol decoupling FAISS (Phase 1) from Qdrant (Phase 2) Yes
ADR-061 FAISS Phase 1 with Post-Filter Metadata Strategy Superseded by ADR-099 RFC-061 Over-fetch + post-filter for CLI-scale; auto index type selection Yes
ADR-062 Sentence-Boundary Transcript Chunking Accepted RFC-061 Regex sentence split, configurable target/overlap tokens, timestamp interpolation Yes
ADR-063 Transparent Semantic Upgrade for gi explore Accepted RFC-061, RFC-050 Auto-detect vector index; semantic if available, substring fallback if not Yes
ADR-064 Canonical Server Layer with Feature-Flagged Route Groups Accepted RFC-062 server/ module with podcast serve CLI; viewer routes v2.6, platform routes v2.7 Yes
ADR-065 Vue 3 + Vite + Cytoscape.js Frontend Stack Accepted RFC-062 Unified frontend stack for viewer and future platform UI Yes
ADR-066 Playwright for UI End-to-End Testing Accepted RFC-062 Browser regression testing; extends ADR-020 test pyramid with UI layer Yes
ADR-067 Pegasus/LED Retirement for Podcast Content Accepted RFC-057 GSG pretraining mismatch → near-duplicate chunks → LED ngram exhaustion; reserved for news content type Yes
ADR-068 BART+LED as Local ML Production Baseline Accepted RFC-057 Autoresearch sweep: +4.26% ROUGE-L over dev baseline (18.82%); 2 params accepted (max_new_tokens=550, num_beams=6) Yes
ADR-069 Hybrid ML Pipeline as Primary Production Direction Accepted RFC-057, RFC-042 BART MAP + Llama 3.2:3b REDUCE at 23.1% ROUGE-L; closes 70% of cloud quality gap; temp=0.5, top_p=1.0 Yes
ADR-070 BART-base as Hybrid MAP Stage Accepted RFC-057 BART beats LongT5 as MAP (21.2% vs 20.8%); pretraining alignment > context window size Yes
ADR-071 Four-Tier Summarization Strategy Accepted RFC-057 ML Dev / ML Prod / LLM Local / LLM Cloud — direct Llama 3.2:3b beats hybrid (24.3% vs 23.7%, 2x faster) Yes
ADR-072 Llama 3.2:3b as Tier 3 Local LLM Accepted RFC-057 3B beats 7-12B models — instruction-following > size; temp=0.3 direct, temp=0.5 hybrid; 26.4% ROUGE-L @ 7.5s/ep Yes
ADR-073 RFC-057 Autoresearch Loop — Closure and Final State Accepted RFC-057 Closes RFC-057; documents Track A/B outcomes, silver refs, 72-config matrix, production defaults Yes
ADR-074 Multi-Feed Corpus Parent Layout and Machine-Readable Manifest Accepted RFC-063 Layout A corpus parent; unified discovery; corpus_manifest.json / optional summaries as operational artifacts Yes
ADR-075 Frozen YAML Performance Profiles for Release Resource Baselines Accepted RFC-064 data/profiles/*.yaml + freeze/diff scripts; resource cost sibling to quality baselines Yes
ADR-076 Streamlit for Operator Run Comparison and Performance Views Accepted RFC-047, RFC-066 Eval / Performance UI stays in tools/run_compare/; Vue viewer stays corpus-first Yes
ADR-077 Local Ollama Model Selection Accepted Default Ollama models per profile and tier Yes
ADR-078 GIL Evidence Stack Bundling — Per-Provider Champion Modes Accepted bundled_ab default; Mistral=bundled_b_only; Ollama bundled-only (staged unviable on local) Yes
ADR-079 OpenTofu for Always-On Hosting IaC Accepted RFC-082 OpenTofu + hcloud + tailscale providers; infra/tofu entry Yes
ADR-080 OpenTofu State Encrypted In-Repo (sops + age) Accepted RFC-082 Committed .enc state; TFSTATE_AGE_KEY in CI Yes
ADR-081 Drill OpenTofu Workspace and Tailscale ACL Ownership Accepted RFC-082 Workspace drill, HCLOUD_TOKEN_DRILL, prod-only tailscale_acl Yes
ADR-082 GitOps App Deploy via stack-test and GitHub Actions Accepted RFC-082 Stack-test gate + publish + deploy-prod; workflow_run target; infra apply manual Yes
ADR-083 Tailscale as Private Ingress for Always-On VPS Accepted RFC-082 App on tailnet; tag:gha-deployer SSH path Yes
ADR-084 Full-Stack Docker Compose Topology (API, Viewer, Pipeline) Accepted RFC-079 compose/docker-compose.stack.yml; shared volume; optional Docker job exec Yes
ADR-085 Ephemeral Stack-Test Integration Gate on Main Accepted RFC-078, RFC-079 Compose overlay + Playwright tests/stack-test/; distinct from ADR-021 Yes
ADR-086 Canonical Identity Layer and Per-Episode bridge.json Cross-Layer Join Accepted RFC-072 CIL ids + bridge.json seam; GIL or KG stay separate (ADR-052) Yes
ADR-087 Autoresearch Track A v2 — Dev or Held-Out Split and Judging Accepted RFC-073, RFC-057 Disjoint held-out; fraction contestation; Efficiency rubric; seed wiring Yes
ADR-088 macOS Local CI Process Safety for ML Workloads Accepted RFC-074 No parse-time ML probes; cleanup or zombie checks; agent no-pileup rules Yes
ADR-089 Prod Failover Orchestrator Separate from DR Drill Accepted RFC-083 Own workflow family; reuse drill workspace/secrets; no auto-destroy; GitHub #764 Yes
ADR-090 Prod Failover — DNS-First Cutover on Tailnet Accepted RFC-083 Canonical hostname DNS flip primary; floating IP optional Yes
ADR-091 Prod Failover — GHA Triggers and Gates Accepted RFC-083 Manual cutover/failback/teardown; freeze prod schedules; spare schedules off after restore Yes
ADR-092 Corpus Snapshot Backup Manifest and Newest-Compatible Restore Default Accepted RFC-084 snapshot.manifest.json; dual placement; newest-compatible default; fail closed; GitHub #763 Yes
ADR-093 Canonical Stack Contract Versus Environment Adapters Accepted RFC-082 One topology/health/stack-test discipline; adapters for transport/secrets only; steady vs restore playbooks separate; GitHub #762 Yes
ADR-094 Graph Handoff Orchestrator — 8-State FSM with Envelope and Generation Tokens Accepted RFC-085 Single FSM authoritative across 13 entry points; envelope contract; generation supersession; viewer-side stuck detection + error strip Yes
ADR-095 Viewer Test Pyramid — Three Tiers (Mocks, Production-Shaped, Real Corpus) Accepted RFC-086 Tier-1 mocked, Tier-2 production-shaped fixtures, Tier-3 real-corpus matrix; make ci-ui-validation; real-bug-first matrix-row rule Yes
ADR-096 DGX Spark in prod — primary-with-fallback contract Accepted RFC-089 Prod may use DGX when each stage names a cloud fallback; v1 stage is Whisper Partial
ADR-097 GitHub Actions self-hosted runner policy on public repos Accepted RFC-089 Ephemeral runner + fork approval + workflow allowlist before dgx-spark Partial
ADR-098 Embedding provider as a profile axis, supersede RFC-089 §D4 Accepted RFC-089 (§D4 superseded) vector_embedding_provider literal (sentence_transformers | ollama); shim deleted; default stays sentence_transformers everywhere — empirical A/B showed MiniLM beats nomic under fair chunking Partial
ADR-099 LanceDB-first single-index search; retire FAISS Accepted RFC-090 Serving ran LanceDB like FAISS (open-per-query, over-fetch with vectors, runtime fallback); LanceDB already holds all content so FAISS is a redundant copy. Make LanceDB the single index — opened once, native hybrid, select() projection — and remove FAISS (build/serve/config); rebuild, no migration. Shipped #1010 (faiss_store.py deleted); Stage-2 native hybrid reverted per #1205 Yes
ADR-100 Response shape guardrails for cloud LLM providers Accepted Snapshot envelope diffs on every cloud LLM SDK bump; quarantine drift before it surfaces as silent extraction regressions Yes
ADR-101 Drop legacy KG + GI shape support Accepted RFC-097 (chunk 9) Skip the 2–4 week bake gate from RFC-097 §161; flip KG v2.0 / GI v3.0 validators to strict immediately. No external consumers, all corpora regenerable Yes
ADR-102 _retro_audit marker for in-place artifact mutation Accepted RFC-097 Canonical shape for the audit trail when in-place mutation is the only viable path for published artifacts (eval runs, prod corpora). Required keys + summary file format + migration-script grandfathering Yes
ADR-103 Deterministic connectivity under LLM-free profiles Accepted RFC-094 Closes #1058 — three deterministic post-passes (KG ORG nodes via spaCy NER, GI MENTIONS_ORG via the existing NER pass, corpus-level Topic clustering via sentence-transformers) plus a multi-show pre-seeded fixture so airgapped CI carries real connectivity data without ever calling a cloud LLM Yes
ADR-104 Enrichment-layer boundary vs KG-direct connectivity Accepted RFC-088, RFC-097 Amends RFC-088 §Key Decision #1: "Enrichers never modify core artifacts produced by core pipeline stages." RFC-097 chunk 9's KG-direct RELATED_TO is part of the core pipeline (conforms to KG v2.0; runs in workflow/orchestration.py); enrichments live under enrichments/ and serve ranking / scoring / UI consumption. Rubric: traversal → core, ranking → enrichments. Same signal can have two outputs (one each), no redundancy. Doc-only
ADR-105 Response-shape guardrails for self-deployed services Accepted The self-hosted sibling of ADR-100 (cloud): snapshot envelope diffs on self-hosted service bumps. Renumbered 2026-07-07 from a duplicate ADR-099 (the other ADR-099 is LanceDB-first search). Yes
ADR-106 transformers v5 upgrade + ML backend unification Accepted RFC-042 #382 (5d504f3d): pin transformers>=5.0.0; collapse the two duplicated HF-seq2seq loading idioms (summarizer.SummaryModel + hybrid_ml_provider.TransformersReduceBackend) into shared HFSeq2SeqBackend + HFEvidenceBackend. Retroactive capture. Yes
ADR-107 Ingestion is the pipeline — drop the standalone ingest primitive Accepted PRD-037 #1069: the single-feed pipeline is ingestion; drop the ingest verb + IngestPolicy. Durable outcome — enrichment made a consistent peer of the pipeline across CLI / docker / scheduler / UI / auto-chain. Yes
ADR-108 Reimagine the NLI enrichers → topic_consensus (activated) + retire stance scoring for a CIL timeline Accepted RFC-088, RFC-103 nli_contradiction/stance_disagreement hit 0% precision. nli_contradictiontopic_consensus, activated at precision 0.91 via a composite of embedding cosine + low NLI contradiction (symmetric entailment failed on real data). Stance-over-time was retired as an enricher — its signal is absent on factual insights — and is now a read-time CIL conversation/position timeline coloured by the deterministic insight_sentiment (VADER). Activated + retired
ADR-109 Per-episode GI quality telemetry — make silent failures loud Proposed ADR-053 Nine defects in one session all produced plausible output and reported success. GI quality is aggregated per RUN, so 30 broken episodes hide behind a healthy average — exactly how prod-v2 accumulated 125 unusable transcripts unnoticed. Emit per-episode quality + failure flags (stub_fallback, zero_quotes, gate_failed_open, truncation_salvaged, insights_at_ceiling), a per-episode log line, Prometheus gauges, and a run-level quality summary. Must catch the known failures on 10-20 episodes before the 100-episode reprocess. Partial
ADR-110 Ask who speaks AFTER we can hear them Accepted #876, #1169 The pipeline asks "who speaks?" before the audio is even downloadeddetect_speakers(title, description, known_hosts) cannot take a transcript. So an LLM shown only show notes returns the people they mention (that is how Elon Musk, named only as the man suing OpenAI, became a speaker), and corroborate_guests then checks that guess against the same show notes it was guessed from — circular. Measured on the prod detector (gemini, 50 eps): 24.0% of talk unattributed, 70 proposed names deleted, 69 of them whole — including Rob Armstrong, co-host of FT Unhedged, and Planet Money's entire newsroom. Fix: keep metadata detection as a cheap proposal; resolve identity after diarization against each voice's own turns; delete positional talk-time painting (the actual invention mechanism); keep the regex as the no-LLM/airgapped path. Yes
ADR-111 The eval and the pipeline are two toys built from ONE set of bricks Proposed ADR-110, #1169 The eval does not USE the pipeline, it RE-IMPLEMENTS it — so every brick is wired twice and each miss fails silently, because the missing value takes a default and the default is "off". Found in one day: the eval never passed transcript_segments (every voice gate dead in eval, live in prod); gi_max_insights 50 in eval vs 12 in prod; the value gate on in eval, off in production; the evidence auto-align re-implemented because model_copy skips validators (one run: 513 insights, ZERO quotes). Root cause is the allowlist: a Config built by hand-copying keys in THREE places (cli.py ~32 names, eval_gi_kg_runtime.py ~23 p.get()s, the experiment YAML) — a key nobody copies does not error, it just turns a feature off. Fix: ONE build_episode_artifact(cfg, inputs) both callers use, ONE config resolver that runs Config's validators, and delete every allowlist. No
ADR-112 The registry is the source of truth for every tuned parameter Accepted ADR-111, #939 The registry governed WHICH MODEL runs each stage but not the TUNED PARAMS, so a value came from four places at once and they disagreed silently: gi_max_insights was 12 (registry) / 50 (profile) / 20 (Config default) simultaneously; the researched v3 tuning (provider_chunked_gated_v3, temperature pin and all) was an orphan no preset pointed at; gi_insight_temperature was recorded and plumbed nowhere, so every bake-off arm sampled at 0.3 while its YAML said 0.0. Fix: REGISTRY_GOVERNED_FIELDS (one list), make profiles-materialize (profiles are a generated VIEW), Config defaults bound to the registry, the value-gate judge DERIVED vendor-disjoint (#939) and OFF where there is no LLM, and a closed-world integrity check that fails the build for any tunable that is neither governed nor explicitly exempt. Yes
ADR-113 Two fuses and an LLM-aware resilience layer Accepted ADR-100, ADR-112 The LLM path had weaker safety than RSS and too-coarse retries. Three failures exposed it: gpt-5.5 made ~3,500 SUCCESSFUL calls on one episode (a failure breaker can't catch a success storm); the Anthropic spend-cap 400 was treated as retryable (looped/crashed instead of stopping); gemini-2.5-flash-lite 503s under load and got hammered by uniform hardcoded retries. Fix: a count fuse (per-episode/per-run call ceiling, hard-abort, all providers incl ollama/vllm), a terminal fuse (out-of-money/access = clean hard stop, not retry), an LLM-aware error taxonomy (retryable-overload / rate-limit / TERMINAL / non-retryable, TERMINAL checked first), and per-model resilience profiles (flash-lite conservative) resolved at the retry_with_metrics chokepoint with no call-site churn. Mock-server tests mirror RSS. Observability + reset shipped in the same branch: GET /api/resilience + POST /api/ops/resilience/reset (resilience_status.py) and an Ops Resilience panel in the viewer. Yes
ADR-114 Shared multi-tenant public edge (host-level Caddy + Let's Encrypt) Accepted RFC-082, RFC-087 This repo owns the VPS edge: one vanilla Caddy engine (import /etc/caddy/sites/*.caddy) + firewall 80/443, each public app drops its own vhost + reserved loopback port. Shared HSTS/logging snippets defined once; validate-before-reload isolates tenant failures; tenants (incl. orrery) leave cleanly. Mirrors the tailscale-serve ownership split. Engine for #1158 / orrery#381 Doc-only
ADR-115 Multi-tenant secret delivery — sops/age at rest, tmpfs + file mounts at runtime Accepted Replace plaintext .env: secrets sops-encrypted per tenant in its own repo (to a shared VPS age recipient), decrypted at deploy into /run/secrets/<tenant>/ (tmpfs, never on disk), mounted as files not env. Infra owns the age key + decrypt helper + sudoers; each tenant brings its own secrets.enc.yaml. Mirrors ADR-114 ownership split; extends ADR-011. OpenBao deferred (T-08) Doc-only
ADR-116 Privilege-split API — public-api (no sock/keys) vs control-api (tailnet), enqueue→drain Accepted kg/gi web app going public needs RBAC across /api/* (none today) + keeping docker.sock/keys off the public plane. Split backend by privilege: public-api (reads + role-gated admin + /api/app/*, enqueues jobs) vs control-api (tailnet, sock/keys, drains the file-locked job registry). One image, two modes. Sequencing: player public first, kg/gi stays tailnet until hardened. #1161 Doc-only
ADR-117 Multi-tenant observability — common box/edge plane + per-tenant app telemetry, GitOps Accepted o11y follows the ADR-114/115 ownership split: infra owns the pipeline + GitOps sync + host/edge/security detection (T-11); each tenant owns its app metrics/logs/dashboards/alerts + Sentry project. tenant label routes each tenant to a Grafana folder OR its own account (§3a per-tenant choice; Sentry per-account by DSN default); common=infra's account always. Cases: common/orrery/podcast-player/podcast-operator. Config-as-code + API sync scripts (no new dep). Trade-off: separate accounts = stronger isolation but harder cross-account correlation. Goal-1 builds the common plane only. Doc-only
ADR-118 Cloudflare additive front — real-IP recovery + origin lock Accepted ADR-114, ADR-115 D4: accept Cloudflare free as the additive WAF/DDoS/rate-limit front (ADR-114 was designed for it). Origin prep: Caddy trusted_proxies for CF real-IP; fail2ban re-keyed to client_ip (post-CF remote_ip is a CF edge — banning it = outage); optional cloudflare_origin_lock TF firewall narrows :443 to CF ranges (:80 stays open for ACME). Keep Let's Encrypt; CF Full(strict). Operator owns CF account/DNS + the lock flip. T-05. Doc-only
ADR-119 Vendor-neutral observability — canonical event emission + pluggable shipping Proposed ADR-117 App emits two open formats (Prometheus /metrics + canonical JSONL via emit_event); shipping is a pluggable adapter (Alloy → VictoriaLogs/Metrics = reference sink, swappable by config). Envelope {ts,schema,event_type,...}; sink="log" (stdout, pipeline events) / sink="file" (persistent corpus JSONL, serve-side). Existing emitters become thin wrappers; removes the "logs go to Loki" coupling. Doc-only
ADR-122 Self-hosted-model resilience policy — backoff→trip→hold, serve vs reprocess Accepted ADR-088 #1253. The self-hosted-model family (DGX whisper, DGX pyannote diarize, MOSS) gets a shared resilience policy selectable by run context: serve = availability (fail fast, fall over — unchanged); reprocess = consistency (timeout → exponential backoff + retry the chosen model, fuse trips only after N, on-open pause-and-probe, no cross-model fallover). MOSS had zero resilience (bare POST). Prior art: RFC-106/#1198, #954, #876. Policy numbers config-tunable (proposed defaults in the ADR). In progress
ADR-134 Provider-specific speaker-labeling strategies Accepted #876/#1170/#1190. There is no diarizer-agnostic labeling: labeling is coupled to the diarizer's clustering footprint (Deepgram merges cold-opens into the host cluster; pyannote community-1 splits hosts/guests and isolates promo readers). Cluster-shape-sensitive labeling (host-candidate eligibility, montage/split handling, ad/recorded tuning, canonicalization gating) becomes a DiarizationLabelingStrategy selected by diarization_provider, over a shared core (invariants N1/"wrong>unnamed", naming precedence, name primitives, LLM path). Overfitting to a diarizer is fine when explicit and contained in its strategy, not smuggled into "generic" functions. Local community-1 = product strategy; Deepgram = legacy/frozen. In progress
ADR-127 Search-index insight_type column + reindex-on-upgrade Accepted Search v3 §S8. Accepted
ADR-130 Provider-agnostic speaker-name recovery from episode metadata Accepted ADR-134, ADR-123 #876/#1190. Every ASR mistranscribes proper nouns (OpenAI "Kevin Russo", turbo "Kevin Roos"/"David Duvino"); the correct spelling is in the episode metadata (100% have title+desc, 93% name a guest). Host canonicalization already snaps mangled names to known_hosts shared-core for all providers (it fixed OpenAI's manglings on v2.2). Add the symmetric guest half — snap mangled names to stated guests (metadata_named/detected_guests) by the same fuzzy rule (with exact-match + host-candidate guards) — plus fix the empty-cached_hosts cause: gemini.detect_hosts short-circuits on org RSS authors and never reads the description, so detect_feed_hosts_and_patterns falls back to the deterministic detect_hosts_from_feed (reads the description statement, as relabel does). Reference-bounded (never invents a name). Not turbo-specific. Measured attribution-aware: host fallback recovers 6/9 feeds; guest snap makes 10 respells, 0 cross-person mislabels. Supersedes a reverted dead-code fix (feed_hosts threading). Accepted
ADR-131 Speech-normalized coverage for the transcription quality gate Proposed ADR-123 #1178/#1258. ADR-123's coverage gate divides transcribed-segment seconds by total audio, so music/ads/silence read as "dropped speech" and trip the turbo→large-v3 failover on the wrong signal. Measured: 11/90 v2.3 episodes fall below 0.85 on the winning transcript (non-speech, e.g. "Move Over Humans" 67.4%), while the real turbo failure (100-min Ezra Klein, 29.9% WER) is a distinct low-speech-coverage signal. Fix: gate on Σseg / speech_seconds (non-speech removed). Fix: gate on Σseg / speech_seconds (non-speech removed). Chosen approach C — reuse the diarizer's speech regions (provider-agnostic DiarizationResult; raw-coverage fallback when diarization is off). Validated on WSJ: raw 76–89% but speech coverage 97–105%. Threshold recalibrated on speech coverage. Accepted
ADR-132 Per-episode processing manifest — schema, stage ownership, versioning Proposed RFC-109, ADR-133, ADR-131 The observability contract: a per-episode <base>.manifest.json sidecar (generalizing ADR-131's .asr.json) + a flattened corpus manifest.jsonl ledger. Fixes the schema (per-stage {ran, method, method_version, model, duration_s, cost_usd, metrics, warnings} + quality_flags + cost_usd_total) and two conventions: stage ownership (each stage writes its own block from its result, never from cfg — the anti-whisper_model-rot rule) and layered versioning (git_sha+dirty ground-truth backstop, pipeline_version for stage-graph composition, per-stage method_version as the reprocess query key). No
ADR-133 metadata.json vs the processing manifest — purpose split and source of truth Proposed RFC-109, ADR-132 Resolves the duplication the operator flagged. Two records, two audiences: metadata.json = the product record (what the episode is — transcript, summary, entities, resolved hosts/guests a consumer reads); the manifest = the process/observability record (how it was produced — provenance, quality, versions, cost). Draws the SoT line per field (provenance config_snapshot/stage_timings/actual-model migrate to the manifest; product fields stay), and a staged write-both→back-reference→deprecate migration so no reader breaks. No
ADR-135 v2.4 — GI route-and-tag insights + KG Voice node (one coordinated schema bump) Accepted #1191, #1220, #1193; ADR-131 The v2.4 reprocess arc = two artifact-shape changes that land in ONE rebuild. #1191: the GI pipeline ranks + tags but never truncates — add rank/tier/routing_tag/salience to the Insight (preserve the value gate's already-computed tier instead of collapsing it to keep/drop), remove the pipeline cutoff, "first N" becomes view-time; the SURFACE/CONNECT/DROP rubric research is a DEFERRED follow-up (field stored now, classifier refined later — no second rebuild). #1220: unresolved diarization voices (SPEAKER_NN) become a Voice node in BOTH KG and GI (repurposing gi.schema's reserved Speaker type), branched off the shared graph_id_utils placeholder check and excluded from PERSON_ORG_NODE_TYPES, so Person queries never see them by construction; #1193's read guards become legacy defense. One coordinated bump (GI 3.0→3.1, KG 2.0→2.1) + two idempotent migrations + fixture regen. No
ADR-136 Per-episode context digest (.context.json) — a reprocess-free consolidated content surface Accepted v2.4; #1220; ADR-133, ADR-132 A new per-episode .context.json sidecar: a flat, denormalized, reprocess-free digest of "what the episode is about", written after GI/KG by rolling up what they already produce — distinct from .manifest.json (provenance/how-processed) and .metadata.json (product record). Phase 1 consolidates ONLY existing outputs, zero new LLM: basic feed/episode info + episode summary + entities denormalized from the graph (people = Person minus bare-speaker Voice, companies = Organization, topics = Topic, voices = count of unresolved Voice). Phase 2+ (deferred, new LLM): glossary/terminology (top translation value), distinct concepts, jokes. Backfillable by deterministic migration. Motivated by the sidecar-extraction gap — reading the clean speaker rate required walking .gi.json. No
ADR-137 LLM host/guest role determination on the existing voice-resolution call Accepted v2.3.1/v2.3.2 host/guest gaps; F1/F2 seat-fill Extend the existing voice-resolution LLM call to ALSO return host/guest role — one call, flat cost, no new round-trip or provider method — feeding it three role-bearing inputs it doesn't see today. No
ADR-138 Ad-cleaning as a standalone pipeline stage Proposed labeling mis-map incident (prod-v2.4, John Kim) A standalone CLEANING stage after transcription and before diarization/labeling: excise ad regions ONCE → one durable artifact (clean transcript + clean segments + ad-map) that every downstream text consumer reads. No
ADR-139 Text normalization contract (match / display / raw) Proposed labeling failures on The Daily / WSJ / lowercase-turbo A pipeline-wide text-normalization contract as a core primitive, applied everywhere text is matched or recognized (not just the labeling failures that surfaced it); three parts — match / display / raw. No
ADR-140 Versioned labeling profiles (a knob-bundle, not a code fork) Proposed ADR-017, ADR-134 A versioned LabelingProfile = a frozen knob-bundle. Labeling is (diarization, transcript, metadata, LLM) → roster, a stateful LLM-in-loop pipeline (not a pure function), so ADR-017's swap-the-whole-function registry doesn't fit. No
ADR-141 Operator = two planes (control + public); keep the split, fix the naming Accepted ADR-116 Keep the two-plane control/public split (the security wall — network + volume isolation between the open internet and host-root/provider-keys, NOT RBAC); do NOT consolidate; fix the naming + document it. No
ADR-142 LiteLLM gateway on the prod VPS (one gateway per failure domain) Accepted #1357 (epic), #1356 Prod runs its OWN LiteLLM instance on the VPS (loopback, zero-hop from the app) so inference survives any homelab/ISP outage; #1356's consumption model is unchanged, only the base URL becomes the local instance. No
ADR-143 Corpus reprocess methodology — single-variable validation, reprocess-once economics, judge-panel parity gate Accepted #1335 (v2.2), #1355 (v2.4), #1189, #630; ADR-123, ADR-135 The durable methodology governing every corpus-producing change (was only in the ephemeral wip plan). Single-variable validation: one producing variable per version, accept on parity-vs-prior (the prior corpus is the accepted baseline), applied v2.1→v2.5. Reprocess-once economics: lock artifact-shape / input-text / model before the scale run or rebuild twice. Cost-aware measurement: deterministic layers (WER, roster parity) are the primary verdict; noisy GI/KG/summary get a cheap rediarize_only noise floor. Judge-panel parity is the LLM-swap ship gate (judge-panel ONLY): disjoint-vendor, scalar, </think>-stripped; human-GT (#1189) is the separate reprocess-acceptance gate, not the parity gate. No

| ADR-147 | First-class vllm provider — real model ids + fail-closed served-model verification | Accepted | ADR-044, ADR-143, ADR-124; agentic-ai-homelab infra/vllm/autoresearch/ | DGX-served models were bolted onto the openai provider with model="autoresearch" (a --served-model-name alias), so a profile could not name what produced the corpus — breaking ADR-143 reproducibility. Add a distinct vllm provider (sibling of openai, not a subclass; shared OpenAI-compatible transport base), name real HF model ids in the registry, and assert served-model == expected (fail-closed). openai is reserved for OpenAI-native models; naming (ner) becomes provider-symmetric too. Homelab serves under the real id in lockstep. | No |

Gap analysis

Counts (reconcile when adding ADRs): 144 files under docs/adr/ADR-*.md (ADR-001–ADR-147; numbering has historical gaps; ADR-134 = #1273 speaker-labeling renumber, ADR-135 = v2.4, ADR-137–142 = host/guest role, ad-cleaning stage, text-normalization contract, versioned labeling profiles, operator two-plane, LiteLLM prod gateway; ADR-143 = corpus reprocess methodology; ADR-147 = first-class vllm provider (real model ids + fail-closed served-model verification); ADR-136 = per-episode context digest). ADR-099 was a duplicate — the response-shape-guardrails file was renumbered to ADR-105 (2026-07-07); ADR-106/107/108 added the same day. ADR-114–119 are the infra/edge programme (shared Caddy edge, secrets, privilege-split, observability, Cloudflare, vendor-neutral event emission). From the index table: 3 Proposed (ADR-055 tied to Draft RFC-053, plus ADR-109/111; the infra programme ADR-114–119 is Accepted; ADR-104 was promoted to Accepted with RFC-088 chunk 8, and ADR-110 was promoted to Accepted — shipped default-on this branch), 3 Superseded (ADR-056, RFC-054 shipped in different shape — per-provider mocks rather than the centralized router this ADR drafted; ADR-060 + ADR-061, FAISS retired for LanceDB — superseded by ADR-099), 2 Accepted with Code = No (ADR-054, tied to Draft RFC-051 Postgres projection; ADR-147, first-class vllm provider — ratified, implementation pending), 3 Accepted with Code = Partial (ADR-031, ADR-047, ADR-058). Accepted means ratified, not necessarily shipped.

When to extract a new ADR

Use an ADR when one or more of these hold; otherwise an RFC + normative doc (API guide, docs/api/*.md, UXS) is usually enough.

ADR type When to extract Recent examples
Closure / program outcome A large RFC program ends; you need an immutable summary. ADR-073 closes RFC-057
Empirical production defaults Benchmarks change default models/tiers you must freeze for onboarding. ADR-067ADR-072
Stack & ownership boundary Who owns HTTP, which frontend stack, which UI E2E runner. ADR-064ADR-066
Heavy optional dependencies An extra bloats install or splits CUDA/CPU paths; defaults must not pay the cost. ADR-058 (amended: bundled in [ml]/[dev])
Cross-cutting protocol / contract Multiple subsystems share the same interface. ADR-060, ADR-053, ADR-051
Process / CI philosophy A policy decision that outlives one RFC. ADR-021

When not to add an ADR

  • Viewer milestones that do not change stack (e.g. RFC-069) — RFC + feature UXS (e.g. UXS-004) + UXS-001 hub + E2E map suffice.
  • Single-route APIs for the viewer with schema in code + tests (e.g. RFC-068) — Server Guide + tests suffice.
  • Operational tooling without architectural boundary moves (e.g. RFC-065) — RFC-first.
  • Frozen artifact workflows (e.g. RFC-064); profile YAML baselines are covered by ADR-075.

ADRs by implementation state

Proposed

ADR Primary RFC Note
ADR-055 RFC-053 No episode profiling / routing in pipeline yet
ADR-104 RFC-088, RFC-097 Settles enrichment-layer ↔ KG-direct boundary (RFC-097 chunk 9 RELATED_TO is core, not enrichment violation). Doc-only; promoted to Accepted in RFC-088 implementation chunk 8 (#1110).
ADR-109 ADR-053 Per-episode GI quality telemetry; must catch known failures before 100-episode reprocess
ADR-110 #876, #1169 Resolve speaker identity after diarization; delete positional talk-time painting
ADR-111 ADR-110, #1169 Unify eval + pipeline into ONE set of bricks; delete the allowlist triple-copy

Superseded

ADR Primary RFC Note
ADR-056 RFC-054 Composable ResponseProfile / Router not built; per-provider mock clients + per-concern unit suites shipped instead. RFC-054 closed via redirect.

Accepted, partial

ADR Primary RFC / gap Note
ADR-058 RFC-058 Core pyannote provider + alignment shipped; diarization result caching deferred as future scope
ADR-031 RFC-038 Dependabot + pydeps shipped; pre-release checklist tracked as #255 future enhancement
ADR-047 RFC-043 Nightly alerts[] detection shipped; PR-comment + webhook scripts deliberately abandoned — operator-side Sentry/Grafana wiring is the destination (see OBSERVABILITY_EXTENSIONS.md)

Accepted, code not landed (expected)

ADR Primary RFC Note
ADR-054 RFC-051 Postgres projection deferred persistence-layer scope; PRD-017/019 still ship without it

Stale-audit corrections (reference)

Trust the Code column in the table above: ADR-048 is implemented; ADR-062 / ADR-063 are Yes; ADR-064ADR-066 are implemented; ADR-021 is reflected in script-based make test-acceptance.

Situation cheat sheet

Situation Guidance
Prefer a new ADR Irreversible stack boundary, cross-cutting protocol, frozen empirical default, heavy optional extra, or closure of a large program (e.g. ADR-073).
Often RFC-only Bounded HTTP routes or viewer tabs where ADR-064ADR-066 + UXS already fix the stack (e.g. RFC-067, RFC-068, RFC-069, RFC-071). Corpus layout + manifest: ADR-074. Frozen resource baselines: ADR-075. Streamlit vs Vue for eval tools: ADR-076. Full-stack Compose + stack-test gate: ADR-084, ADR-085. CIL + bridge.json: ADR-086. Autoresearch Track A v2: ADR-087. macOS ML make safety: ADR-088. Prod failover design: RFC-083; decisions ADR-089ADR-091. Corpus snapshot backup manifest + restore defaults: RFC-084; ADR-092. Cross-surface stack contract vs adapters: ADR-093 (#762).
Proposed ADRs Promote ADR-055 to Accepted (or supersede) when RFC-053 ships end-to-end. ADR-056 already Superseded (2026-06-26) — the centralized router this ADR drafted was replaced by per-provider mock clients + per-concern unit suites; no further action needed. ADR-109, ADR-110, ADR-111 are Pending — promote each when the corresponding implementation lands.

Future triggers

  • Multi-feed manifest as an immutable external contract beyond CORPUS_MULTI_FEED_ARTIFACTS.md — partially addressed by ADR-074.
  • .pipeline_status.json schema if external monitors depend on it and breaking changes need versioning.
  • Profile YAML for non-Python consumers beyond tools/run_compare / make profile-diff — partially addressed by ADR-075.
  • RFC-070 + ADR-060 when platform vector backends land materially.
  • Full-stack Compose, stack-test, CIL or bridge, autoresearch v2, macOS ML process safety — see ADR-084ADR-088 (normative detail remains in RFC-072, RFC-073 v2 file, RFC-074, RFC-078, RFC-079).
  • Prod failover (stand up spare, validate, gated cutover)RFC-083 (Draft); decisions ADR-089ADR-091; GitHub #764.
  • Corpus snapshot tarball metadata + version-aware restoreRFC-084 (Completed); ADR-092; GitHub #763.

Open decisions without ADRs: see Architecture Decision Candidates below.

Related: PRD gap analysis, RFC gap analysis.


Architecture Decision Candidates

These items have been identified as potential architectural decisions but are currently under review.

| Candidate Decision | Origin | Status | Description | | :--- | :--- | :--- | :--- | :--- | | Informational-Only Metric Gates | RFC-043 | Open | Should regressions (runtime, coverage) block PRs or just notify? | | Excel-Based Result Aggregation | RFC-015 | Open | Should we maintain experiment_results.xlsx or move fully to web? | | Manual vs. Automated Golden Creation | RFC-041 | Open | Should golden data creation always require manual approval? | | ~~Diarization-Free Dialogue Formatting~~ | RFC-006 | Resolved → ADR-058 | Additive pyannote diarization accepted; gap-based rotation preserved as default fallback | | Minimalist Parser Dependency Strategy | RFC-002 | Open | Raw ElementTree vs. external RSS libraries | | Two-Phase Configuration Validation | RFC-007 | Open | argparse syntax + Pydantic semantic validation |


Creating New ADRs

Use the ADR Template to document new architectural decisions. Decisions typically originate from an RFC that has been reviewed and often Completed when implementation lands (RFCs use Completed, not AcceptedAccepted is the ADR status).