Modules
The attestation RECORD STORE — human assertions about a cell's data, as immutable append-only history (the host project's ADR-002).
The content-addressed BASIS of an attestation — a digest of what the scope selected at signing time (ADR-002 decision 4, in the host's docs).
The read-time force of attestation records: stance ⨝ basis ⨝ eligibility ⨝
tolerance → an ADMISSION per scope. Pure — takes everything as data (raw
rows, stances, eligibility keys, a requirement, now), so the whole
semantics is testable without a database; ReactiveDag.Attestation.Op is the
thin DB glue around it.
The recompute of an ATTESTED cell — the derived view an attested combinator
or a gate:d edge lowers to. Thin DB glue: read the three inputs, run the
pure ReactiveDag.Attestation.Evaluation, write ordinary spine rows.
An Ash resource extension for the attestation record store — the
Ash-idiomatic storage pattern (as ash_authentication's token resource): the
HOST defines the resource (choosing repo, table, domain, policies), this
extension stamps the required shape onto it, and the library reaches it
through config :reactive_dag, attestation_resource: MyApp.Attestation.Record.
The :sign action's change: applies the resource's who_from_actor (an
actor present → who forced from it), validates the polarity, and errors
a :reject with a blank reason — a rejection asserts the data is WRONG,
and a bare "no" leaves whoever must act with nothing to fix and an auditor
with an unexplained refusal. (:withdraw — "I no longer vouch" — asserts
nothing about the data, so its reason stays optional.)
A named attestation REQUIREMENT — the policy for one kind of sign-off,
declared once on the node that owns the raw data (attestation :name do … end
in the reactive block) and consumed by name from an attested combinator or
a gate: on an edge.
WHAT an attestation is about — one row, or the set a filter selects.
One node in the DAG — the domain-neutral IR both host apps compile down to.
The seam for WRITING a cell's coordination tuples — the third seam, alongside
RecomputeStrategy (how a cell recomputes) and KeyRule (how a change
propagates). An op, mid-recompute, records which of its keys are present /
gone; this behaviour is where those writes land.
The reactive propagation loop — the heart of the substrate, shared by both hosts.
What a drain ACTUALLY did — the processing trace, returned by
ReactiveDag.Drain.run/2.
The drain exceeded its pass budget — likely a cycle, or a recompute that
keeps re-dirtying its own inputs. :report carries the PARTIAL trace up to
the abort: report.steps' tail shows exactly which cells keep triggering
each other, which is the diagnostic for the loop this error suspects.
The DSL compile pipeline over the flat Cell IR — the resolve→lower→validate
machinery both host DSLs share, parameterized by app hooks so each keeps its
own domain vocabulary.
The dirty frontier, owned by the library and backed by the reactive_dag_dirty
table (created by ReactiveDag.Migration). The host is an Ash/AshPostgres app,
so we go through its repo with raw SQL — values always parameterized; the
table name (the one identifier SQL cannot parameterize) comes from config and
is validated against an identifier grammar at read time, so a typo fails
loudly instead of as a syntax error deep in a query. Claim-as-delete is a raw
DELETE … RETURNING that Ash actions don't express cleanly.
Pure DAG construction: a list of ReactiveDag.Cell → a ReactiveDag.Plan.
How a change to a child propagates to a parent — the op-aware propagation
seam. When changed keys of child feed parent, the rule decides which of
the parent's keys become dirty
Shared machinery for lowering a NESTED op-expression into a flat cell list —
the recursion both host DSLs independently grew (cascade's Lower.resolve_legs,
the portal's Graph.build_node). Same algorithm: walk an op's legs, recurse
into each, a ref resolves to an existing cell-id (no new cell), a nested op
becomes an intermediate cell whose inputs are the recursed leg ids.
The library-owned DDL, callable from a host migration — the dirty-frontier
table ReactiveDag.Frontier reads and writes (coalesced by (cell_id, key);
claim is a DELETE … RETURNING)
An Ash resource extension that makes a resource a node in a reactive DAG.
The resource IS the node and its own payload table: its reactive block
defines the computation, its attributes are the rows the node materializes.
This is the intended shape — one resource, both roles.
A PURE-ASH-QUERY reduce: the datastore does the grouping via a RELATIONSHIP
aggregate. The node's own resource is the group's resource — ONE row per group
— and over names its has_many to the rows being aggregated. The library loads
the aggregates in ONE Ash query — Postgres computes the GROUP BY — and each
parent row's aggregate values are its payload. No rows cross into the BEAM; no
into/read/upsert (contrast the in-BEAM reduce, which loads every row).
The ATTESTED VIEW combinator: this node is the derived cell whose rows are
over's rows joined against currently-applying attestation records under a
named requirement — both cells exist in the graph (the raw list AND the
signed list), and a consumer picks per edge. ref :x, gate: :req is sugar
that interposes an anonymous cell of exactly this shape.
An anonymous nested op-expression leg: composes inline as an intermediate
cell (its as id, or a positional id derived from the parent). Its own legs
are ref/compose, so the algebra reads as an expression tree.
The ESCAPE HATCH: declare an arbitrary recompute MODULE (a ReactiveDag.Op)
for a node whose computation the reduce/join combinators can't express —
an LLM call, a PDF/Tigris fetch, a bespoke multi-input recompute. compute MyApp.EventsExtract sits in the block alongside the combinators, mirroring
Ash's calculate :x, :type, MyModule (the arbitrary case is an entity too,
not a schema key beside the declarative ones).
A declarative JOIN: read ONE input's payload, index it into a LEFT and a
RIGHT side (each a %{join_key => item} built from a per-side key fn), then
emit one row per left key joined to its right item (right may be absent). The
common declared-vs-observed reconcile/variance shape — the author writes the
two side keys + the join row, not the read/write/changed plumbing.
A GENERIC ReactiveDag.KeyRule for graphs declared with ReactiveDag.Node.
Node records each node's key_rule (:identity | :all) in cell.meta;
this reads it
Closes the payload loop for a resource-backed node: writes a combinator's output
row into the node's OWN resource (cell.meta.resource), keyed by the cell key.
A GENERIC ReactiveDag.RecomputeStrategy for graphs declared with
ReactiveDag.Node. Because Node standardizes where a cell's op module lives
— cell.meta.compute, a ReactiveDag.Op — the dispatch is uniform and the
host no longer hand-writes it
Runs a pure-Ash-query aggregate node: the datastore groups + aggregates the
node's over relationship in ONE query (a relationship aggregate — Postgres does
the GROUP BY), and each parent row's aggregate values become its payload.
A declarative REDUCE (fold): read an input node's payload, group it, and
reduce each group to one output row — the common map/fold shape, so the
author writes the grouping + reduction, not the read/write/changed plumbing.
Anything the combinator can't express (an LLM call, an external fetch, a
bespoke join) uses the compute: module escape hatch instead.
A by-name input edge to another named node (ref :id). The general form —
nestable inside compose. The flat depends_on: [:a, :b] schema key is sugar
that lowers to one %Ref{} per id.
A by-name REFERENCE input edge: the node READS the target as context but is NOT
recomputed when the target changes. Still a real input (validated, ordered by
depth so the target settles first, read at recompute) — it just doesn't
propagate. For a node whose recompute is expensive/non-deterministic and
consults mutable reference data it shouldn't be re-triggered by (an LLM step
that looks up a human-curated people/positions table). Contrast ref, which
dirties this node on change.
The behaviour a node's compute module implements — the recompute for ONE op,
the per-cell unit of work.
The compiled DAG plan — pure data the drain executes. Decoupled from any DSL:
a host app lowers its declarations into a Cell list and Graph.build/1
produces this. The drain only ever sees the Plan.
The seam where the host app's OP ALGEBRA + RECOMPUTE MODEL plug in.
A generic RecomputeStrategy for SET-BASED ops — the layering for hosts whose
recompute is set algebra over the coordination tuple (the compliance portal),
the counterpart to ReactiveDag.Node.Recompute for per-key/BEAM hosts.
A scanner — the fourth seam, alongside ReactiveDag.RecomputeStrategy,
ReactiveDag.KeyRule, and ReactiveDag.CoordinationWriter.
The shared COORDINATION tuple — the reactive layer's projection of a cell into
a thin (cell_id, key, status, freshness) row. A cell IS its set of these
rows; a parent reads a child's set by (cell_id, key).
The DEFAULT ReactiveDag.CoordinationWriter — spine-only, over the configured
tuple table via ReactiveDag.Tuple. Suitable for a host with no extension
columns on its coordination tuple. A host with extensions (cascade's
source_ref/tombstoned_at, the portal's strength) configures its own
writer that does the spine + extension write in one atomic upsert.
The generic READ layer over the coordination-tuple spine — a cell's live verdict
and its failing-sample, rolled up from the tuple status histogram. This is the
engine piece a host used to hand-write (the portal's ModelEval.Verdict); it's
domain-neutral, so it lives here and the host keeps only its own addressing
sugar (for_guarantee, for_control, typed detail joins) on top.