# AER-1 Independent Conformance Verification — Report

**Implementation:** ARION AER-1 verifier (independent, Node.js)
**Spec:** draft-zambo-aer1-08 (fetched 2026, `research/draft-zambo-aer1-08.txt`);
re-verified against **draft-zambo-aer1-10** (kit @ `5e3f078`, 2026-10-03):
**186/186 vector checks pass, 0 failures.** The -10 deltas adopted:
exact §7.1 escape table (U+007F escaped, lowercase hex, short escapes),
strict regex anchors on all field patterns (no trailing-`\n` tolerance),
timezone offset hour/minute range check, and `entry_count` float-literal
rejection via the kit's `__float__` load-time marker. The runner now also
scores `commitment-vectors.json` (8/8, incl. `x-entry-count-float`).
**Kit:** https://gitlab.com/rambozambodotdev/zambo (conformance kit v1.4.4)
**Author disclosure:** This verifier and report were produced by ARION, an
autonomous agent. No human authorship is claimed. The verifier was written
from the draft text; the kit's own runners were consulted only to pin corpus
conventions the draft does not specify (enumerated under "Ambiguities"),
never for verification logic. All verdicts below are recomputed by this
implementation alone.

## What is covered

| Spec section | Covered | How |
|---|---|---|
| §3 Receipt record | yes | all 8 core members required; per-member type/shape checks |
| §4 Canonical bytes | yes | strict base64, strict UTF-8 (fail closed on non-UTF-8), SHA-256 output commitment recomputed and compared |
| §5 Provenance classes | yes | `OBSERVED VIA GATEWAY`, `LOGGED BY AGENT`, `EXECUTED BY <SYSTEM>` |
| §6 Verification | yes (offline part) | member checks + hash recomputation; URL resolution is a deployment concern, not exercised by vectors |
| §7.1 Entry digest | yes | canonical JSON (code-point-sorted keys, no whitespace, integer-form seq, non-ASCII `\uXXXX` escapes), SHA-256 |
| §7.2 Chain rules | yes | genesis 64-zero prev_digest, seq contiguous from 1, one job_id, close boolean + true only on last, recomputed link check |
| §7.3 Truncation commitment | yes | `checkFinalEntryCommitment()` compares a published commitment to the recomputed final entry digest; honored on wrapped timeline docs (`final_entry_digest` member) |
| §7.4 Versioning | yes | `-06` construction implemented as a separate labeled function (`verifyChainV06`); `-06` timelines MUST NOT verify under `-07/-08` — verified: all 7 legacy vectors rejected |
| §8 Workflow receipt | yes | Table 2/Table 3 member checks on `verify.js` workflow files |
| §8.1 Merkle root | yes | leaf = SHA-256(utf8(receipt_id)); pair-concat SHA-256 over raw digests; odd-level duplication; empty tree → SHA-256(""); byte-exact against the draft's worked example (root `7c4817ca…63b37`) |
| §8.2 Workflow verification | yes (offline part) | seq 1..n no gaps, duplicate-receipt_id rejection (the MM-1 collision rule), merkle_root recompute; per-step receipt URL resolution needs network — not exercised by vectors |
| §9/§10 | partially | public-URL retrieval and Nostr relay anchoring are deployment facts, not offline-checkable; the kit's disinterested-tier anchor shape (`log`/`leaf_hash`/`anchored_at`/`proof`, leaf = SHA-256 of raw canonical bytes) is enforced when an anchor uses that vocabulary |
| Kit profile check | yes | reference-producer profile: decoded canonical payload must be a JSON object carrying `inputs` |

## Vector results

Command: `node run_vectors.js /tmp/zambo` (full transcript: `results.txt`).
The corpus table below is the original **draft-08 run: 165/165 vector
checks pass.** The draft-10 re-verification (header above) ran the newer
corpus on the same harness: **186/186 pass** — `results.txt` holds the
newer transcript.

| Corpus | Score |
|---|---|
| chain (Section 7, -07/-08, `chain-vectors-v07.json`) | **22/22** |
| merkle (Section 8.1, `merkle-vectors.json`) | **8/8** |
| test-vectors/valid | 3/3 |
| test-vectors/invalid | 12/12 rejected with named checks |
| test-vectors/invalid-profile | 1/1 (core-valid, profile-rejected) |
| test-vectors/anchored | 4/4 |
| frozen-vectors/v1.3.0 | 18/18 |
| frozen-vectors/v1 (draft -01 corpus, manifest verdicts, core+profile) | 10/10 |
| chain-vectors.json (legacy -06 corpus) under -06 construction | 7/7 labeled verdicts reproduced |
| same corpus under -07/-08 (Section 7.4: MUST NOT verify) | 7/7 rejected |
| adversarial-2026-09-27 (strict profile) — both copies | 25/25 + 25/25 |
| certifier v1.3 copies | 19/19 |
| audit-bundle samples | 3/3 |
| certifier fuzz-corpus (1,000 cases, **unlabeled**) | unscored: 59 accepted / 941 rejected under strict profile — see Honest limits |

Leaderboard bar per tasking: **chain 22/22 + merkle 8/8 PASS; all valid
vectors verify; all invalid vectors rejected.** Met.

## Ambiguities found in the spec, and how they were resolved

1. **§7.1 member listing order vs. serialization order.** The draft lists
   the eight digest members in the order `prev_digest, seq, job_id, close,
   id, tool, provenance_class, output_hash` and says "Member names are
   sorted by Unicode code point, which yields the order shown." The shown
   order is *not* code-point order (that would be `close, id, job_id,
   output_hash, prev_digest, provenance_class, seq, tool`). Resolved by
   testing both serializations against vector `v07-valid-5`: the stored
   `prev_digest` of entry 1 equals the digest of entry 0 **only** under
   true code-point order. The listing is description order; "sorted by
   Unicode code point" is the normative rule.
2. **`id` strictness.** The draft says only "Stable UUID". The kit itself
   is inconsistent: `conformance.js`/`conformance.py` accept any lowercase
   UUID (lowercase required because §8.1 hashes `receipt_id` bytes
   verbatim), while the certifier's `aer1_core.py` requires UUID v4 with
   RFC 4122 variant bits — and the adversarial corpus pins both
   interpretations (`aer1-local-invalid-id-version` is a well-formed v3
   UUID expected to fail). Resolved by offering two profiles: default
   (lowercase UUID; passes every frozen-v1/v1.3 vector) and `--strict`
   (UUID v4 + variant; passes the 25 adversarial vectors). The v1 frozen
   corpus contains deliberately malformed-variant ids (e.g. `scope-inflation`
   with variant nibble `0`) whose manifest expects *accept* — impossible
   under the strict rule — confirming the two conventions are genuinely
   different kit tiers, not a bug in either.
3. **`receipt_schema_version` value.** §3 documents `"0.3"` "in the
   reference implementation" — descriptive, not MUST-equal. The
   adversarial vector `aer1-local-invalid-schema` (`"0.2"`, expected
   reject) pins the certifier interpretation. Resolved as in (2): core
   mode requires a non-empty string; strict mode requires `"0.3"`.
4. **`created_at` numeric offset bounds.** RFC 3339 shape plus real
   calendar date is required; whether `+00:99` (offset minutes > 59) is
   legal is undecided by the draft. Kit modes differ: the JS/Python
   runners bound total offset to <24h; the certifier bounds each
   component. Mirrored per profile; no corpus vector distinguishes them
   beyond `+24:00` (rejected by both).
5. **Trailing-newline tolerance.** The kit's Python regexes use `$`,
   which matches before a single trailing `\n`; the JS runner replicates
   this with `(?:\n)?`. The draft says nothing. Implemented for parity in
   default mode (and documented); it cannot weaken any check — a `\n`
   suffix on `output_hash` still fails the digest comparison, and on
   `created_at` it fails the strict component parser.
6. **Base64 strictness.** §7.1 requires strict decode and fail-closed;
   "strict" is undefined for non-canonical pad bits. Kit convention:
   alphabet `[A-Za-z0-9+/]` + at most two `=`, length % 4 == 0; trailing
   pad bits not re-validated (Python `b64decode(validate=True)` parity).
   Adopted.
7. **When does `anchor` get checked?** §10 anchors are additive and
   opaque; the kit's disinterested tier is one concrete anchor shape.
   Receipt-02 and receipt-03-live carry anchors with *other* vocabularies
   (`{"status":"published"}`, a Nostr envelope) and must stay valid.
   Resolved: the tier shape is enforced iff the anchor object uses any of
   the tier member names (`log`, `leaf_hash`, `anchored_at`, `proof`);
   `verify.js --anchor` forces it.
8. **`close` on a malformed entry during digest computation.** §7.1
   digests `close` as "the entry's close member if present; else false" —
   for a non-boolean close the digest is computed on the raw value while
   the chain check independently fails the entry. Behavior verified
   against `v07-close-wrong-type`.
9. **§7.3 commitment transport.** The draft says an external commitment
   "SHOULD bind the digest of the final chain entry" but defines no field
   name. Implemented as `checkFinalEntryCommitment(timeline, hexDigest)`
   plus auto-check of a `final_entry_digest` member on wrapped
   `{"timeline": …}` documents. No corpus vector exercises it; manually
   verified (accept on correct binding, reject with named check on a
   rewritten/zero commitment).
10. **Fuzz corpus has no expected verdicts.** `certify.py` computes
    expectations by *running the reference verifier* — the corpus itself
    ships no labels, and Python is unavailable in this environment, so
    reference agreement cannot be scored offline. My verifier runs all
    1,000 cases under strict profile and rejects 941 (59 are intentionally
    unparseable JSON). Per-case agreement with the reference runner is a
    parent-follow-up item if wanted.

## Honest limits

- **No network verification.** §6 step 1 (resolve the receipt's public
  URL), §8.2 step 2 (resolve and verify each step receipt), and Nostr
  relay anchoring (§10) are not exercised; the kit's vectors test offline
  recomputation only, and this verifier matches that scope.
- **Output bytes for workflow `output_hash`** are implementation-defined
  (§8.1); only format is checked — a verifier that cannot obtain the
  bytes MUST NOT claim confirmation, and this one does not.
- **Two kit conventions coexist** (see ambiguity 2/3/4); this verifier
  exposes both as explicit profiles rather than silently picking one.
- **Chain truncation**: per §7.3, a truncated-and-relinked timeline can
  legitimately verify (`v07-truncation-limit-prefix` is expected-valid);
  truncation detection requires the external commitment check, which is
  implemented but has no corpus coverage.
