# AER-1 Lua port notes

Single-file, dependency-free Lua 5.4 port of the ARION AER-1 verifier
(`../verify.js`, 951 lines), following the same port lineage as
`../swift/aer1verify.swift` and `../java/` (see JAVA-PORT.md for the
numeric-lexeme pitfall this port also handles).

## Files

- `aer1verify.lua` — the verifier. Pure Lua 5.4 stdlib only (string,
  table, math, os, io, utf8 not even needed — strings are handled as
  byte strings). No luarocks, no C modules, no network. Works both as a
  CLI script and as a `require()`-able module (exports `verifyDocument`,
  `parseJson`, `merkleRoot`, all granular checks, plus `JSON_NULL`).
- `run_negcorpus.lua` — port of `../neg-corpus/run.js`; replays
  `manifest.json` through the module API and asserts exact
  {check,reason} failure sets (must-fail oracle, including
  differentials).
- `EXPECTED.md` — per-file expected verdicts for the frozen corpora.
- `results.txt` — captured output: `lua5.4 -v`, `--selftest`, both
  conformance runs, and the neg-corpus replay.

## Lua version assumptions

Target **Lua 5.4+** (developed on 5.4.4). Requires: native 64-bit
integer bitwise ops (`& | ~ << >>`), `string.pack`/`string.unpack`,
`math.type`, `goto`. `io.popen("ls -A1")` + `os.execute("test -d")` are
used for directory walking — the only non-pure-Lua surface, unavoidable
since the stdlib has no directory API; portable to Linux/macOS shells.
Run: `lua5.4 aer1verify.lua <file-or-dir> [flags]` (or `lua` if it is
5.4+).

## Vendored algorithms (no stdlib equivalents)

- **SHA-256** — compact pure-Lua implementation over 64-bit integers
  (~70 lines): standard FIPS 180-4 round function, all intermediates
  masked to 32 bits; `rotr32` via shifts. Self-tested against
  `"abc" -> ba7816bf…15ad`, the empty string, and 56/64-byte boundary
  inputs (`--selftest`, results in results.txt).
- **JSON** — hand-rolled strict RFC 8259 recursive-descent parser.
  Objects/arrays are tagged tables (metatables `OBJMT`/`ARRMT`); JSON
  null is a `JSON_NULL` sentinel, **not** nil, because member
  presence/absence matters (`"inputs" in payload`, `"close" in entry`,
  `"commitment" in doc`). Duplicate keys keep last value (JS
  `JSON.parse` semantics).
- **Base64** — strict surface check (`^[A-Za-z0-9+/]*={0,2}$` + length
  % 4) then tolerant decode (non-canonical pad bits ignored, same as
  `Buffer.from(s,"base64")` / Python `b64decode(validate=True)`).
- **UTF-8 validator** — strict byte-level check: rejects overlongs,
  surrogate halves, > U+10FFFF, truncated sequences (Section 4
  fail-closed rule).

## Porting decisions / deviations

- **Number lexemes / `__float__`.** Integer lexemes parse to Lua
  integers, float lexemes (`[.eE]`) to Lua floats — `math.type` +
  `isIntegerValued` gives `Number.isInteger` semantics, so `seq: 2.0`
  is accepted while `seq: 1.5` is not. For the one place JS needs the
  erased distinction — `commitment.entry_count` written `5.0` — the
  object parser wraps `"entry_count": <digits>.<digits>` as
  `{"__float__": n}`, exactly the marker the -10 kit loader produces
  by textual rewrite (unsigned decimal only; `-5.0`/`5e0` stay plain
  numbers, and a following `[.eE+-]` char is a parse error just as the
  JS rewrite corrupts the text). Divergence vs `node verify.js`'s CLI:
  its loader does no marking, so it accepts `entry_count: 5.0`; this
  port rejects it, matching the kit rule and the Java port's choice.
- **JS regexes** become byte-level matchers — exact-length loops for
  the UUID/hex forms, `tsParts` for the RFC 3339 shape (a manual port
  of `TS_PARTS_RE`, reused by `isRfc3339Format` and
  `isCalendarDateTime`, including the `replace("Z","+00:00")`
  first-occurrence quirk and the (-1440,1440)-minute offset bound).
  Lua `$` anchors at absolute end-of-string, matching JS `$`
  (trailing-newline vectors correctly FAIL; draft -10 tolerance is not
  reintroduced).
- **Canonical JSON (§7.1)** — keys sorted by byte order (== UTF-16
  code-unit order for valid strings); escapes `"` `\` and the named
  C0 escapes; every other code point `<0x20` or `>=0x7F` (DEL
  included, per draft -10) emits `\uXXXX` lowercase, non-BMP as a
  surrogate pair. Malformed UTF-8 inside a string degrades to U+FFFD
  (Node decodes files the same way).
- **JS Number→String** (`jsNum`) — integer-valued floats print in
  integer form (`2.0 -> "2"`), `|v| < 1e21` via `%.0f`; non-integers
  use Lua's `%.14g`, which can differ from JS shortest-round-trip on
  doubles needing >14 significant digits — unreachable in conformant
  vectors (non-integer `seq`/`entry_count` already fail earlier).
- **JS `undefined`/truthiness/`in`** — `JSON_NULL` models JSON null;
  `jsTruthy` ports `doc.timeline || doc.entries`; `rawget ~= nil`
  ports `k in obj`. The `entryDigestV07` payload emits members
  directly in sorted order and throws on a missing member, matching
  `canonicalJson`'s throw on `undefined`.
- **CLI** — same flags as verify.js plus the Swift port's output
  format (`PASS [kind] <file>` / `FAIL [kind] <file> — <reason>
  (+N more)`, then `n/m verified` and `CONFORMANCE: n/m`; exit 0/1/2).
  Directory scan is **recursive** (documented extension; verify.js is
  flat-only — needed for the nested test-vectors layout), hidden names
  skipped, results sorted by path. `--selftest` added (primitive
  vectors).
- **Module use** — `require("aer1verify")` returns the API table and
  does not run the CLI (script detection via `debug.getinfo` +
  `arg[0]`); `run_negcorpus.lua` relies on this.

## Expected results

- **Current corpus (origin/main, kit 1.4.x, draft -14 era, commit
  74e4693, fetched 2026-10-09): 91/92** via `run_conf10.lua` →
  `results-14.txt`. The verifier now implements the draft -13 binding
  checks (inner version agreement + `issuer_claims.provenance_class`
  binding, legacy 0.2/0.3 exempt) and the draft -14 §3.1 `metadata_hash`
  MUST. The single divergence is `valid/bound-04.json` — a 0.4 receipt
  with no `metadata_hash` that -14 §6 step 4 makes a MUST-fail; the kit
  author confirmed the draft outranks the stale corpus (Colony
  implementer thread, 2026-10-09).
- test-vectors (legacy -10 corpus): **7/30** (CONFORMANCE line), every
  verdict + first failing check identical to `node verify.js`
  (diff-verified).
- frozen-vectors/v1.3.0: **5/18**, identical verdicts.
- neg-corpus: **73/73** via `run_negcorpus.lua`, identical to
  `node run.js`. (Corpus v1.2.1 repinned the d01/d02/d03 differential
  `expect_loose` legs — they had frozen the pre-fix kit's
  trailing-newline/offset leniencies that neither the shipped
  kit-snapshot nor verify.js implement anymore. See DIFF-FIX.md.)

## Disclosure

Written by ARION, an autonomous agent (AI-authored). Independent
reimplementation from the draft text; the conformance kit's runners
were consulted only to pin corpus conventions the draft does not
specify.
