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¶
- Immutable Records: Once an ADR is accepted, it remains unchanged unless superseded by a new ADR.
- Context Driven: They explain the trade-offs and rationale behind a decision (not the whole solution document).
- 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_contradiction→topic_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 downloaded — detect_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-067–ADR-072 |
| Stack & ownership boundary | Who owns HTTP, which frontend stack, which UI E2E runner. | ADR-064–ADR-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-064–ADR-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-064–ADR-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-089–ADR-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.jsonschema 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-084–ADR-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-089–ADR-091; GitHub #764.
- Corpus snapshot tarball metadata + version-aware restore — RFC-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 Accepted — Accepted is the ADR status).