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.
Consumers declare what they need
Section titled “Consumers declare what they need”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[].unitPriceCentsA fact with no consumer is reported by ATLAS010 (a warning); a reaction to a fact nobody emits by ATLAS002.
Sensitive data stays home
Section titled “Sensitive data stays home”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.
Fact evolution
Section titled “Fact evolution”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 arev1. - 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 tracedupcast-failedand skipped. d.factand 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 ?? '').
Cause and depth
Section titled “Cause and depth”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.