Skip to main content
A fact (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 by transition_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.
Every transition increments 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.