5 Conflicts, trust, and forgetting
The write path is where claimgraph earns its keep. Every assertion passes through a pure decision function that weighs the incoming fact against what already stands: same-value duplicates reinforce, contradicted observations supersede, threatened commitments flag, and two trust defenses catch the writes that look like poisoning. This chapter walks each path with real writes.
(ns conflicts
(:require [claimgraph.core :as core]
[claimgraph.logic :as logic]
[claimgraph.store.memory :as mem]))(def store (doto (mem/create) (core/seed!)))(defn brief [f]
{:subject (get-in f [:subject :name])
:predicate (:predicate f)
:object (or (some-> (:object-ref f) :name) (:object-lit f))
:class (:epistemic f)})5.1 A commitment flags; it never loses quietly
The team decided against GraphQL, and the decision was recorded as a commitment from a decision record:
(core/assert-fact store
{:subject "api-layer"
:predicate :core/decided-against
:object "GraphQL" :object-kind :literal
:epistemic :commitment
:source-type :decision-record}){:status :created,
:fact
{:object-ref nil,
:source-type :decision-record,
:object-lit "GraphQL",
:scope "project",
:epistemic :commitment,
:recorded-at #inst "2026-08-08T16:41:18.632-00:00",
:id "f-2e1c8276-a246-4490-85f8-c10e2fa19bfb",
:t-valid #inst "2026-08-08T16:41:18.632-00:00",
:object-kind :literal,
:t-invalid nil,
:episode nil,
:confidence 0.8,
:predicate :core/decided-against,
:subject
{:id "e-abce03af-5442-4eed-8788-587be6f1cec7",
:name "api-layer",
:type nil,
:scope "project",
:aliases []},
:last-reinforced-at #inst "2026-08-08T16:41:18.632-00:00"}}Months later a session produces “api-layer prefers GraphQL”. prefers and decided-against share the :stance exclusion group, so the write path detects the collision even though the predicates differ, and because one side is a commitment, the policy is flag, not supersede:
(def stance-clash
(core/assert-fact store
{:subject "api-layer"
:predicate :core/prefers
:object "GraphQL" :object-kind :literal
:source-type :session-log})){:status (:status stance-clash)
:candidates (mapv brief (:candidates stance-clash))}{:status :flagged,
:candidates
[{:subject "api-layer",
:predicate :core/decided-against,
:object "GraphQL",
:class :commitment}]}Both facts are now valid and linked. The conflict stays open until someone rules:
(:open (core/conflicts store))1The skill’s instruction at this point is explicit: do not pick a winner. Show the human the candidates. Here the human says the decision stands, so the newcomer’s interval closes:
(core/invalidate store {:fact-id (get-in stance-clash [:fact :id])
:reason "the ADR stands; session was exploratory"}){:status :invalidated,
:fact-id "f-7f9ad904-6262-4493-8fa6-2633f13b123c",
:at #inst "2026-08-08T16:41:18.635-00:00"}(:open (core/conflicts store))0For conflict classes that do not need a human (duplicates, clean supersessions), claim judge classifies pairs offline with an LLM and --resolve acts only on high-confidence verdicts. A contradicts verdict is never auto-resolved, no matter how confident the judge is.
5.2 Reinforcement: a high-water mark with a ceiling
Re-asserting an existing fact is not an error and not a new fact. It is the world confirming the old one. The disuse clock resets, and base confidence behaves as a ceiling-capped high-water mark:
(core/assert-fact store
{:subject "team"
:predicate :core/prefers
:object "trunk-based development" :object-kind :literal
:source-type :user-assertion
:confidence 0.8}){:status :created,
:fact
{:object-ref nil,
:source-type :user-assertion,
:object-lit "trunk-based development",
:scope "project",
:epistemic :preference,
:recorded-at #inst "2026-08-08T16:41:18.636-00:00",
:id "f-cac64a88-94fc-4bfc-832b-71dc59dabd1a",
:t-valid #inst "2026-08-08T16:41:18.636-00:00",
:object-kind :literal,
:t-invalid nil,
:episode nil,
:confidence 0.8,
:predicate :core/prefers,
:subject
{:id "e-353f6424-ca64-4134-b26d-29124c95e1da",
:name "team",
:type nil,
:scope "project",
:aliases []},
:last-reinforced-at #inst "2026-08-08T16:41:18.636-00:00"}}Weaker evidence never lowers it:
(-> (core/assert-fact store
{:subject "team"
:predicate :core/prefers
:object "trunk-based development" :object-kind :literal
:source-type :user-assertion
:confidence 0.6})
((juxt :status (comp :confidence :fact))))[:reinforced 0.8]Stronger evidence raises it, but only to the source’s ceiling (0.9 for user assertions, so 0.95 in does not mean 0.95 out):
(-> (core/assert-fact store
{:subject "team"
:predicate :core/prefers
:object "trunk-based development" :object-kind :literal
:source-type :user-assertion
:confidence 0.95})
((juxt :status (comp :confidence :fact))))[:reinforced 0.9]5.3 Trust defense one: outranked writes cannot supersede
The code ingester established the current version at trust rank 3:
(core/assert-fact store
{:subject "AuthService" :predicate :core/has-version
:object "2.0.0" :object-kind :literal
:source-type :code :confidence 0.95}){:status :created,
:fact
{:object-ref nil,
:source-type :code,
:object-lit "2.0.0",
:scope "project",
:epistemic :observation,
:recorded-at #inst "2026-08-08T16:41:18.639-00:00",
:id "f-57abd19a-af5d-47e2-aaff-5ed0daedd583",
:t-valid #inst "2026-08-08T16:41:18.639-00:00",
:object-kind :literal,
:t-invalid nil,
:episode nil,
:confidence 0.95,
:predicate :core/has-version,
:subject
{:id "e-bc3c50e8-ebbb-417c-a3e6-748c49bd1464",
:name "AuthService",
:type nil,
:scope "project",
:aliases []},
:last-reinforced-at #inst "2026-08-08T16:41:18.639-00:00"}}An agent note (trust rank 1) claims a different version. On a single-valued predicate that would normally supersede, but a lower-trust source never silently closes a higher-trust fact’s interval:
(-> (core/assert-fact store
{:subject "AuthService" :predicate :core/has-version
:object "3.0.0" :object-kind :literal
:source-type :agent-note})
(select-keys [:status :reason])){:status :flagged, :reason :outranked}5.4 Trust defense two: revenants
This is the write pattern memory poisoning actually takes: resurrect a dead value from a low-trust source. The project deployed via Heroku, then migrated:
(def heroku
(:fact (core/assert-fact store
{:subject "shoply" :predicate :core/deployed-via
:object "Heroku" :object-kind :literal
:source-type :user-assertion})))(core/invalidate store {:fact-id (:id heroku)
:reason "migrated to Fly"}){:status :invalidated,
:fact-id "f-c5255f70-f16c-4c6d-b05b-bf713243eb36",
:at #inst "2026-08-08T16:41:18.640-00:00"}(core/assert-fact store
{:subject "shoply" :predicate :core/deployed-via
:object "Fly" :object-kind :literal
:source-type :user-assertion}){:status :created,
:fact
{:object-ref nil,
:source-type :user-assertion,
:object-lit "Fly",
:scope "project",
:epistemic :observation,
:recorded-at #inst "2026-08-08T16:41:18.641-00:00",
:id "f-0c452f71-b6bc-45ea-83a3-e78fae3a8159",
:t-valid #inst "2026-08-08T16:41:18.641-00:00",
:object-kind :literal,
:t-invalid nil,
:episode nil,
:confidence 0.8,
:predicate :core/deployed-via,
:subject
{:id "e-d03e6c7b-cee0-4508-9507-8f3d650e2dd1",
:name "shoply",
:type nil,
:scope "project",
:aliases []},
:last-reinforced-at #inst "2026-08-08T16:41:18.641-00:00"}}Now a planted note re-asserts Heroku. deployed-via is multi-valued, so without a defense the store would happily hold both and a compiled view could cite either. Instead: this (subject, predicate) already lived through Heroku and invalidated it, a live rival exists, and the writer is low-trust. The resurrection flags against the rival:
(def planted
(core/assert-fact store
{:subject "shoply" :predicate :core/deployed-via
:object "Heroku" :object-kind :literal
:source-type :agent-note})){:status (:status planted)
:reason (:reason planted)
:rivals (mapv brief (:candidates planted))}{:status :flagged,
:reason :revenant,
:rivals
[{:subject "shoply",
:predicate :core/deployed-via,
:object "Fly",
:class :observation}]}The disputed pair sits in the open-conflict queue, and (as the ambient chapter shows) disputed facts are excluded from the compiled current-truth view. The poison can still be seen; it can no longer be believed by default. The benchmark chapter measures what this defense is worth on an end task.
5.5 Forgetting: decay is computed, never stored
Effective confidence is a pure function of the fact and the clock, so it can be shown without waiting 180 days. A session-derived fact at base 0.7, read at increasing distances from its last reinforcement:
(let [fact {:confidence 0.7
:epistemic :observation
:source-type :session-log
:recorded-at #inst "2026-01-01"
:last-reinforced-at #inst "2026-01-01"}
at (fn [t] (logic/effective-confidence fact t))]
{:fresh (at #inst "2026-01-01")
:after-90-days (at #inst "2026-04-01")
:after-180-days (at #inst "2026-06-30")
:after-a-year (at #inst "2027-01-01")}){:fresh 0.7,
:after-90-days 0.35,
:after-180-days 0.175,
:after-a-year 0.05}One half-life per 90 days, with a floor at 0.05. A commitment with the same timestamps does not move at all:
(logic/effective-confidence {:confidence 0.9
:epistemic :commitment
:source-type :decision-record
:recorded-at #inst "2026-01-01"
:last-reinforced-at #inst "2026-01-01"}
#inst "2027-01-01")0.9Disuse is not falsity. Falsity is handled by invalidation (mechanical, from the code ingester’s reconciliation, or explicit); disuse only sinks a fact’s rank until something restates it, retrieves it in accepted work (claim outcome accepted), or lets it rest at the floor.
source: book/chapters/conflicts.clj