# Claims review — 0.2.45

- base: v0.2.44
- head: e67cac8
- surface: sha256:e9d01e4e6f99670242922cf7c7d960d85adb9ca786d238d8a5f4279bc1c3eb43
- verdict: pass
- reviewer: maintainer, correcting a disposition written in the previous review
  against an objection it had not read carefully enough

One surface changed: the four exit-code tables in `docs/RECEIPT-SPEC.md`. Every
`2` row was rewritten, the verifier's `20` row was widened, and both changes
came from measuring rather than from taste.

## Findings

### 1. The previous review answered an objection the reporter had not made

- Claim, in `docs/claims/reviews/0.2.44.md`: that a missing `--pubkey` returning
  13 "stays for the reason 0.2.42 gave for the verifier: 13 is fail-closed. A
  caller that has special-cased it is asserting *do not trust this result*, and
  moving a missing key to 2 would switch that assertion off."
- Mechanism: the report being answered. Joel Hillier's message of 22 August
  01:56 says, of the same behaviour, *"The refusal itself is right and I'd keep
  it, and the message says plainly what happened."*
- Why it falls short: the objection was never that the refusal should go. It was
  that the published description is false — `test-vectors/manifest.json`
  described 13 as *"signature invalid or missing while a pubkey was supplied"*,
  and 13 is what is returned when no pubkey was supplied — and that the check
  ordering decided which of two wrong answers a caller received. A disposition
  that defends a position the reporter already granted looks like an answer and
  settles nothing. Rejecting a finding is legitimate; rejecting a finding one
  has misread is not, and the second is harder to notice because it produces
  the same paragraph.
- Disposition: both halves are fixed in this release. The description now
  matches the specification's row, and the target is resolved before the key is
  loaded. The previous review's second reason — that reordering "belongs in a
  release that is about the fail-closed posture" — does not survive being
  checked either: fail-closed means never reporting success without verifying
  signatures, a missing target now exits 2, and a target that is present still
  requires a key. Nothing about the posture moved. It was a harmless reorder
  classified as a safety change, which is how work gets deferred for a reason
  that reads well and is not true.

### 2. docs/RECEIPT-SPEC.md — the `2` row has now been rewritten in three consecutive releases

- Claim, before this release: "The artefact under test was never opened, so
  nothing is being judged."
- Mechanism: the paths that reach 2. In 0.2.44 they were argument errors and
  absent targets, and the sentence held. This release adds a file that is valid
  JSON in the wrong shape, and reaching that verdict requires opening the file
  and reading all of it.
- Why it falls short: the sentence was true when written and was made false by
  the next commit that widened the code. This is the third wording — 0.2.42
  shipped "Usage error", 0.2.44 narrowed it to "the artefact under test was
  never opened" after that overclaimed, and 0.2.45 has now falsified that one.
  A row that has to be re-examined every time a path is added to its code is a
  row whose text is doing work the code should be doing.
- Disposition: reworded to what is true of every path — "No verdict was reached
  because the run could not begin" — and, more usefully, `test/exit_contract.mjs`
  now asserts the behaviour of each path rather than leaving the row to carry
  it alone. The pattern is recorded because the next widening of code 2 should
  start by asking whether the row survives it.

### 3. docs/RECEIPT-SPEC.md — the `20` row moved because a property test disagreed with the split

- Claim: "| 20 | The input was read as receipts and rejected — a receipt that
  failed the schema, or a file that is not valid JSON. A one-byte mutation of a
  signed chain lands here rather than on 2: the bytes were examined. |"
- Mechanism: `src/invariants.property.test.ts` mutates one byte of a valid
  signed chain and asserts the result is never 0, over a set of verdict codes.
- Why it falls short — of the first attempt, not the shipped text: the first
  version of this change sent *every* parse failure to 2, on the reasoning that
  no receipt had been read. The property test failed, and it was right to. A
  corrupted receipts file is not a caller who mistyped a command; it is the
  artefact being examined and rejected, which is a verdict, and answering 2
  would send an operator to check their arguments instead of their evidence.
  The distinction that decides it was already in the code — whether the file is
  valid JSON at all — and it had been wired to the wrong branch.
- Disposition: valid JSON in the wrong shape is 2; anything else stays 20, in a
  directory as well as out of one, because skipping a corrupt neighbour would
  make reporting evidence depend on whether the caller named a file or a folder.

## Not a finding, recorded so the next reader does not re-derive it

`test/exit_code_descriptions.mjs` is the mechanism this class has been missing,
and what it caught on its first run is the argument for it: code 2 described
differently by the manifest and the specification in the same release, and code
13 described by the manifest as something that is false in the one case it is
returned for. Neither was visible to `spec_exitcode_drift.mjs`, which compares
the *set* of codes in the binaries with the set in the tables — the same
blindness that let 20 name two events until 0.2.42.

Its own limit, stated: it checks that the three texts agree with each other. It
does not check that any of them is true of the binary. `exit_contract.mjs` asks
that question of the codes and cannot ask it of the sentences, and a run that is
green here means the descriptions are consistent, not that they are correct.
