Skip to content

Provenance + authority on facts; keep and surface contested claims #13

Description

@minerva-sky

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

  1. Migration: asserted_by, authority, asserted_at (+ backfill authority = inferred).
  2. Resolver authority gate (Gap 1) + conflict row snapshot (Gap 3).
  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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions