Skip to content

Conformance (normative)

This page defines what it means for an implementation to conform to Docket's receipt format, and how a claim of conformance is checked. It is normative: the word MUST carries the meaning it has in docket-receipt-1.md, and a claim that departs from this page is not a conformance claim.

Conformance is a claim about refusal as well as acceptance. An implementation that accepts every valid receipt but also accepts a receipt whose totals do not sum has not implemented the format; it has implemented a JSON reader.

1. The check-id registry

Every verdict an implementation reports is named by an id from registry.json. The registry defines, for each id, the single assertion that id makes. Implementations do not invent ids, and do not reuse an id for a second assertion: an id is a promise to a machine consumer, and a name that means two things means nothing.

The registry also carries a capability table — which ids each shipped implementation emits. That table is derived from observed output (see §4), not maintained by reading source.

An implementation MUST report every assertion it performs under the registry's id for that assertion, with one of three statuses:

status means requirement
pass the assertion holds the detail MUST name the evidence (a count, a hash prefix, a recomputed value)
fail the assertion does not hold the detail MUST name what disagreed, and with what
skip the implementation looked and could not judge the detail MUST say why, and MUST NOT be counted as a pass

An id an implementation does not perform is not a skip. A skip asserts "I looked"; a missing id asserts nothing, and a consumer that averages over statuses would be misled by the difference. Such ids are declared in the registry's capability table under <implementation>_conditional, with a stated reason, and a conformance run reports them as not-judged.

2. The corpus

path contains rule
spec/vectors/*.json valid documents and their digests a conforming implementation MUST accept every one (exit 0, ok: true)
spec/vectors/invalid/NN-name.json a document that MUST be refused, or MUST be accepted a refusal is a failing check or a parse-level rejection (§3); an acceptance is marked "accept": true
spec/vectors/invalid/NN-name.expected.json the expectation names the id that must fail, the rule it enforces and the mutation applied
spec/vectors/invalid/base.ledger.jsonl, ledgers/*.jsonl ledgers the vectors are verified against an expectation names its ledger, or null

Every negative document is a complete receipt, not a diff: a third party must be able to point their own verifier at the file without running any of this repository's code. The corpus is generated by spec/vectors/invalid/build.py, which derives each vector from a valid one, applies exactly one stated mutation, and then runs the reference verifier and refuses to write the corpus unless the first failure is the id the expectation names. build.py --check re-derives all of it and fails if the committed bytes differ.

3. Parse-level rejection

Some documents are never read far enough to produce a check. The format MUST refuse, before any check runs:

  • a byte sequence that is not UTF-8;
  • a document that is not JSON (including NaN and Infinity, which JSON does not have — RFC 8259 §6);
  • an object that names the same member twice (RFC 8259 §4 leaves the meaning undefined and this format does not guess which value was meant);
  • a document that is not a JSON object at all — this one is reported as the check receipt-format, because there is an object to ask about even when the answer is "not a receipt".

A parse-level rejection is signalled by a non-zero exit with no JSON verdict on stdout, and a one-line reason on stderr naming the offending construct. A consumer MUST treat "non-zero exit and no verdict" as refused, not as a crash; a consumer that reads only stdout sees nothing and cannot tell the two apart. tools/conformance.py implements this rule, and the corpus exercises it with five vectors.

4. The expectation file

{
  "schema": "docket/conformance-expectation/v1",
  "vector": "18-totals-mismatch.json",
  "ledger": "base.ledger.jsonl",
  "check": "totals",
  "reject": null,
  "accept": null,
  "rule": "spec §8 item 5",
  "mutation": "totals.cost_usd does not equal the sum of the rows"
}

Exactly one of three outcomes is named:

  • check — the id that must be the first failing check;
  • reject — the document must be refused before any check runs (§3). Its reason_any_of is a list of tokens the reason on stderr may name, and every token must appear in the document. For damage with no nameable token (a BOM, trailing bytes, invalid UTF-8) the list is empty: the requirement is that a reason is stated, not that it is phrased the way this repository phrases it. A corpus written before a third implementation exists must not encode the first two's vocabulary;
  • ledger_reject — the receipt is valid and the ledger is malformed (a duplicated name, a half-written last line): the pair must be refused with a reason, and the reason may only be required to name something in the ledger. A verifier that repairs a ledger — drops the half-line, keeps the last of two names — is reporting on a document nobody wrote;
  • accept — true when the document is unusual but the format requires reading it (4e0 where an integer is expected, -0, a decomposed Unicode form). These exist because a corpus that only says what to refuse lets two implementations disagree about what to read, which is the first ambiguity a second implementer meets.

rule cites the normative sentence the vector enforces; mutation says what was changed. An expectation without a rule is a fixture, not an argument, and tests/test_conformance.py refuses one.

Why "first failing check" and not "any failing check": almost any corruption breaks several assertions (a doctored ledger breaks the chain, the root and every inclusion proof). Naming the first is what makes the vector a statement about where the failure belongs — a verifier that reports the symptom instead of the cause has a real defect, and the corpus should say so rather than accept any failure it can get.

5. Running a conformance check

$ make conformance                                    # both implementations shipped here
$ python3 tools/conformance.py --cmd "my-verify {file} --json{ledger_flag}"
$ python3 tools/conformance.py --capabilities --python --node

{file} is replaced with the vector, {ledger_flag} with --ledger "<path>" when the expectation names a ledger (and with nothing when the vector has none). The command's verifier MUST print the shared JSON shape — {ok, checks: [{name, status, detail}]} — because a corpus cannot judge a verifier whose verdicts it cannot read.

Four outcomes are reported, and the first is the only one that is fatal to a conformance claim:

outcome meaning
wrong-ok a document that must be refused was accepted. No amount of other passes compensates.
wrong-check refused, but by a different id than the vector names — usually a real finding about where the failure is reported
not-judged the implementation skips the id, or does not emit it at all
pass accepted when it must be, or refused by the named id

An implementation that reads a byte sequence which is not UTF-8, or silently replaces an invalid byte, has not verified the document on disk — it has verified a repaired copy of it. The Node verifier did exactly that (readFileSync(path, "utf8") substitutes U+FFFD) until 33-invalid-utf8 caught it; decoding is now fatal.

--allow-gaps exits 0 when nothing is wrong and some vectors were not-judged. That is the honest intermediate state of an implementation that declares its gaps; it is never a full conformance claim.

6. What conformance does not establish

  • Not correctness of the prices. A conformance check proves the arithmetic was recomputed from the embedded table, not that the table was right. Compare price_table.digest with a copy you trust.
  • Not completeness of a ledger. A chain that verifies proves the events that are there were not altered; §9 of the format spec is the receipt's own statement about what it cannot see, and limits-disclosure only checks that the statement is present.
  • Not agreement between implementations on documents outside the corpus. The corpus is 2 valid documents plus 42 in spec/vectors/invalid/ (38 refusals, 4 required acceptances), 44 vectors in all; it is a floor on behaviour, not a census. Differential fuzzing is the tool for the space between the vectors.
  • Not a certificate. A conformance run is evidence about one implementation, at one commit, on one corpus revision. Print the commit and the corpus digest beside the result, as the release record does.

Vocabulary changes are recorded in registry.json under history, with the reason each one was made. A change to an id is a change to this page.