runward

RW™ · V0.44.0

Docs · Décisions · ADR-0095

ADR-0095.

`check --json` carries the ratification row by row, the causes by kind, and the assurance mode in force

Date: 2026-10-13 Status: accepted (2026-10-13) — class R under ADR-0088 decision 2; the maintainer accepted the record with the recommended answers: the per-row list is ratification.byRow, compared separately by verify; by/for are copied as the tree declares them, pseudonymisation belongs to the exporter (D2). An agent wrote this record; the maintainer decided (ADR-0088 decision 4, applied to decisions as to rows) Deciders: the maintainer Method: a fresh clone of stranxik/runward at main@958d068 (runward 0.43.1), read on 2026-10-13: src/lib/conformance.ts (ratificationLedger, RatificationLedger, unboundRatifications, UnboundCause, UNBOUND_CAUSE_TEXT), src/lib/check-contract.ts (conformanceRows, delegationPayload, the machine payload), src/commands/verify.ts (the cmp calls and the additive-key rule), src/lib/witness.ts (NOT_WITNESSED), ADR-0066, 0080, 0082, 0088, 0089, 0090, 0093. One measurement: node dist/cli.js check --strict --json on runward's own mission, whose top-level keys, ratification object and delegation block are quoted below Relates to: ADR-0093 (prerequisite I0, decision 6), ADR-0066, ADR-0080, ADR-0088, ADR-0089, ADR-0090 Would amend, by name, once accepted: none. ADR-0090 carries a dated note pointing here; its decision 2 already names delegation.assurance, and this record fixes only its machine shape.

Context

What a consumer can read today, measured. On runward's own mission, check --strict --json has 26 top-level keys. Ratification is a block of counts:

"ratification": { "rows": 48, "lineByLine": 2, "enBloc": 46, "blind": 0, "untraced": 0 }

plus, only when non-zero, agent, agents[], singleAccountable, changed and changedRows[]. Which row was ratified, how, by whom, on which digest, is not in the payload: it is in the Markdown of the five gated deliverables. Per-row causes exist only under the regulated tier (regulated.unbound[], eleven UnboundCause values).

The causes of a red verdict are rows of conformance[], each with a kind (rule-violation, evidence-refused, unbound-row, the charter kinds…), and gaps gives counts per scope (deliverables, conformance, hooks, deferred, proposed, unboundRows, charter). No field groups the causes by kind.

The assurance mode is absent. ADR-0090 decision 2 names delegation.assurance on check --json. Measured: the delegation block carries file, banner, stage, accountable, delegates, classes, effective, expires, preMerge, sample, gaps, and no mode and no degraded flag. It arrives with the mode: field of ADR-0090's work order, not yet built.

Why it matters now. ADR-0093 rule R1: no view computes a state, every count comes from runward with the list it counts. Without these fields, the Markdown summary (D1), the change row (D2) and the period (D3) would each have to re-read the deliverables' Markdown: a second reader of the same rows, so a second verdict that can disagree with the first.

Two constraints the payload already imposes.

  • ratification.rows is the count. A list cannot take that key without breaking every consumer of ADR-0030's contract.
  • verify compares ratification as a whole object (cmp("ratification", …)). A list added inside it would make every attestation sealed by 0.43.x fail to verify against a newer tree reading. The precedent for the fix exists: evidence.proseRows is compared on its own, only when present (RWD-2026-0150).

The witness does not carry the ratification ledger (NOT_WITNESSED.disclosures), so nothing here changes the witness or its checker.

Decision

Three additive fields, each a pure function of what the verdict already reads, each re-derived by verify, none counting toward the verdict.

  1. The per-row list, under the key chosen in question 1 (recommended ratification.byRow), sorted by gated deliverable in phase order, then manifest order. One element per decided row:

    { "deliverable": "architecture.md", "rule": "…", "phaseId": "…",
      "state": "ratified | changed | untraced | blind",
      "mode": "line-by-line | en bloc | blind | agent | agent (single accountable) | null",
      "by": "…", "for": "…", "date": "YYYY-MM-DD",
      "bound": "<digest> | null", "current": "<rowDigest>",
      "unboundCause": "<UnboundCause> | null" }
    

    state follows the latest entry, as ratificationLedger and unboundRatifications already do; unboundCause is filled only when the regulated tier is on, so no other mission's reading changes; names are copied as the tree declares them (question 2). The counts stay where they are and must equal the list's counts, asserted by a test.

  2. gaps.causesByKind, strict only (like conformance[]): for each kind present, { count, indices }, the indices pointing into conformance[]. A count never travels without its list.

  3. delegation.assurance, present when a charter is: { declared, inForce, degraded, notInForce[] } and the sentence ADR-0090 decision 2 fixes at the character. Its computation is ADR-0090's; this record fixes the machine shape so records (ADR-0093 decision 6) can copy it. Until it ships, consumers read not-observed, never a default mode.

Re-derivation. verify compares each new field on its own, only when present in the attestation, like evidence.proseRows: an attestation sealed before the fields existed keeps verifying, and one sealed with them and edited is caught. Stripping all three passes, like deleting any additive field: it removes information, never adds any.

Path. One pull request on the pre-approval path, its golden payloads regenerated in the same commit, check --strict --json byte-identical across two runs, and no exit code changed on any fixture of the attack corpus.

Alternatives discarded

  • Reuse ratification.rows for the list. Breaks every consumer reading the count (ADR-0030).
  • Let each renderer read the Markdown. A second reader of the rows; ADR-0064's own "what would settle it" forbids a rendering that computes.
  • Put the list inside ratification and keep the whole-object compare. Every 0.43.x attestation would fail verify on an unchanged tree; an older producer is not a liar.
  • Group causes in each view. Rule R1 of ADR-0093: the grouping is runward's, published with its list.
  • Compute the assurance mode here. ADR-0090 owns it; two computations of one mode can disagree.

Consequences

  • Positive. Every number the D1 summary prints is a JSON path of the payload; the change row of D2 and the period of D3 read one source; the assurance mode becomes machine-readable the day it ships.
  • Negative, accepted. A larger payload (one element per decided row: 48 on runward's own mission); three more fields verify must keep re-deriving; declared names travel further than the Markdown.
  • On other boundaries. No verdict or exit code changes; the witness is unchanged.

What would settle it

Questions for the maintainer (each with the recommendation):

  1. The key and its place. (a) ratification.byRow, compared on its own by verify; (b) a top-level ratificationRows. Recommended: (a): the ledger stays one block for a reader, and the evidence.proseRows precedent already shows verify handling a nested additive list.
  2. Names in the list. (a) by and for copied as the tree declares them; (b) canonical ids only, free names dropped. Recommended: (a): the names are already in the committed deliverables, and pseudonymisation belongs to the exporter (ledger export, D2), which knows the retention its reader needs; dropping them here would make the list disagree with the Markdown it summarises.

Evidence that would ratify or reverse it:

  • The list's counts equal ratification's counts on every fixture, and a planted mismatch reddens.
  • verify on a 0.43.1 attestation and an unchanged tree still exits 0; an edited byRow element exits non-zero.
  • The D1 Markdown golden test: every number in the summary is a JSON path into this payload.
  • Reversal: a consumer that cannot use the list without re-reading the Markdown, or a payload size measured as a problem on a real mission.

Reevaluation trigger (mandatory, dated)

Reopen on 2027-01-13, or earlier when: ADR-0090's mode: field ships with a shape other than the one above; the witness starts carrying the ratification ledger; or a consumer of check --json reports that a renamed or moved ratification field broke it.

Trigger set on: 2026-10-13 · Watched via: the payload golden tests, verify's additive-key tests, the D1 Markdown golden test.

References

  • src/lib/conformance.ts, src/lib/check-contract.ts, src/commands/verify.ts, src/lib/witness.ts at main@958d068.
  • ADR-0030 (the machine surface is a contract), ADR-0064, ADR-0066, ADR-0080, ADR-0082, ADR-0088 decisions 2 and 4, ADR-0090 decision 2, ADR-0093 decision 6 and rule R1.

Note, 2026-10-13: what the implementation measured

Recorded so a reader finds it here; it decides nothing. Decision 1 says the counts "must equal the list's counts". Measured on runward's own mission with the implementation: ratification.rows reads 48, byRow holds 46 elements. The counts count ratification entries (ratificationLedger adds each entry's rows), the list counts rows: async-job-guardrails (threat model) and handover-agents-charter-final (hand-over) each carry two entries. The counts keep their meaning (ADR-0030). The test pins the relation that holds (equal when every decided row carries one entry) and the one that does not (a row ratified twice adds two to the counts and one element to the list). Whether the counts should one day count rows is a question for the maintainer, not an act of this implementation. Decision 3: this version has no reader of ADR-0090's machine floor, so a machine mode is reported degraded with every control M1 to M8 named in notInForce.

← Tous les ADR