ReactiveDag.Attestation.Evaluation (reactive_dag v0.16.0)

Copy Markdown View Source

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 — see lapses for 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

admission()

@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

evaluate(raw_rows, stances, eligibility_keys, req, now)

@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.

evaluate_scope(scope, selected_rows, scope_stances, eligibility_keys, req, now)

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.