ReactiveDag.Attestation (reactive_dag v0.16.0)

Copy Markdown View Source

The attestation RECORD STORE — human assertions about a cell's data, as immutable append-only history (the host project's ADR-002).

A record is (cell_id, scope, who, polarity, reason, basis, basis_version, signed_at, meta): someone (who) affirmed, rejected, or withdrew their word on what a scope of cell_id's data looked like (basis — a content digest, ReactiveDag.Attestation.Basis) at a moment. Records are never updated or deleted; a signer's current STANCE on a scope is simply their most recent record, and whether that stance currently COUNTS is a read-time predicate (ReactiveDag.Attestation.Evaluation) — never a stored status.

Storage: a host-defined Ash resource

Records live in an Ash resource the host defines and this module reaches via config — the Ash-idiomatic library-storage pattern. The ReactiveDag.Attestation.Record extension stamps the required shape (attributes, the :sign create, a primary read) and ENFORCES append-only (an update/destroy action fails compilation); the host chooses repo, table, domain — and composes policies, notifications, and generated migrations onto it like any other resource.

config :reactive_dag, attestation_resource: MyApp.Attestation.Record

Writes pass actor: through to Ash, so a resource configured with who_from_actor derives the signer from the actor — impersonation prevented at the write, not merely discounted at read time. Internal reads (stances/1, history/2) run with authorize?: false: they are the system's own evaluation input, not a user-facing query.

The leaf cell

The store surfaces in a graph as ONE leaf cell (leaf_cell/0, default "attestations", configurable via :attestation_cell) — an implicit input of every attested cell, so signing propagates exactly as a scan finishing does: leaf write → dirty → drain. affirm/4 / reject/5 return the serialized scope as the changed leaf key for the host's refresh call.

Summary

Functions

Record that who AFFIRMS scope of cell_id as it currently stands. The basis is digested from the rows the scope selects right now (pass rows: to supply them, e.g. in a transaction that just read them). reason: is optional on an affirmation. meta: (a map) rides on the record and comes back on every read — host context like a ticket id or a UI origin. actor: is passed through to Ash — with a who_from_actor on the record resource, the signer is derived from it.

Full append-only history for cell_id (optionally one scope), newest first.

The id of the store's leaf cell (config :attestation_cell).

Record that who REJECTS scope of cell_id as it currently stands. reason is REQUIRED: an affirmation asserts the data as presented, but a rejection asserts it is wrong — a bare "no" leaves whoever must act with nothing to fix and an auditor with an unexplained refusal. (The resource's :sign action enforces the same rule for writes that bypass this module.)

Every signer's current STANCE on cell_id's scopes: the most recent record per (scope, who). This is the store's read for evaluation — history stays in the table; only the latest word per signer per scope has force.

Record that who WITHDRAWS their word on scope — "I no longer vouch", which is a different act from rejecting ("the data is wrong"). A withdrawal supersedes the signer's previous stance and itself carries NO force in either direction: the scope returns to unaffirmed (pending / re-askable), never to refused. reason: is optional — withdrawing asserts nothing about the data, so there is nothing that must be explained.

Types

polarity()

@type polarity() :: :affirm | :reject | :withdraw

record()

@type record() :: %{
  id: String.t(),
  cell_id: String.t(),
  scope: ReactiveDag.Attestation.Scope.t(),
  who: String.t(),
  polarity: polarity(),
  reason: String.t() | nil,
  basis: String.t(),
  basis_version: pos_integer(),
  signed_at: DateTime.t(),
  meta: map() | nil
}

Functions

affirm(cell_id, scope, who, opts \\ [])

@spec affirm(String.t(), ReactiveDag.Attestation.Scope.t(), String.t(), keyword()) ::
  {:ok, record(), [String.t()]}

Record that who AFFIRMS scope of cell_id as it currently stands. The basis is digested from the rows the scope selects right now (pass rows: to supply them, e.g. in a transaction that just read them). reason: is optional on an affirmation. meta: (a map) rides on the record and comes back on every read — host context like a ticket id or a UI origin. actor: is passed through to Ash — with a who_from_actor on the record resource, the signer is derived from it.

Returns {:ok, record, changed_leaf_keys} — the changed keys are for marking the store's leaf cell dirty (leaf_cell/0).

history(cell_id, opts \\ [])

@spec history(
  String.t(),
  keyword()
) :: [record()]

Full append-only history for cell_id (optionally one scope), newest first.

leaf_cell()

@spec leaf_cell() :: String.t()

The id of the store's leaf cell (config :attestation_cell).

reject(cell_id, scope, who, reason, opts \\ [])

@spec reject(
  String.t(),
  ReactiveDag.Attestation.Scope.t(),
  String.t(),
  String.t(),
  keyword()
) ::
  {:ok, record(), [String.t()]}

Record that who REJECTS scope of cell_id as it currently stands. reason is REQUIRED: an affirmation asserts the data as presented, but a rejection asserts it is wrong — a bare "no" leaves whoever must act with nothing to fix and an auditor with an unexplained refusal. (The resource's :sign action enforces the same rule for writes that bypass this module.)

stances(cell_id)

@spec stances(String.t()) :: [record()]

Every signer's current STANCE on cell_id's scopes: the most recent record per (scope, who). This is the store's read for evaluation — history stays in the table; only the latest word per signer per scope has force.

withdraw(cell_id, scope, who, opts \\ [])

@spec withdraw(String.t(), ReactiveDag.Attestation.Scope.t(), String.t(), keyword()) ::
  {:ok, record(), [String.t()]}

Record that who WITHDRAWS their word on scope — "I no longer vouch", which is a different act from rejecting ("the data is wrong"). A withdrawal supersedes the signer's previous stance and itself carries NO force in either direction: the scope returns to unaffirmed (pending / re-askable), never to refused. reason: is optional — withdrawing asserts nothing about the data, so there is nothing that must be explained.