Decision Record

Check it without trusting us.

A decision record is a directory. It holds the decisions a deterministic procedure made, the policy it applied, the inputs it was given, and a signature over all of it, written so that someone who does not trust whoever produced it can check it.

What it is

A format, not a service

There is nothing to call and no account to hold. A record is files on disk, and the verifier is a command that reads them and exits 0, 1, 2 or 3. Both halves are published, so a reader runs the check on their own machine against their own copy, holding the directory and a key registry and nothing else.

The procedures it records are deterministic ones: an authorisation check, a tool-call gate, a rate limiter: anything whose answer is a function of its inputs and its policy. Not a model. That restriction is what makes a decision re-derivable by a reader rather than merely attested by its author.

What a reader checks

Fourteen properties, four states

The verifier reports each property separately and in one of four states: established, failed, unknown, or not applicable. The verdict is the worst state present.

Unknown is not a pass. A property nobody could check is not a property that holds, and the format refuses to round it up. That one rule is why the output below is worth reading in full rather than skimming for a green tick.

What you are checking for

Somebody handed you a record. Now what?

Every row below is a record built on purpose and run through the verifier, with the output captured rather than written. The whole set is examples/acceptance/ACCEPTANCE.md, produced by bash examples/acceptance/run.sh.

What you suspect What the verifier reports
Nothing. You are checking because you were asked to rely on it. Every applicable property established, and the ones it could not check named as not applicable rather than passed.
A row was altered after the fact. FAILED on the chain, naming the row and printing both hashes. Ten dependent properties report UNKNOWN rather than a result computed over rows nothing trusts.
Rows were deleted and the whole thing re-signed. The chain re-derives and it looks clean. A self-signed record cannot bound what its producer left out, and the verifier says so rather than implying otherwise.
… and you hold a receipt for one of the deleted decisions. Refuted. A receipt held by the subject of a decision, naming a decision the record lacks, is the one mechanism that works against the producer.
The “independent” witness is the producer under a second name. NOT ESTABLISHED at exit 1, not FAILED at exit 2. Nothing is wrong; something a reader needs is missing, and the verifier compares key material rather than believing the label.
The record is honest about a call that never happened. ESTABLISHED, and wrong. No property of a record can check this, which is why the record is published rather than described.
… with an attester holding a key upstream of the producer. FAILED on the one row whose input was replaced. Every other property is unchanged, because an attester signs what arrived, not what was decided.

The pattern in that table

Everything a producer can do alone, the producer can do convincingly. Every row that ends in a refusal needs a second party: somebody holding a receipt, a witness whose key is not the producer's, or an attester in front of the decider. That is the finding, and it is why a vendor signing its own logs, which is what this project built first, is the arrangement that establishes least.

Run it

Three commands, on your machine

Python 3 and one dependency.

dr.verify

git clone https://github.com/SDL-HQ/sdl-decision-record.git
cd sdl-decision-record && pip install cryptography
python3 -m dr.verify examples/out/toolgate-production \
  --registry examples/keys/registry.json

What that prints

examples/out/toolgate-production

  OK      document       format_version 1 shape, and payload_hash recomputes from the canonical payload
  OK      signing_trust  key 'example-2026-10' is active in the registry
  OK      signature      the signature verifies over the payload
  OK      chain          40 rows re-derive from GENESIS to the signed terminal hash
  OK      decisions      40 rows written, by outcome {'ALLOW': 21, 'DENY': 19}, one decider, 1 policy; a count of what the producer wrote, not of what happened
  OK      profile        the rows support the declared profile 'production'
  n/a     correctness    the production profile carries no correctness property: the rows have no labels and the record claims none
  OK      retained       2 retained file(s) present with their digests, inputs match the rows, policy matches the rows
  n/a     replay         not requested; pass --replay <decider module> to re-decide the retained inputs
  n/a     invocation     not claimed: no decider attestations are retained, so the rows are the producer's word on what the decider said; replay checks the words, not that the decider was called
  n/a     stream         the record is not part of a stream
  n/a     completeness   not claimed and not establishable: the 40 rows are what the producer wrote, and nothing in a self-signed record bounds what it did not write. A receipt (--receipt) for a decision the record lacks refutes it
  n/a     receipts       no receipts supplied
  n/a     witness        no witness statement supplied; a record witnesses nothing about itself

VERDICT: ESTABLISHED

Reading it

The interesting lines are the ones that say no

The seven established lines are integrity: the payload hashes, the signature verifies against a named key, forty rows re-derive from GENESIS to the terminal hash the signature covers, and the retained files match the rows that reference them.

The seven remaining lines are the part worth reading. One of them says that completeness is not claimed and not establishable. The forty rows are what the producer wrote, and nothing inside a self-signed record bounds what it did not write. Another says that the rows are the producer's word on what the decider said, because no decider attestation was retained.

Most evidence formats describe what they prove. This one enumerates what it does not, because a record is not permitted to grade itself: the judgement belongs to the reader, and the record's job is to supply material they can judge. A count of decisions is described as "a count of what the producer wrote, not of what happened" in the verifier's own output, not in a footnote.

How to refuse one

The repository ships the procedure for not believing it

Under examples/acceptance/ there are deliberately forged records, the commands that catch each one, and the output each command actually produces, captured rather than written. A record whose rows fail to re-derive reports every property downstream of them as UNKNOWN, giving the reason not computed: the rows were not established rather than guessing.

That directory exists because an evidence format whose author only publishes the cases it survives has published advertising. The acceptance procedure is how a reader refuses a record, written by the party who would rather it were accepted.

Here is the case that matters most, because it is the one the first transcript said it could not rule out. A record with two rows removed, checked against a receipt the producer itself issued for one of them at decision time, to the party that decision was about:

dr.verify, with a receipt

python3 -m dr.verify examples/acceptance/out/truncated \
  --registry examples/keys/registry.json \
  --decider-registry examples/keys/deciders.json \
  --receipt examples/acceptance/out/receipts/receipt-000020.json

What that prints

examples/acceptance/out/truncated

  OK      document       format_version 2 shape, and payload_hash recomputes from the canonical payload
  OK      signing_trust  key 'example-2026-10' is active in the registry
  OK      signature      the signature verifies over the payload
  OK      chain          18 rows re-derive from GENESIS to the signed terminal hash
  OK      decisions      18 rows written, by outcome {'ALLOW': 9, 'DENY': 9}, one decider, 1 policy; a count of what the producer wrote, not of what happened
  OK      profile        the rows support the declared profile 'production'
  n/a     correctness    the production profile carries no correctness property: the rows have no labels and the record claims none
  OK      retained       3 retained file(s) present with their digests, inputs match the rows, policy matches the rows
  n/a     replay         not requested; pass --replay <decider module> to re-decide the retained inputs
  OK      invocation     every one of the 18 rows carries a decision the decider signed under ['example-decider-2026-10'] at decision time, bound to this record and sequence; the producer could not have written these rows without the decider
  OK      stream         first record of stream 'acceptance'; nothing prior to link to
  FAILED  completeness   refuted: the producer signed a receipt for decision seq 20 of this run and the record has 18 rows, so at least 2 acknowledged decision(s) are not in it
  FAILED  receipts       receipt-000020.json: the producer acknowledged decision seq 20 of this run at 2026-10-18T01:00:19Z; the record has 18 rows
  n/a     witness        no witness statement supplied; a record witnesses nothing about itself

VERDICT: FAILED

Three things to notice

Every integrity property passes. The payload hashes, the signature verifies, eighteen rows re-derive to the terminal hash, and the retained files match. A record can be internally flawless and still be refuted, which is why integrity is reported as seven separate properties rather than as a verdict.

And invocation is established here, where the first transcript reported it not claimed. This producer retained the decider's own signatures, so the verifier can say that the producer could not have written these rows without the decider. That is the difference retaining an attestation makes, stated by the tool rather than argued for on a page.

Last, the version number is a feature and not a generation. format_version is 1, or 2 when a record carries a stream block, which is the only thing version 2 adds. The first transcript declares version 1 and reports stream as not applicable; this one is the first record of a stream named acceptance and declares version 2. Both are current. A verifier judges each record by the rules of the version it declares and reports a version it cannot read as unknown, rather than guessing.

And this verifier

If you do not trust the checker either, write one

The headline on this page is only honest if a reader can build their own verifier from the specification. So that was measured rather than asserted. A second verifier was written from SPEC.md alone, prevented by construction from importing the reference implementation, and run against a conformance corpus of forty-seven cases.

It disagreed on five of them. Those five disagreements are the result; the agreements bound nothing, because both implementations have one author. Each disagreement named a sentence in the specification that was not specific enough to implement from, and the specification was corrected. The measurement and its limits are published alongside the code, including the sentence that would close each remaining reading.

The limit

What no record can establish about itself

Nothing inside a record proves that the retained input was the real call. A producer can write a wholly honest record of a question nobody asked: the chain re-derives, the signature verifies, the decider computes the stated answer from the stated input, and every property reports established. A limit nobody has exhibited is a limit nobody believes, so here is that record, committed to the repository with the keys that verify it.

Bounding it requires a party outside the producer: a witness, a receipt held by the decision's subject, or an attester upstream of the decider holding a key the producer does not. The specification says so at each point where it applies, and says what each of those parties does and does not establish.

dr.verify, over the artefact

python3 -m dr.verify experiments/rederivability/out/f5-input-fabricated \
  --registry experiments/rederivability/out/registry.json \
  --decider-registry experiments/rederivability/out/deciders.json

What that prints

experiments/rederivability/out/f5-input-fabricated

  OK      document       format_version 1 shape, and payload_hash recomputes from the canonical payload
  OK      signing_trust  key 'rederivability-test-producer' is active in the registry
  OK      signature      the signature verifies over the payload
  OK      chain          40 rows re-derive from GENESIS to the signed terminal hash
  OK      decisions      40 rows written, by outcome {'ALLOW': 17, 'DENY': 23}, one decider, 1 policy; a count of what the producer wrote, not of what happened
  OK      profile        the rows support the declared profile 'production'
  n/a     correctness    the production profile carries no correctness property: the rows have no labels and the record claims none
  OK      retained       3 retained file(s) present with their digests, inputs match the rows, policy matches the rows
  n/a     replay         not requested; pass --replay <decider module> to re-decide the retained inputs
  OK      invocation     every one of the 40 rows carries a decision the decider signed under ['rederivability-test-decider'] at decision time, bound to this record and sequence; the producer could not have written these rows without the decider
  n/a     stream         the record is not part of a stream
  n/a     completeness   not claimed and not establishable: the 40 rows are what the producer wrote, and nothing in a self-signed record bounds what it did not write. A receipt (--receipt) for a decision the record lacks refutes it
  n/a     receipts       no receipts supplied
  n/a     witness        no witness statement supplied; a record witnesses nothing about itself

VERDICT: ESTABLISHED

Established, and wrong

Forty rows re-derive to the signed terminal hash. The decider signed every one of them at decision time under a key the producer does not hold. The retained inputs match the digests the rows cite. The verdict is ESTABLISHED and one of those inputs is a call the system never handled.

Nothing in the record is false. Each property reports exactly what it checked, and none of them checks the one thing that is wrong, because no property of a record can. This is the roof, and it is committed to the repository with the keys that verify it so that a reader meets it as an artefact rather than as a paragraph.

Here is the same substitution with an attester in front of the gate, signing each input as it arrives under a key held by the customer and numbering it itself:

verify_arrival.py, with the attester's own log

python3 tools/verify_arrival.py examples/acceptance/out/fabricated \
  --registry examples/acceptance/out/fabricated.producer.json \
  --decider-registry examples/acceptance/out/fabricated.decider.json \
  --attester-registry examples/acceptance/out/fabricated.attester.json \
  --arrival-log examples/acceptance/out/fabricated.attester-log.jsonl

What that prints

examples/acceptance/out/fabricated

  OK      document            format_version 1 shape, and payload_hash recomputes from the canonical payload
  OK      signing_trust       key 'producer' is active in the registry
  OK      signature           the signature verifies over the payload
  OK      chain               20 rows re-derive from GENESIS to the signed terminal hash
  OK      decisions           20 rows written, by outcome {'ALLOW': 10, 'DENY': 10}, one decider, 1 policy; a count of what the producer wrote, not of what happened
  OK      profile             the rows support the declared profile 'production'
  n/a     correctness         the production profile carries no correctness property: the rows have no labels and the record claims none
  OK      retained            4 retained file(s) present with their digests, inputs match the rows, policy matches the rows
  n/a     replay              not requested; pass --replay <decider module> to re-decide the retained inputs
  OK      invocation          every one of the 20 rows carries a decision the decider signed under ['decider'] at decision time, bound to this record and sequence; the producer could not have written these rows without the decider
  n/a     stream              the record is not part of a stream
  n/a     completeness        not claimed and not establishable: the 20 rows are what the producer wrote, and nothing in a self-signed record bounds what it did not write. A receipt (--receipt) for a decision the record lacks refutes it
  n/a     receipts            no receipts supplied
  n/a     witness             no witness statement supplied; a record witnesses nothing about itself
  FAILED  arrival             seq 4: the retained input is not what arrived; arrival 4 was signed over a different input
  UNKNOWN arrivals_accounted  arrival is failed, so the range the rows name cannot be trusted

VERDICT: FAILED

What the attester does and does not add

The same record now fails, on one line, at the row whose input was replaced. Every other property is unchanged, because the attester has nothing to say about a decider: it signs what arrived, not what was decided.

And the roof moves up rather than away. The attester is a party whose interest runs against the producer's, which the format can check only to the extent of comparing key material. Whether that party would in fact dispute the producer's account is organisational, and the specification says so at the point where it matters.

Specification

SPEC.md defines the format, the properties and the four states, and is written to be implemented from without reading the reference code.

Emitter and verifier

Both halves published, with a conformance corpus and captured transcripts that reproduce byte for byte. The terms it is published under are stated in the repository, in one place.

Worked records

Twenty-four records from seven deciders across two policy grammars, with their policies and inputs retained so each decision can be re-derived.

Read the source

github.com/SDL-HQ/sdl-decision-record carries the specification, the verifier and writer, the conformance corpus and the acceptance procedure. Questions to info@structuraldesignlabs.com.