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.RecordWrites 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
@type polarity() :: :affirm | :reject | :withdraw
@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
@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).
Full append-only history for cell_id (optionally one scope), newest first.
@spec leaf_cell() :: String.t()
The id of the store's leaf cell (config :attestation_cell).
@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.)
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.
@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.