# AER-1 conformance verifier — PHP 8.2

An independent, zero-dependency, CLI-only verifier for **AER-1**
(`draft-zambo-aer1-14`), the Agent Execution Receipt format. Written for the
ARION project. It agrees with the reference runner
(`aer-1/conformance.py`) vector-for-vector on the corpus at
`origin/main` (kit 1.4.x, commit 74e4693), and additionally implements the
draft -14 `metadata_hash` MUST that the runner has not yet absorbed.

Result: **91/92 corpus checks**. The single divergence is
`test-vectors/valid/bound-04.json` — a 0.4 receipt with no `metadata_hash`
filed under `valid/`; draft -14 §6 step 4 makes it a MUST-fail, and the kit
author confirmed (Colony implementer thread, 2026-10-09) that the draft
outranks the corpus pending its -14 refresh pass. This verifier follows
the draft text. Full transcript: `RESULTS.txt`.

## Files

| File | Purpose |
|------|---------|
| `aer1.php` | The verifier. Single file, stdlib only (`hash`, `pcre`, `iconv`, `json`). CLI entry point plus a require-able function library. |
| `run_conformance.php` | Conformance harness — runs every corpus and prints one line per vector plus a summary. Exit 0/1. |
| `RESULTS.txt` | Captured final harness output (all PASS). |

## Usage

```sh
# Core check (Section 3): exit 0 = conformant, 1 = rejected (reasons on stderr)
php aer1.php receipt.json

# Optional tiers
php aer1.php --tier=strict receipt.json   # core + UUIDv4 id + anchor profile
php aer1.php --profile receipt.json       # reference-producer profile check

# Full conformance run (spec repo expected at ../../research/zambo-aer1/aer-1,
# override with: php run_conformance.php /path/to/aer-1 or AER1_DIR=...)
php run_conformance.php
```

## Implemented rules (ports of `conformance.py`)

- **Section 3 core check** — `aer1_check()`: the eight required members;
  `output_hash` = `"sha256:"` + lowercase hex SHA-256 of the strict-base64
  decoded `canonical_bytes`; strict UTF-8 after decode; lowercase UUID-shaped
  `id` (any version at core tier); calendar-valid RFC 3339 `created_at`
  (leap years, offset ranges, leap second `60`); `tool` object with non-empty
  `name`/`version`/`scope`; non-empty `receipt_schema_version`;
  `verification_status === "verified"`; `provenance_class` =
  `EXECUTED BY <NAME>` / `OBSERVED VIA GATEWAY` / `LOGGED BY AGENT`.
- **Strict id tier + anchor profile** — `aer1_check_disinterested_tier()`:
  UUID v4 with RFC 4122 variant bits; anchor member-set, leaf-hash binding,
  timestamp checks.
- **Reference-producer profile** — `aer1_check_reference_profile()`:
  canonical payload must be a JSON object carrying `inputs`.
- **Section 7 v06 chain** — `aer1_verify_chain()`: regression gate.
- **Section 7.1/-07 entry digest** — `aer1_chain_entry_digest_v07()` +
  `aer1_verify_chain_v07()`: hardened digest binding `prev_digest`, `seq`,
  `job_id`, `close`, `id`, `tool`, `provenance_class`, `output_hash`;
  canonical JSON with code-point key sort, no whitespace, and the exact
  Section 7.1 escape table (short escapes, `\u007f` for DELETE, lowercase
  hex, surrogate pairs above U+FFFF — byte-identical to Python
  `json.dumps(ensure_ascii=True)`).
- **Section 7.3 commitment** — `aer1_verify_chain_commitment()`.
- **Section 8.1 Merkle root** — `aer1_merkle_root()` (empty tree =
  SHA-256 of empty input; odd trailing node duplicated; canonical and
  legacy `leaves` forms).
- **Section 8.2 offline workflow** — `aer1_verify_workflow()`, including the
  MM-1 no-duplicate-`receipt_id` rule.
- **Draft -13 binding** — version agreement (inner committed
  `receipt_schema_version` must equal the outer value) and provenance
  binding (`issuer_claims.provenance_class` must equal the outer value);
  legacy 0.2/0.3 envelopes without an inner commitment pass.
- **Draft -14 §3.1 `metadata_hash`** — REQUIRED at
  `receipt_schema_version` ≥ 0.4; when present, recomputed as SHA-256 over
  the canonical metadata block and compared.

## Fidelity notes

- **int vs float**: PHP's `json_decode` preserves the `5`/`5.0` lexical
  distinction, so `entry_count: 5.0` is rejected (non-integer type) while
  `seq: 2.0` is accepted (integer-valued), exactly like Python.
- **Anchor quirk**: Python's `\Z` is strict end-of-string; PCRE's `\Z` is
  not. All regexes here use `\z`. `COMMIT_DIGEST_RE` deliberately keeps
  Python's `$` semantics (allows one trailing newline) for parity.
- **Object vs list**: JSON is decoded with objects as `Aer1JObject`
  (an `ArrayObject` subclass), so `{}` never collapses into `[]` the way
  `json_decode(assoc=true)` would — type checks fail closed exactly like
  Python's `isinstance(dict)`.
- **Padding**: PHP's strict `base64_decode` tolerates missing padding;
  `aer1_b64_decode` additionally requires length ≡ 0 (mod 4) to match
  Python `b64decode(validate=True)`.
- **Known residual limit**: float rendering in the entry digest matches
  Python `repr()` for integral and ordinary floats; extreme
  exponent-notation floats could diverge. No digested member is ever a
  non-integral float in the corpora (`seq` is normalized to int first).

## Results (draft-zambo-aer1-10 corpus)

- valid: **5/5** verified
- invalid: **23/23** rejected (reasons identical to the reference runner)
- invalid-profile: **1/1** (core accepted, profile rejected)
- anchored: **5/5**
- Merkle: **8/8** (incl. workflow-verdict extension)
- chain v06: **7/7**, chain v07: **28/28**
- commitment: **8/8**

`CONFORMANCE OK: all vectors behave as expected.` — see `RESULTS.txt`.
