The affirmation recorder

Drift derivation ends with a worklist: the directly and doubly outdated edges are where a human judgement is owed. The recorder is what turns one such judgement into records — and the deliberate smallness of that sentence is the page’s subject. The recorder composes; it persists nothing, decides nothing, and derives nothing. An operator runs it.

One judgement composes a pair

An affirmation has two effects on the case: a review event joins the record of judgements, and the affirmed edge’s record changes to active, carrying the hash the judgement binds. Both are composed by the recorder, in one call, from the same endpoint hashes:

composed = affirmation.compose(
    outdated_edge,                      # a derived record, from drift.derive
    from_node=current_nodes[outdated_edge.from_id],
    to_node=current_nodes[outdated_edge.to_id],
    role="SoftwareEngineer",
    reason="reviewed the change; the binding still holds",
    from_source_revision=impl_revision,  # supplied by the caller —
    to_source_revision=req_revision,     # the recorder runs no git
)
store.append_review_events([composed.event])
store.write_edges([composed.edge])

The pair exists because the store computes no hashes: it persists what it is handed, and the commitment layer is deliberately out of its reach. The affirmation’s effect on the edge record therefore has to be composed above the store, and composing it beside the event means record and event cannot disagree about what was affirmed — the event carries the endpoints’ named content hashes and anchors, and the edge’s hash folds the two node hashes computed from those same records. A workflow that affirms calls one component and hands the store both halves.

Those are the current records’ hashes — what the reviewer judged today — never the hashes the edge was last affirmed against, which would re-sign the past. The recorder derives the edge’s hash from the endpoint records it is handed and originates nothing itself: every judgement field is the operator’s, required, with no default. Even an empty reason must be given, never assumed.

What the recorder trusts, and what it refuses

The edge handed to compose is a derived record — the suspect detector’s output, carrying the state the recorded and current streams imply. The recorder reads that state and does not recompute it; a recorder that re-derived would be a second telling of link state, and the import layering forbids it the suspect detector for exactly that reason.

Of the six states, three are affirmable — pending, directly outdated, doubly outdated — because there a judgement about the edge’s own content is what is missing (SEG-SREQ-027). The refusals each point at the act that would actually resolve the edge: an active edge has nothing to affirm; a transitively suspect edge clears by recomputation once the change sites below it are re-affirmed (see Drift derivation); a broken edge needs its endpoint back before a judgement could bind anything. The recorder also refuses endpoint records that are not the named edge’s endpoints — hashes of some other node would compose a record about an edge nobody reviewed.

Source revisions are opaque strings here. The recorder and the library run no git. The command-line adapter reads repositories (ADR-0010, ADR-0015). So the caller supplies each endpoint’s revision, and their format is judged where the record is persisted — against the case’s own schema — not by the recorder.

Only a strong edge can be affirmed

The state gate above is a companion, not the whole test: affirmable and compose also gate on the edge’s kind, via propagating_edge_kinds() (SEG-SREQ-056). Confirms, Witnesses, Excuses and Calls are refused whatever their state, because they are machine-derived evidence, not something a human judgement is about. A Confirms or Witnesses edge is resolved by re-running the test that produced it; an Excuses edge (Waiver to TestOutcome, per case/schema/edge-excuses.schema.json) records a producer’s excusal claim, not a reviewer’s — and the state gate alone would not have caught this, since an evidence edge sits pending exactly like a strong edge genuinely awaiting its first affirmation. The refusal names the distinction directly: an evidence edge cannot be affirmed because re-execution, not judgement, is what would resolve it.

The case stores no evidence edge (The affirmation store’s write face), so the refusal is reached only for an evidence edge that a producer supplies in the current stream; the selection of an edge verb never finds one in a case.

affirmable therefore takes both the state and the kind:

affirmation.affirmable(edge.state, edge.kind)

and compose checks the kind before it ever asks about state, so the message an operator sees for an evidence edge names the actual reason (machine-derived, re-execution resolves it) rather than borrowing the state gate’s language. This is a companion to SEG-SREQ-027, not an amendment of it: every strong kind — Refines, Verifies, Implements — is governed by state exactly as before.

Composing is not affirming

compose is pure: same inputs, same values, no store touched, no identifier and no timestamp minted (position in the store is identity, and who/when/authority belong to the commit that introduces the record). The bytes the store then writes are a proposal on the working tree; they become an affirmation when a maintainer reviews and commits them. That boundary is the guarantee-boundary page’s subject; this component is built and tested as capability only.