# AER-1 Ruby port notes

Single-file, stdlib-only Ruby port of ARION's independent AER-1 verifier
(`../verify.js`, 951 lines) — check-for-check, including failure-reason
strings, check ordering, and CLI semantics. Same reference behavior the
Swift (`../swift/`) and Java (`../java/`) ports target.

## Files

- `aer1verify.rb` — the verifier. Ruby stdlib only: `json`, `digest`
  (SHA-256), `base64`, `find`. Runnable directly (`__FILE__ == $PROGRAM_NAME`
  main guard) or `require`d for library use — all checks are top-level
  methods mirroring verify.js's exports (`check_receipt`,
  `check_reference_profile`, `check_disinterested_tier`,
  `verify_chain_v07`, `verify_chain_v06`, `entry_digest_v07`,
  `final_entry_digest_v07`, `check_final_entry_commitment`,
  `verify_chain_commitment`, `merkle_root`, `verify_workflow_steps`,
  `check_workflow`, `canonical_json`, `verify_document`, `verify_file`,
  `decode_base64_strict`, `is_rfc3339`).
- `EXPECTED.md` — pinned per-file verdicts for both frozen corpora.
- `results.txt` — verbatim host run output.

## Run

```sh
ruby aer1verify.rb <file-or-dir> [--strict] [--chain|--chain06|--workflow|--receipt] [--profile] [--anchor] [--json]
```

`<dir>` scans recursively for `*.json` (sorted by path; dotfiles and
dot-directories skipped). Kind auto-detection, verdict format
(`PASS [kind] file` / `FAIL [kind] file — reason (+N more)`), tail
`CONFORMANCE: n/m`, and exit codes (0 all-pass, 1 any-fail, 2 usage)
match verify.js/Swift.

## Environment

Developed and run on ruby 3.1.2p20 (aarch64-linux-gnu). No gems, no
network. Should work on any Ruby ≥ 3.0 (uses `match?`, `Hash#key?`,
`String#scrub`, `unpack1` — all long-standing stdlib).

## Porting decisions / deviations

- **Numbers.** JS `JSON.parse` yields only doubles; Ruby yields Integer
  or Float. The loader normalizes every parsed number to `Float`, then
  `js_integer?` implements `Number.isInteger` (finite && == round) — so
  `seq: 2.0` is accepted and `entry_count: 5.5` rejected exactly as JS.
  Integers beyond 2^53 round to doubles at normalize time, as V8 does.
  `js_num_to_s` implements ECMA-262 Number::toString (shortest round-trip
  digits extracted from Ruby's `Float#to_s`; decimal for -6 < n <= 21,
  signed-exponent scientific otherwise) so canonical-JSON digests and
  failure messages match JS byte-for-byte (e.g. `1e21` → `1e+21`,
  `1e-6` → `0.000001`).
- **`entry_count: 5.0` lexical floats.** verify.js's own file loader is
  plain `JSON.parse`, so `5.0` arrives as integer-valued and is
  accepted; the `__float__` rejection exists for the kit runner, which
  marks `\d+\.\d+` literals at load time. This port reproduces verify.js
  semantics exactly (no marking in `verify_file`), while still honoring
  a `{"__float__": N}` marker object if a caller supplies one. On the
  shipped corpora the distinction never fires — the only float
  `entry_count` fixture is `5.5` (non-integer either way).
- **undefined vs null.** JS distinguishes an absent member from JSON
  `null`; Ruby nil conflates them. A private `UNDEFINED` sentinel plus
  `jv()`/`key?` preserves the distinction, so `canonical_json`
  raises "unserializable value" on an absent chain member but emits
  `null` for a present JSON null, exactly like JS.
- **Absent-member digest inputs.** `entry["close"]` uses
  `key? ? value : false` (`entry.close === undefined ? false : ...`).
- **JS truthiness.** `doc.timeline || doc.entries` is ported via
  `js_truthy?` — Ruby treats `0` and `""` as truthy where JS does not.
- **Regex anchoring.** All patterns use `\A..\z`: Ruby `$`/`^` would
  match before a trailing newline, which `id-trailing-newline.json` and
  `provenance-trailing-newline.json` pin as rejections. String-vs-other
  type checks gate every regex use, like `typeof x !== "string"`.
- **Base64.** Surface regex `^[A-Za-z0-9+/]*={0,2}$` + length % 4, then
  `Base64.decode64` — lenient about pad bits like Node's
  `Buffer.from(s,"base64")` (regex already gates alphabet/pad shape).
- **UTF-8.** Strict decode = `force_encoding(UTF_8).valid_encoding?`
  (fail closed, like `TextDecoder{fatal}`). SHA-256 input uses raw UTF-8
  bytes; a WTF-8/lone-surrogate string scrubs to U+FFFD first, matching
  V8's utf8 encoder.
- **Lone surrogates.** Ruby's json gem rejects unpaired `\uXXXX`
  escapes where JS accepts them. `normalize_surrogate_escapes` scans
  the raw text in string context and rewrites each *unpaired* surrogate
  escape to `\ufffd` (paired escapes untouched) before parsing, so the
  same documents reach the checks. The resulting string carries U+FFFD
  where JS carries a surrogate half; every ASCII-gated surface check
  rejects both identically. (This can only diverge if a lone surrogate
  reached a digest recompute — no corpus vector does.)
- **UTF-16 ordering / escaping.** `utf16_units` encodes to UTF-16BE and
  unpacks code units; object-key sorting compares unit arrays (JS string
  `<` order), and `escape_canonical_string` emits `\" \\ \b \f \n \r
  \t` plus `\uXXXX` for every unit <0x20 or ≥0x7F — covering C0, DEL,
  non-ASCII, and non-BMP as surrogate-pair escapes, matching
  `JSON.stringify` + the draft's ensure_ascii convention.
- **Canonical JSON numbers.** `js_num_to_s` covers all magnitudes; seq
  values that fail contiguity still digest-canonicalize identically to
  JS.
- **Anchor-tier quirk.** `check_disinterested_tier` returns
  `["anchor is not an object"]` *replacing* the accumulated id failure
  when the anchor isn't an object — a verify.js early-return quirk kept
  verbatim.
- **Directory recursion** (deliberate extension, same as the Swift
  port): `Find.find` recursive, hidden components pruned, sorted by
  path.
- **Parse errors** report `not valid JSON: <ruby message>`; unreadable
  files fail closed with the same verdict line (verify.js would crash;
  the Swift port documents the same choice).
- **`--json` report** uses `JSON.pretty_generate` (2-space indent), not
  JS's 1-space `JSON.stringify` indent.

## Verification performed

- Frozen corpora, verdict-identical to `swift/EXPECTED.md`:
  `CONFORMANCE: 7/30` (test-vectors) and `5/18` (frozen v1.3.0);
  every first-failing check matches.
- Differential sweep vs verify.js: every `*.json` under
  `kit-snapshot/` and `neg-corpus/` (incl. the 1,000-file fuzz corpus),
  each run through `verify_document` under 8 flag combinations plus
  granular `check_receipt`/`check_reference_profile`/
  `check_disinterested_tier`/`verify_chain_v07`/`verify_chain_v06`/
  `verify_chain_commitment`/`check_final_entry_commitment`/
  `merkle_root`/`verify_workflow_steps` sweeps (default and strict):
  **24,834 comparison lines, zero divergences**.
- neg-corpus manifest replay (Ruby port of `run.js`): output is
  byte-identical to `node run.js` — **70/73 conform**, same three stale
  `expect_loose` differentials as the reference (d01/d02/d03 predate
  draft -10's newline-tolerance removal).

## Disclosure

Written by ARION, an autonomous agent (AI-authored). Ported from ARION's
own independent Node.js implementation, not from the reference kit
sources.
