docs/adr/0039-dns-consensus-degradation-policy.md
Rama master · réplica de lectura del repositorio original.
# ADR-0039: DNS Consensus Failure Policy
## Metadata
| Field | Value |
|-------|-------|
| **Status** | Superseded by [ADR-0072](0072-dns-consensus-revalidation-hardening.md) and [ADR-0075](0075-dns-unbound-single-source-completion.md) |
| **Date** | 2026-08-07 |
| **Authors** | AlessioBrillo |
| **Deciders** | AlessioBrillo |
| **Supersedes** | N/A |
| **Relates to** | ADR-0002, ADR-0035, ADR-0037 |
| **Project** | DOMINUS |
## Context
The DNS pre-filter uses a 2-of-3 resolver consensus: the primary bulk check
races multiple resolvers internally, and every `Available` verdict is
cross-checked against an independent secondary provider before it is
accepted. This exists to uphold the ADR-0002 conservatism principle — a
domain must never pass on the opinion of a single resolver.
The consensus check has an operating condition that is outside DOMINUS'
control: the secondary must actually *answer*. When a secondary provider is
briefly down, rate-limited, or selecting an overloaded resolver, every
Available verdict becomes unverifiable and the whole run is downgraded to
`Unknown` — and thus filtered. Two failure modes result:
1. **Silent throughput collapse**: nothing in the run result told the user
*why* everything was filtered. A run with 500 candidates and a sick
secondary produced 0 recommendations indistinguishable from a genuine
"no eligible domains" verdict.
2. **Hard outage vs. degraded warning**: the alternative — aborting the run
when consensus fails — is disproportionate. The primary DNS check is still
a legitimate fast signal; discarding the whole run lazily loses all
candidates because one secondary hiccupped. But blindly accepting
primary-only verdicts violates ADR-0002.
A third constraint complicates the threshold: on small runs (a handful of
candidates), a single unverifiable response is a large fraction. Without a
floor, one bad resolver response would flag the run degraded over a domain or
two — a false alarm that makes the degradation signal meaningless.
## Decision
**Fail-closed with a visible flag.** When the secondary cannot confirm a
material share of the primary's `Available` verdicts:
- Every unconfirmed domain is downgraded to `Unknown` and filtered (existing
ADR-0002 behaviour — consensus is strictly required; no single-resolver
domains pass).
- The run **continues** but is flagged **degraded**, surfacing an explicit
`consensus-unverified` `StageDegradation` on the run so it is never
confused with a genuine no-recommendations run (ADR-0037 machinery).
- Degradation fires only when both conditions hold:
- `unverifiable / consensusTotal >= degradedRatio` (default 0.5 — the
secondary failed majority), **and**
- `consensusTotal >= degradedMin` (default 10) — small runs never flag.
- The verdict tally (`verified`, `disagreed`, `unverifiable`, `degraded`)
is carried per run to the metrics collector and exposed as
`dominus_dns_consensus_*` Prometheus series.
### Configuration
| Variable | Default | Meaning |
|----------|---------|---------|
| `DNS_CONSENSUS_DEGRADED_RATIO` | `0.5` | Minimum fraction of unverifiable verdicts (over consensus-checked domains) that flags degradation |
| `DNS_CONSENSUS_DEGRADED_MIN` | `10` | Minimum consensus-checked domains before degradation can be flagged |
These join the existing `DNS_CONSENSUS_SECONDARY_PROVIDER` knob and are read
in `src/config.ts`, threaded through the provider factory into the
`ConsensusDnsConfig` consumed by `DnsPreFilterStage` (`src/pipeline/stages/dns-prefilter-stage.ts`).
### Options considered and rejected
- **Fail-open (accept primary-only verdicts on consensus failure)** — rejected:
lets a domain through on a single resolver's opinion, directly violating the
ADR-0002 conservatism principle that this consensus exists to enforce.
- **Fail-closed and silent (filter everything, no flag)** — rejected: produces
the silent-throughput-collapse failure mode that motivated this ADR. Output
that rests on a single (or sick) resolver must be visible.
- **Hard abort of the run** — rejected: one secondary hiccup would forfeit
otherwise valid candidates; the primary check is a legitimate fast signal and
ADR-0037 already provides a proper degraded-run channel.
- **Always flag on any unverifiable** — rejected: noisy on small runs, which
now the `degradedMin` floor exists to protect.
## Consequences
### Positive
- The consensus check stays strict: no domain passes on a single resolver —
the ADR-0002 principle is preserved even when the secondary is sick.
- Degradation is loud: the ADR-0037 `degradedReasons` channel, job/API/CLI
surface and dashboard banner now distinguish "degraded run" from "no
recommendations", with a human-readable message (`X/N Available verdicts
unconfirmed by the secondary provider`).
- Tunable via config without a code change; sane defaults keep tiny runs quiet.
- Verdict tallies are observable: `verified`/`disagreed`/`unverifiable`
counters plus a degraded-run gauge in `/metrics` (and the collector
snapshot), so an operator can detect a chronically sick secondary.
### Negative
- A genuinely unavailable, valuable, non-trademarked domain is still dropped
when the secondary can't confirm it — consensus is conservative by design.
- With defaults, a run with 9 or fewer consensus-checked domains never flags
degraded even at 100% unverifiable (7 falses), trading strictness for
noise-free small runs — an explicit, documented trade-off.
- One more config surface to tune (ratio/min), mitigated by the default
majority + floor pairing.
### Risks and mitigation
- **False security from a healthy secondary**: the consensus gate is only as
good as the secondary's independence; the metrics series now expose
repeated `disagreed`/`unverifiable` spikes for monitoring.
- **Threshold tuning**: if real-world secondaries degrade proportionally lower
than the default `0.5`, operators should lower the ratio; the series
provide the data to decide.