# 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 ` | | §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.