Data Manifesto

Just the Facts

There is exactly one kind of data in Onto: a fact. A fact is an EAV (Entity-Attribute-Value) triple (a tuple that contains exactly 3 elements)- the same model as Datomic, DataScript, and RDF. Everything the system knows is expressed this way, in five categories:

# entity fact    (domain data)
[:reservation/1  :reservation/date    "2024-03-15"]  

# event fact     (history)          
[:event/42       :event/type          :reservation/requested]  

# derived fact   (rule output)
[:reservation/1  :reservation/status  :confirmed]              

# clock fact     (time)
[:system/clock   :clock/now           1706001000]              

# intent fact    (side effect signal)
[:reservation/1  :intent/send-email   true]                    

<aside> 💡

The one-line version: everything is a fact, all facts have a cause, all state is a conclusion and Datalog is what makes those three properties guarantees instead of conventions.

</aside>

State is derived, never stored

We never store state imperatively. Rules are Datalog implications - when these facts are true, these other facts follow - and the runtime keeps a live fact base, refiring rules whenever facts change. Derived facts cascade to a fixpoint automatically: no event handlers, no pub/sub, no wiring. A reservation's status isn't a column you update; it's a conclusion the rule base reaches from the facts. The state machine is emergent from the rules, not encoded anywhere.

Facts change only through something that meant it

Two immutability principles govern writes:

  1. Append-only. What happened is recorded as immutable event facts. Current state is derived by replay, which makes bitemporal queries free, reconstruct any past state by replaying the log to that point.
  2. No raw writes. A fact changes only through a story beat (an event that happened) or a rule firing (a conclusion that followed). Never a manual edit, never a direct import. Every change has a cause you can point to.

Every write goes through one loop:

guard → apply → derive → check → commit-or-reject

Preconditions guard; the batch applies to a candidate snapshot; derived facts and formulas recompute to a fixpoint; postconditions and invariants check the result. A rejection leaves the fact base exactly as it was - nothing bad becomes real.

Time and side effects are facts too

The clock is a fact (:clock/now); a tick advances it, recomputes, and re-derives - so deadlines fire without anyone writing. Side effects stay out of the rules entirely: a rule asserts an :intent/* fact, and a platform executor performs the effect exactly once (keyed by a content-addressed intent identity), then closes it. Rules stay pure - same facts in, same facts out.

Why Datalog

Because the guarantees are structural, not aspirational. Stratified Datalog gives us termination (rule sets always halt), determinism (same facts → same derived facts), and monotonicity (order-independent firing) by construction - entire bug classes eliminated without proving anything. And we deliberately restrict the fragment (linear arithmetic only, discrete integer timeline) so the same rules that run are also decidably checkable (QF-LIA + EUF) and readable as plain English. The design work is the restriction; the logic is imported.