Fact, table facts) is what Haki considers true at a given
moment: “invoices are in French.” Unlike an event, a fact is
versioned and mutated over time — through explicit status
transitions, never through silent overwrite.
The six statuses
FactStatus
Created by the Consolidator, not yet active. Initial state of every new
fact (
create_fact, status=candidate, version=1).FactStatus
Served by
/v1/context. One active fact per (subject, predicate)
under normal operation.FactStatus
Replaced by a more recent fact (
supersedes_id points to the new one).
Stays in history, never served as current again.FactStatus
Disputed — either via
POST /v1/feedback with rating=incorrect, or as
the losing side of an unresolved conflict. Not served by the Context
Assembler (status filter).FactStatus
Forgotten reversibly (
POST /v1/forget, mode=disable).FactStatus
Real erasure, terminal — no outgoing transition is allowed. Also
sets
recorded_to (system bitemporal end).Allowed transition graph
Enforced bytransition_fact_status (app/ledger/core.py); any attempt
outside this graph raises illegal_status_transition (422).
candidate → superseded exists specifically for conflict
resolution (sprint 6): the losing fact of a ConflictSet is
typically still candidate, and resolving the set moves it straight to
superseded — see Conflicts.version by 1.
Full fields of a fact
Full read view (FactOut, used by GET /v1/facts):
Typology and volatility
Most facts go stale in silence — the subject moves, changes jobs, finishes a project — and nothing ever contradicts the old value.fact_kind
classifies WHAT the fact is (attribute: a state of the world;
preference: how the subject wants things; instruction: a durable
operating rule the subject stated, in the third person — never a directive
addressed to the agent itself, which is rejected at the write gate).
volatility classifies HOW FAST it goes stale without a correcting event:
The clock is
coalesce(last_reinforced_at, valid_from, recorded_from): a
new event re-asserting the exact same value refreshes it (write-time
reinforcement, see Consolidator) without
creating a new fact version. An expired fact is never deleted or
superseded — only its presentation in /v1/context changes; it stays
visible via GET /v1/facts.
The predicate is not a guaranteed stable key: two different phrasings
of the same concept (bike_count vs bikes_owned) can coexist if the
extractor names them differently across calls. That is exactly the
problem the Consolidator’s semantic matching solves — see
Consolidator.
API reference — Facts and traces
GET /v1/facts, status filtering, 200-row limit.
