API Reference reactive_dag v#0.16.0

Copy Markdown View Source

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.

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.