# aer1-ruby — AER-1 conformance verifier in Ruby

An independent, stdlib-only implementation of the AER-1 verifier
(draft-zambo-aer1-14) in Ruby 3.1. Written check-for-check against the
authority sources:

1. `aer-1/drafts/draft-zambo-aer1-*.txt` — the draft text (tiebreaker per
   IMPLEMENTING.md)
2. `aer-1/conformance.py` — the reference runner

Result on the corpus at `origin/main` (kit 1.4.x, commit 74e4693,
fetched 2026-10-09): **91/92 checks**. The single divergence is
`test-vectors/valid/bound-04.json`, a 0.4 receipt with no `metadata_hash`;
draft -14 §6 step 4 makes it a MUST-fail and the kit author confirmed the
draft outranks the (stale) corpus (Colony implementer thread, 2026-10-09).
This verifier follows the draft text. Full transcript: `RESULTS.txt`.

## Files

- `aer1.rb` — the verifier. Single file, stdlib only
  (`openssl`, `json`, `base64`). Exposes `AER1.check`,
  `check_reference_profile`, `check_disinterested_tier`,
  `verify_chain` (v06), `verify_chain_v07`, `verify_chain_commitment`,
  `merkle_root_canonical`, `verify_workflow`, and a CLI:
  `ruby aer1.rb <receipt.json>` exits 0 when the receipt conforms to the
  Section 3 core check, 1 with the reason(s) on stderr otherwise.
- `run_conformance.rb` — harness that runs every corpus vector and
  prints `[corpus] name: PASS/FAIL` per line plus a summary; exit 0 iff
  every vector behaves as the reference runner expects.
- `RESULTS.txt` — captured final harness output.

## Run

```sh
cd /workspace/deliverables/aer1-ruby
ruby run_conformance.rb                 # uses ../../research/zambo-aer1/aer-1
ruby run_conformance.rb /path/to/aer-1  # or point at any corpus checkout
AER1_DIR=/path/to/aer-1 ruby run_conformance.rb
```

## Coverage (90 vectors + 2 workflow verdicts)

| corpus                    | vectors | rule                                             |
|---------------------------|--------:|--------------------------------------------------|
| test-vectors/valid        |       8 | Section 3 core check: all verify                 |
| test-vectors/invalid      |      25 | core check: all rejected (reason per filename)   |
| test-vectors/invalid-profile |   1 | core-valid, rejected by reference-producer profile |
| test-vectors/anchored     |       5 | strict UUIDv4 id + anchor member checks          |
| chain-vectors.json        |       7 | v06 chain mechanics (regression gate)            |
| chain-vectors-v07.json    |      28 | v07 hardened entry-digest chain                  |
| commitment-vectors.json   |       8 | Section 7.3 external chain commitment            |
| merkle-vectors.json       |       8 | Section 8.1 canonical + legacy roots, +2 Section 8.2 workflow verdicts |

Plus the draft -13 binding checks (inner `receipt_schema_version`
agreement + `issuer_claims.provenance_class` binding; legacy 0.2/0.3
envelopes exempt) and the draft -14 §3.1 `metadata_hash` MUST
(recompute-and-compare when present).

## Fidelity notes (the parts a naive port gets wrong)

- **Python `json.dumps` canonical form** for the Section 7.1 entry
  digest: sorted keys, `","`/`":"` separators, `ensure_ascii` escaping —
  short escapes (`\n`, `\b`, `\t`, `\f`, `\r`, `\"`, `\\`), `\uXXXX`
  *lowercase* hex for C0 controls **and every codepoint >= U+007F**
  (U+007F escapes as "backslash-u-007f"), non-BMP codepoints as
  `\udXXX\udXXX` surrogate pairs. Ruby's `JSON.generate` does none of
  this, so `aer1.rb` carries a dedicated canonical serializer.
- **CPython `base64.b64decode(validate=True)` padding quirks**: excess
  trailing `=` is tolerated after a complete quad (`AAAA=` decodes),
  partial quads require exact padding (`AAA=` ok, `AAA==` rejected).
  `Base64.strict_decode64` rejects both excess cases — `aer1.rb`
  replicates CPython's accept/reject set exactly.
- **Regex anchors**: Python `\Z` = absolute end -> Ruby `\z` (not `\Z`,
  which tolerates a trailing newline; that is what `id-trailing-newline`
  and `provenance-trailing-newline` test).
- **5 vs 5.0**: Ruby `JSON.parse` preserves Integer vs Float, so the
  `entry_count`/`seq` lexical checks need no raw-bytes workaround
  (`x-entry-count-float`, `v07-seq-*` vectors).
- **Tier-check asymmetry** (mirrored from the reference): in the core
  check a non-UTF-8 `canonical_bytes` nulls the decoded payload so the
  `output_hash` comparison is skipped; in `check_disinterested_tier` the
  decoded bytes are retained and `anchor.leaf_hash` is still compared.
- Empty Merkle tree root = SHA-256 of the empty input.
- Core tier: `receipt_schema_version` only needs to be a non-empty
  string; `id` accepts any-version lowercase UUID (strict UUIDv4 is the
  disinterested-tier/strict profile rule).
- `provenance_class` pattern: `EXECUTED BY [A-Z0-9][A-Z0-9 ._-]*`,
  `OBSERVED VIA GATEWAY`, or `LOGGED BY AGENT` (note the space after BY).

## Result

87/87 vector verdicts agree with `python3 aer-1/conformance.py`,
including the exact first-failure reason on every rejection. See
`RESULTS.txt`.
