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
NaNandInfinity, 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). Itsreason_any_ofis 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—truewhen the document is unusual but the format requires reading it (4e0where 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.digestwith 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-disclosureonly 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.