ADR 0015 - Survived-challenge recording and the earned-supersession gate
Date: 2026-07-03
Status
Section titled “Status”Accepted.
Context
Section titled “Context”The firewall rejects challenges — a supersession below the incumbent’s authority
(InsufficientAuthority), a supersession whose backing fact is actually weaker than its
stated authority (LaunderedAuthority), a retraction/expiration below the incumbent, a
contradiction against a canonical fact (CanonicalContradiction). Today a rejected
challenge leaves no trace: the candidate event is refused before persistence and only
the caller’s error output knows it happened. Two things are lost:
- The strongest entrenchment signal. A fact that was attacked and stood is better
evidence than a fact nobody ever tested.
FactState.corroborating_sourcesdeliberately measures same-value corroboration only; its own doc comment names the gap: “surviving a contradiction or a rejected supersession is deliberately not counted here (that would require recording rejected attempts)”. This is the open half of earned entrenchment (research/novelty.md rank 3). - The attack audit. MINJA-style injection probes are invisible: an operator cannot see
that
source:web-scrapetried five times to overwrite a canonical fact, because failed attempts vanish.
Separately, [SubjectProjection::unearned_supersessions] audits admitted supersessions
after the fact — flagging AuthorityDowngrade and WeakerCorroboration — but nothing
enforces the corroboration half at write time: an equal-authority, single-source fact can
displace an incumbent backed by many sources.
Decision
Section titled “Decision”1. Record survived challenges as events (default on)
Section titled “1. Record survived challenges as events (default on)”A new event kind on the incumbent’s stream, written by the op boundary (CLI/MCP shared
op_* path) when arbitration rejects a challenge:
FactEventKind::ChallengeRejected { challenge: ChallengeKind, // Supersession | Contradiction | Retraction | Expiration by: Option<FactId>, // the challenging fact, when the challenge named one rejection: ChallengeRejection, // InsufficientAuthority | LaunderedAuthority // | CanonicalContradiction | WeakerEntrenchment // (WeakerCorroboration retained for pre-0.3 events)}- The event carries the challenger’s provenance — the attack attempt is attributed, and under signed identity (ADR 0013) the record is attested by the challenger’s own key: the challenger authenticated to make the write, so the record of its failure is signed by it.
- The event’s
authorityis the challenger’s effective authority — the stated level, except for a laundered supersession where it is the backing fact’s actual (lower) level. “Survived a High challenge” must mean the challenge was actually High. - The fold treats it as non-terminal bookkeeping on a live incumbent: it accumulates
FactState.survived_challenges: BTreeMap<SourceId, AuthorityLevel>(challenger source → highest effective authority that challenged and lost), mirroringcorroborating_sources.survived_challenges_at_or_above(min)is the Sybil-resistant read: minting low-authority challengers cannot simulate surviving strong attacks. - Recorded only for challenges that were real contests lost on strength: the four
rejections above, against a live (non-terminal) incumbent. Malformed writes, duplicate
assertions, terminal-state mutations, and supersessions naming a nonexistent fact
(
UnbackedSupersession) are noise or lineage errors, not survived challenges. DENT8_RECORD_CHALLENGES=0opts out. Recording is on by default: silent rejection is the same silent-half-coverage failure mode the witness lane exists to end. The rejected caller still gets its error unchanged (same exit code); the text/JSON gains a note that the incumbent recorded the challenge.
2. The earned-supersession gate (opt-in)
Section titled “2. The earned-supersession gate (opt-in)”DENT8_ENTRENCHMENT_GATE=1 turns the WeakerCorroboration audit into a write-time
gate at the op boundary: an equal-authority supersession whose backing fact has strictly
weaker authority-weighted corroboration than the incumbent (both measured by
corroboration_at_or_above at the shared level) is rejected — and recorded as a survived
challenge like any other. The AuthorityDowngrade half needs no gate: the subject-aware
anti-laundering check already blocks it at write time.
Off by default because it changes admission semantics: a genuinely newer fact usually arrives from one source first, and rejecting it until it gathers corroboration trades plasticity for entrenchment. That is a policy choice an operator must make, not a default. The projection audit remains the always-on detector either way.
Compatibility
Section titled “Compatibility”- Hashes are untouched. Existing events’ canonical bytes and hashes are unchanged; no
CANON_VERSIONbump (same additive rule as ADR 0013). Golden fixtures stay frozen. - Forward-compatibility note. A log containing
ChallengeRejectedevents cannot be read by pre-0.2 binaries (unknown enum variant). This is a capability addition, not a format break: old logs verify identically under both. FactState.survived_challengesis#[serde(default)], so previously materialized projections deserialize (and are rebuilt from the log as always).
Trust model and residuals
Section titled “Trust model and residuals”- The op boundary emits the record; a writer with raw store access could append forged
ChallengeRejectedevents to inflate an incumbent’s survival record. Two mitigations already in place:survived_challenges_at_or_aboveonly counts challenges at an authority the forger would have to hold, and under signed identity the record’s attestation must verify against the challenger’s key with entitlement at write time (ADR 0014). Same boundary trust model as identity enforcement (ADR 0012). - Survived challenges are consulted by arbitration as of ADR 0017: the opt-in earned-supersession gate (and the subject-level audit) weigh earned entrenchment = corroboration + survived challenges, so a fact that survived an equal-authority challenge resists the next fresh equal-authority replacement. This ADR deliberately delivered recording first — the history had to exist and accumulate honestly before a gate consumed it.
Consequences
Section titled “Consequences”- Every rejected challenge becomes replayable evidence:
explaincan say “survived 2 challenge(s), 1 at ≥High”, and an injection probe campaign is visible in the log with the attacker’s own attestation on it. - The log grows one bookkeeping event per rejected challenge (bounded by the writer’s ability to attempt writes at all).
dent8-store’s public API is unchanged (the gate lives at the op boundary); the audit keeps working on logs written before this ADR.