Context
From Fowler's "The Specification Is Not a Document" (https://aicoding.leaflet.pub/3mtgs36dnq22o): document contradictions rather than hiding them. Store who asserted a fact and with what authority; when claims conflict, keep both marked contested instead of silently superseding; surface contested pairs on recall.
The codebase is already most of the way there — this issue proposes closing three specific gaps, not a new subsystem.
What already exists
Fact#status includes "disputed", and Resolve::Resolver creates disputed fact + conflict rows on :conflict outcomes.
- A
provenance table links facts to source content with line ranges (migrations 001, 008).
confidence is already a first-class fact attribute.
Gap 1: supersession ignores authority
Resolver#supersession_signal? returns true for any strength == "stated" assertion, so a newly stated claim silently retires the old one — even if the old one was stated by the user and the new one came from a stale doc. There is no notion of who asserted a fact or how much weight that source carries.
Proposal:
- Add
asserted_by (freeform: user, agent:<name>, doc:<path>) and authority (small ordinal enum, e.g. user_stated > maintainer_doc > inferred) to facts — or, if churn on the facts table is unwanted, to provenance rows with the fact's effective authority = max over its provenance.
- Resolver rule change: a stated claim supersedes only when
new.authority >= old.authority. When authorities are equal-and-conflicting or incomparable, fall through to the existing :conflict path (keep both, one marked disputed, conflict row records the pair) instead of superseding.
Gap 2: contested pairs are invisible on recall
FactQueryBuilder only ever returns active facts; a disputed fact stays hidden until explicitly asked for (the resolver comments around the example-text guard describe exactly this failure). That means recall confidently returns one side of a live contradiction.
Proposal:
- When a recalled slot (subject + predicate) has disputed siblings, include them, annotated. Formatter output shape:
X is contested: "A" (user, 2026-08-12) vs "B" (doc:README, 2026-08-20).
- Keep the default result ordering by authority then recency, so callers that only read the first value still get the best guess — but the contradiction is on the record.
Gap 3: conflict rows don't record authority
Conflict rows should carry both sides' asserted_by/authority/asserted_at snapshot so a contested pair is judgeable later without re-deriving provenance.
Non-goals
- No auto-resolution of contested pairs (human or explicit tool call resolves them).
- No change to the example-text/ReferenceMaterialDetector guard — it correctly discards, not disputes.
Suggested order
- Migration:
asserted_by, authority, asserted_at (+ backfill authority = inferred).
- Resolver authority gate (Gap 1) + conflict row snapshot (Gap 3).
- Recall surfacing (Gap 2).
Filed from bean task-4bed2663 (Val intake 2026-08-20). Happy to take the implementation in a follow-up branch if this direction looks right.
Context
From Fowler's "The Specification Is Not a Document" (https://aicoding.leaflet.pub/3mtgs36dnq22o): document contradictions rather than hiding them. Store who asserted a fact and with what authority; when claims conflict, keep both marked contested instead of silently superseding; surface contested pairs on recall.
The codebase is already most of the way there — this issue proposes closing three specific gaps, not a new subsystem.
What already exists
Fact#statusincludes"disputed", andResolve::Resolvercreates disputed fact + conflict rows on:conflictoutcomes.provenancetable links facts to source content with line ranges (migrations 001, 008).confidenceis already a first-class fact attribute.Gap 1: supersession ignores authority
Resolver#supersession_signal?returns true for anystrength == "stated"assertion, so a newly stated claim silently retires the old one — even if the old one was stated by the user and the new one came from a stale doc. There is no notion of who asserted a fact or how much weight that source carries.Proposal:
asserted_by(freeform:user,agent:<name>,doc:<path>) andauthority(small ordinal enum, e.g.user_stated > maintainer_doc > inferred) to facts — or, if churn on the facts table is unwanted, to provenance rows with the fact's effective authority = max over its provenance.new.authority >= old.authority. When authorities are equal-and-conflicting or incomparable, fall through to the existing:conflictpath (keep both, one marked disputed, conflict row records the pair) instead of superseding.Gap 2: contested pairs are invisible on recall
FactQueryBuilderonly ever returns active facts; a disputed fact stays hidden until explicitly asked for (the resolver comments around the example-text guard describe exactly this failure). That means recall confidently returns one side of a live contradiction.Proposal:
X is contested: "A" (user, 2026-08-12) vs "B" (doc:README, 2026-08-20).Gap 3: conflict rows don't record authority
Conflict rows should carry both sides'
asserted_by/authority/asserted_atsnapshot so a contested pair is judgeable later without re-deriving provenance.Non-goals
Suggested order
asserted_by,authority,asserted_at(+ backfillauthority = inferred).Filed from bean task-4bed2663 (Val intake 2026-08-20). Happy to take the implementation in a follow-up branch if this direction looks right.