# Claims review — 0.2.44

- base: v0.2.43
- head: 150f134
- surface: sha256:9383a93c1e38d7031c88b62995af3d03222a7f650c29d4f9f75180f9d42e3c14
- verdict: pass
- reviewer: maintainer, running the four binaries against the four tables. The
  previous review of this defect was done by reading and missed half of it.

One surface changed: the four exit-code tables in `docs/RECEIPT-SPEC.md`. Two
rows added to each, three rows reworded to say what was read before a verdict
was given.

## Findings

### 1. docs/RECEIPT-SPEC.md — exit 1 is published on four tables and observed on none

- Claim: "| 1 | The tool failed unexpectedly — no verdict was reached |", now in
  the verifier, coverage, reconciliation and stamping tables.
- Mechanism: `process.exit(1)` in the `main().catch` handler at the foot of each
  of the four binaries, replacing `process.exit(20)`.
- Why it falls short: no CLI input reaches that handler. Four candidate triggers
  were tried against `conarium-verify` — a directory passed to `--anchors`,
  `/dev/null` passed to `--anchors`, a non-numeric `--expect-count`, a malformed
  `--expect-last-hash` — and every one was caught by the code that owns it and
  answered with its own exit code. That is the right result for a last-resort
  handler and it is also the reason the row is unmeasured: `exit_contract.mjs`
  runs twenty-one invocations and none of them produces a 1. The evidence for
  this row is that the line is short and reads correctly. **That is reading
  substituted for running, which is the substitution this release exists to
  correct** — 0.2.42 named the remaining misuse of 20 by reading the file, and
  the half it missed was found by executing it.
- Disposition: shipping, and said plainly rather than left to look measured. The
  changelog states that this one line is covered by reading rather than by a
  run. The alternative — an environment variable or hidden flag that forces a
  throw so a test can observe it — puts a deliberate crash path into a security
  tool to satisfy a checkbox, and that trade is worse than the gap.
- Second-order, recorded because it is the more useful half: **the drift guard
  went green through all of this.** `test/spec_exitcode_drift.mjs` compares the
  *set* of literal codes in the binaries against the set in the tables. `1` was
  added on both sides, so the sets matched. It was equally blind when `20` named
  two different events, which is how that survived to 0.2.42. A set comparison
  cannot see meaning, and nothing in the tree can — `exit_contract.mjs` narrows
  the gap by asserting what specific invocations return, but it asserts the
  cases someone thought of.

### 2. docs/RECEIPT-SPEC.md — "Nothing was read" was read as literally as it was written

- Claim, as first written in this release: "| 2 | The command could not be run
  as given — … Nothing was read, so nothing is being judged. |"
- Mechanism: the paths that now exit 2 are a `parseArgs` throw, a missing
  required argument, and a target that is absent or unreadable.
- Why it falls short: on several of those paths a file *has* been read. In
  `conarium-verify` and `conarium-coverage` the public key is loaded before the
  target is looked for, so `conarium-verify <missing> --pubkey <key>` opens and
  parses a key file and then exits 2. In `conarium-stamp` the "cannot read"
  branch has opened the target and failed partway. What is true — and what the
  code actually establishes — is narrower: the artefact under test was never
  opened. "Nothing" claimed the whole filesystem.
- Disposition: reworded in `150f134`, in all four tables. The row now says "The
  artefact under test was never opened".

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

**`conarium-coverage <missing declaration>` with no `--pubkey` is 13, and 13 is
a verdict code.** `loadVerifyKeys` runs before the declaration path is checked,
so a file that does not exist is answered with *signature invalid / pubkey
missing*. It is the same shape as the defect this release fixes, one argument
over, and `exit_contract.mjs` asserts it deliberately rather than by omission.

It 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 in the name of a
tidier table. The ordering is the fixable part — checking the target before the
key would answer the more specific question first — and it is not fixed here,
because reordering the fail-closed check is a change to the safety posture and
belongs in a release that is about the safety posture.

**Three tests had written the old answer down.** `test/reconcile_cli.test.mjs`
asserted 20 for an unparseable `--skew`, for no arguments at all, and for
`--json --json-v2` together. All three are `parseArgs` throws — no snapshot is
opened in any of them — so all three were asserting the defect. Their
expectations moved to 2 with the reason written beside each one, rather than in
a commit message nobody reads at the line where the question comes up.
