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.
A stance's force
A record is immutable history; whether it COUNTS is computed here, and it can fail three independent ways, each meaning something different:
:basis— the WORLD moved: what was signed is not what is there;:tolerance— TIME passed: the assertion has aged out;:eligibility— AUTHORITY moved: the licence to sign was withdrawn.
All three read as not-in-force — never as green, never silently as rejected —
but the failed predicate is reported (lapses), because the remedies differ
and a UI must say which is being asked for. The predicates apply to BOTH
polarities: a rejection decays and loses authority exactly as an affirmation
does (everything decays; only the timescale is policy).
Admission
:refused— an in-force rejection exists. Conservative and sticky by design: it is not out-voted by affirmations. What clears it is the world changing (the data is corrected → its basis moves → the rejection lapses like anything else) or the rejector's own later affirmation (stance = latest record per signer). A system that re-asks until it gets a yes is laundering attestations, not collecting them.:affirmed— no in-force rejection, and in-force affirmations satisfy the quorum over the CURRENTLY-eligible set. An empty eligible set can never affirm — nobody-may-sign must not read as signed.:pending— neither: awaiting a signer (or every past signature has lapsed — seelapsesfor why).
Summary
Functions
Evaluate a :key-scoped requirement over a raw cell's rows: one admission
per row, in row order. eligibility_keys are the signers cell's current
keys; stances are ReactiveDag.Attestation.stances/1 for the raw cell.
Evaluate ONE scope against the rows it currently selects. The general entry —
evaluate/5 maps it over a cell's rows; a filter-scoped (set-level,
completeness) claim calls it directly with Scope.select/2'd rows.
Types
@type admission() :: %{ scope: ReactiveDag.Attestation.Scope.t(), state: :affirmed | :pending | :refused, signers: [String.t()], reasons: [%{who: String.t(), reason: String.t()}], lapses: [%{who: String.t(), lapse: :basis | :tolerance | :eligibility}] }
Functions
@spec evaluate( [map()], [map()], [String.t()], ReactiveDag.Attestation.Requirement.t(), DateTime.t() ) :: [admission()]
Evaluate a :key-scoped requirement over a raw cell's rows: one admission
per row, in row order. eligibility_keys are the signers cell's current
keys; stances are ReactiveDag.Attestation.stances/1 for the raw cell.
@spec evaluate_scope( ReactiveDag.Attestation.Scope.t(), [map()], [map()], [String.t()], ReactiveDag.Attestation.Requirement.t(), DateTime.t() ) :: admission()
Evaluate ONE scope against the rows it currently selects. The general entry —
evaluate/5 maps it over a cell's rows; a filter-scoped (set-level,
completeness) claim calls it directly with Scope.select/2'd rows.