Skip to content

Facts

Facts and evolution

A fact is something that happened: past tense, immutable. A capability declares the facts it may emit in emits, each with a shape, and emits them with d.fact(type, payload) in accept.

Writes and emitted facts are committed in one transaction with an outbox: there is no dual write between a store and a bus. Every consumer then reads the outbox through its own cursor.

A reaction or projection declares needs: a shape with only the fields it uses. atlas check (ATLAS003) proves that the producer’s shape satisfies it, and atlas context lists exactly who reads which field:

DOWNSTREAM BLAST RADIUS (who breaks if what this produces changes)
OrderPlaced.orderId <- order-summary, stock
OrderPlaced.customerId <- order-summary
OrderPlaced: unused by consumers (safe to change): lines[].unitPriceCents

A fact with no consumer is reported by ATLAS010 (a warning); a reaction to a fact nobody emits by ATLAS002.

t.sensitive(...) fields may live in owns and command inputs, never in emits or sends (ATLAS016, and the runtime refuses the effects). Publish an id or a non-sensitive derivative instead.

Stored facts are never rewritten. When a fact’s shape changes incompatibly (a new required field, a rename, a retype), version it:

const V1 = t.object({ storeId: t.string, name: t.string });
const V2 = t.object({ storeId: t.string, name: t.string, description: t.string });
emits: { StoreOpened: t.fact(2, V2, { from: 1, shape: V1, up: (v1) => ({ ...v1, description: '' }), examples: [{ storeId: 's1', name: 'Old' }] }) }
// v3 later: t.fact(3, V3, { from: 2, shape: V2, up: ... }, { from: 1, shape: V1, up: ... })
  • New facts are stored with their version (v); rows from before versions are v1.
  • Delivery and rebuilds lift older facts step by step (v1 -> v2 -> v3, each step parses its version first) before parsing needs. A fact no chain can lift is traced upcast-failed and skipped.
  • d.fact and consumers see only the latest shape; ATLAS003 compares against it.
  • ATLAS025 checks the chain: no gaps, one step per older version, and each upcaster, run on its examples and on generated values of its shape, returns a valid next version.
  • Upcasters are pure (ATLAS007, also in shapes modules).

v1 is everything stored before the fact had versions: if some of those rows already carry newer fields, make them optional in the v1 shape and keep them in up (description: v1.description ?? '').

A delivery stores the outbox id of the entry that triggered it as the cause of each entry it writes, and every entry stores its depth: 0 for what a dispatched command or a released timer writes, the cause’s depth + 1 for what a delivery writes. await: 'all' follows cause across processes, and maxDepth (default 1000) bounds a cascade that never ends: an entry past it settles as failed with cascade-exceeded.