# Adversarial Execution Evidence — Verifier Failure Codes

> ## THIS DOCUMENT IS NOT THE SPECIFICATION, AND IT IS NOT A RESTATEMENT OF ONE EITHER
>
> [`adversarial-execution-evidence.md`](adversarial-execution-evidence.md) in this same directory is a
> **local restatement** of the upstream authority (`spec/predicates/adversarial-execution-evidence.md`
> in the `in-toto/attestation` fork, mirrored in the `aee-conformance` repository), checked against that
> authority by `scripts/lint_spec_restatement.py` and listed
> in that script's `_DOCUMENTS` registry.
>
> **This document is a different kind of thing, and it is deliberately NOT in that registry.** It
> documents `packages/verify/src/evidence.ts`'s `Code` union — the exhaustive set of failure codes this
> repository's TypeScript reference verifier can emit — grounded in the verifier's own source, not in
> the upstream specification's prose. The upstream authority does not enumerate an implementation's
> internal code strings at all (the spec constrains observable behavior, not a particular verifier's
> diagnostic vocabulary), so there is no authority text for a restatement gate to check this document
> against. **This file's authority is `evidence.ts` itself**, and the thing that keeps it honest is
> `scripts/lint_failure_codes.py`: every code documented here is
> cross-checked against the live `Code` union (bidirectionally — nothing may exist on only one side) and
> every citation below is cross-checked against the live conformance corpus, not trusted as prose.
>
> Do not cite this file as normative, and do not treat it as a second implementation of the spec's
> failure taxonomy. It is a reference glossary over one file in this codebase.

This verifier's `Code` union (`packages/verify/src/evidence.ts:140-220`) has **67 members**. `verifyStatement`
(`evidence.ts:2542`) runs a pinned sequence — GATE 0 (statement well-formedness) → GATE 1 (coverage
validity, both the per-row pipeline and the statement-level requirements v0.7 adds) → the
recompute-equality check → GATE 2 (evidence tier) → the consumer-policy anchor step — and stops at the
first gate that emits a non-empty code list. Every code below is grouped the way `evidence.ts`'s own
`Code` union comments already group it (lines 141, 168, 176, 197, 205, 211, 213), because that grouping
is the verifier's own structure, not one invented for this document.

**63 of the 67 codes** are exercised by at least one vector in the
[`aee-conformance`](https://github.com/astrogilda/aee-conformance) conformance corpus. Each is cited
below by the **condition** its forcing vector exercises — an `aee-c-N` id from that suite's condition
registry, naming a requirement of the specification — and the lint script resolves that condition
through the corpus manifest to a vector whose `expected.codes` carries the code, so a citation is
verified rather than merely present. **4 codes are structurally exempt** from vector coverage; see
[Exemptions](#exemptions) for why, and each of the four entries below repeats the specific reason.

**Why a condition and not a vector name.** These entries cited vectors by `id` until 2026-09-02, when
the corpus renamed every vector to a content address for the express purpose of stopping identifiers
carrying meaning. Sixty-three citations here went dangling at once. Substituting the new addresses
would have produced a specification citing sixty-three opaque digests, unreadable to a person and
broken again by the next regeneration. A condition survives that change and every change like it: the
rename altered every `id` and every `file` in the manifest and left the `(kind, conditions, expected)`
content of all 272 vectors identical, vector for vector. It also says more than a filename ever did.
`bad-729-duplicate-attackid-rows` asserted in its own name, checked by nothing, what the vector was
for; `aee-c-90` is a requirement, and the corpus is what says which vectors force it.

**Corpus pin.** Every citation below was checked against the conformance corpus this repository
vendors at `packages/verify/vectors/aee-v06/` — release `v0.8.0`, source commit
`0c4e27ec4f28b03d9688885bb5a00c6e96334d1e`, manifest digest
`eb24532be577ee6a936410e6850482847cecac07147aa3c76d737fc730d4bf2b` — which carries 272 vectors:
61 `accept`, 209 `reject`, 2 `indeterminate`. This paragraph is not decoration and it is not a
snapshot that rots. `scripts/lint_failure_codes.py` reads it, holds the vendored copy to it on all
three axes, and REFUSES to check a single citation below when they disagree, naming what was
expected and what is there. Until 2026-09-02 this document was instead graded against whatever the
`aee-conformance` sibling checkout had most recently committed, and a local rename of every vector
in that checkout invalidated roughly forty citations in this published document with no version
boundary anywhere in between. A corpus that moves under a specification is a corpus the
specification never agreed to; moving this pin is now a deliberate act, taken by somebody who has
re-read the citations against the new corpus.

## How to read an entry

Each code below gets one subsection: a plain-language statement of the condition that makes the
verifier append it to a report's `codes` array, grounded in the actual call site(s) in `evidence.ts`
(cited as `evidence.ts:LINE`), followed by a `**Condition:**` line naming the conformance condition
some vector exercises while expecting that code. The `aee-c-N` id is the checked half of that line:
`scripts/lint_failure_codes.py` resolves it through the pinned manifest and fails when no vector
declares it, and when vectors do declare it but none of them expects this code. The clause after the
id is this document's own summary of what the condition requires, for a reader who does not want to
open the registry; the conformance suite's condition registry is the authority for its exact wording.
A code with more than one emission site lists all of them — the
`appendCode` helper (`evidence.ts:224`) is idempotent (it will not add a code already present), so two
call sites for the same code are two independent ways of reaching the same diagnosis, not a
double-count.

No RFC 2119 language appears below. This is a description of what the code does, not a new set of
obligations.

## GATE 0 — statement well-formedness

GATE 0 (`evidence.ts:1520`, function `gate0`) is a pure function of the carried bytes: no signatures, no
consumer policy. It runs first; any non-empty result here means the statement never reaches GATE 1.

### `statement-malformed`

The catch-all code for a structural shape violation: the statement or a member inside it is not the
JSON shape the parser expects. It has many independent emission sites, all through `appendCode(codes,
"statement-malformed")`:

- The whole statement fails to parse as JSON, carries a duplicate member anywhere (at any depth, not
  only inside an observation-record payload), exceeds the 128-level nesting bound, contains an
  ill-formed string scalar, or has no `predicate` member at all — `parseStatement` returns `null` in any
  of these cases, and `verifyStatement` synthesizes `statement-malformed` directly rather than running
  any gate (`evidence.ts:2544-2545`).
- The `predicate` member itself is present but is not a JSON object (`evidence.ts:1044`).
- `predicate.coverage` is present but is not an object (`evidence.ts:1062`).
- `predicate.attackResults` is present but is not an array (`evidence.ts:1068`).
- `predicate.observationRecords` is present but is not an array (`evidence.ts:1083`).
- `predicate.batchRoot` is present but is not a string (`evidence.ts:1088`).
- `predicate.observationEnvironment` is present but is not an object (`evidence.ts:1102`).
- `observationEnvironment.observationVocabulary` is present but is not an object (`evidence.ts:1140`).
- An entry of `attackResults` is present but is not an object (`evidence.ts:1156`).
- A row member that must be a string (`attackId`, `containmentObserved`, `basis`, `method`,
  `attribution`, `actualLayer`) is present with the wrong JSON type (`evidence.ts:1168`) — this also
  counts the member as absent, so for `actualLayer` it additionally triggers
  `malformed-missing-actual-layer` at GATE 0 (the Go reference verifier's "nil-pointer double-append"
  parity, documented at `evidence.ts:1159-1163`).
- `observationEnvironment.corpus.manifest` is present but its RFC 8785 canonicalization throws (a lone
  UTF-16 surrogate or an unsafe-integer number inside it) — `gate0Corpus`, `evidence.ts:1373-1375`.
- Two rows in `attackResults` carry the same `attackId` — `gate0CoverageIntegrity`,
  `evidence.ts:1418`.
- `predicate.attackResults` is entirely absent (`rowsPresent` is false) — `evidence.ts:1554`.

**Condition:** `aee-c-90` — no two `attackResults` rows share an `attackId`

### `statement-type-unsupported`

The statement's top-level `_type` does not equal `"https://in-toto.io/Statement/v1"`
(`STATEMENT_TYPE`), checked at `evidence.ts:1524`.

**Condition:** `aee-c-77` — the statement `_type` and `predicateType` URIs

### `predicate-type-unsupported`

The statement's `predicateType` does not equal this verifier's implemented AEE predicate type URI
(`PREDICATE_TYPE`, `https://in-toto.io/attestation/adversarial-execution-evidence/v0.7`), checked at
`evidence.ts:1525`.

**Condition:** `aee-c-77` — the statement `_type` and `predicateType` URIs

### `member-spelling`

The predicate carries the member name `does_not_assert` — the retired snake_case spelling of
`doesNotAssert` that a past spec version renamed with no accepted alias. `evidence.ts:104` names the
constant (`REJECTED_SNAKE_SPELLING`); `evidence.ts:1529` fires the code whenever
`has(p.raw, "does_not_assert")` is true, regardless of what the member's value is.

**Condition:** `aee-c-84` — `doesNotAssert` has a single canonical spelling

### `result-vocabulary`

`predicate.result` is either absent, present with the wrong JSON type (caught earlier at parse time,
`evidence.ts:1050`), or present as a string that is not one of the four closed result tokens
(`pass` / `pass_indirect` / `degraded` / `fail`) — checked again at GATE 0 proper via `isResultToken`
(`evidence.ts:1531`).

**Condition:** `aee-c-1` — the closed lowercase `result` vocabulary

### `environment-incomplete`

Three distinct absences collapse to this one code:

- `observationEnvironment` is absent from the predicate entirely (`evidence.ts:1535`).
- `observationEnvironment` is present but is missing one of `substrate`, `corpus`, `catchPolicy`, or
  `networkPosture` (`evidence.ts:1538-1539`).
- `observationEnvironment.corpus` is present but has no `manifest` member at all — `gate0Corpus`,
  `evidence.ts:1362-1363`.

**Condition:** `aee-c-78` — `observationEnvironment`'s required members

### `posture-vocabulary`

`observationEnvironment.networkPosture` is present, but its `posture` string is not a member of the
closed `EGRESS_POSTURES` set imported from `./bindings` (`evidence.ts:1546-1547`). This check only runs
when `networkPosture` itself is present — an absent `networkPosture` reports only
`environment-incomplete`, never a second code for the same absence.

**Condition:** `aee-c-93` — `networkPosture.posture` is a registered value

### `vocabulary-missing`

`observationEnvironment.observationVocabulary` is absent — either because the whole environment is
absent (`evidence.ts:1536`) or because the environment is present but carries no `observationVocabulary`
sub-object (`evidence.ts:1540`).

**Condition:** `aee-c-51` — `observationVocabulary` is required

### `vocabulary-not-canonical`

`gate0Vocabulary` (`evidence.ts:1336-1358`) fires this for any of three independent shape violations of
`observationVocabulary`:

- `labels` or `caught` is missing from the vocabulary object entirely (`evidence.ts:1337-1339`).
- `labels` or `caught` is present but not strictly ascending and duplicate-free under UTF-16 code-unit
  order (`evidence.ts:1341-1342`).
- Some entry of `labels` or `caught` contains a UTF-16 surrogate code unit — the normative BMP-only
  string profile (`evidence.ts:1346-1347`).

**Condition:** `aee-c-53` — the vocabulary arrays are sorted ascending and duplicate-free

### `vocabulary-caught-not-subset`

`observationVocabulary.caught` contains an entry that does not appear in `observationVocabulary.labels`
(`evidence.ts:1348-1354`).

**Condition:** `aee-c-52` — `caught` is a subset of `labels`

### `vocabulary-digest-mismatch`

`observationVocabulary.digest.sha256` does not equal the SHA-256 of the canonical
`{"caught":[...],"labels":[...]}` reconstruction of the carried arrays (`evidence.ts:1355-1357`).

**Condition:** `aee-c-54` — the vocabulary digest is JCS over `{caught, labels}`

### `corpus-digest-mismatch`

`observationEnvironment.corpus.digest.sha256` does not equal the SHA-256 of the RFC 8785 canonical
bytes of `corpus.manifest` (`gate0Corpus`, `evidence.ts:1366-1369`).

**Condition:** `aee-c-79` — the corpus digest re-derives from the embedded manifest

### `manifest-duplicate-attack`

The same attack identifier appears under more than one class in `corpus.manifest.classes`
(`gate0Corpus`, `evidence.ts:1377-1384`).

**Condition:** `aee-c-80` — an `attackId` falls under at most one manifest class

### `corpus-manifest-no-attacks`

`corpus.manifest.classes` declares zero attack identifiers across every one of its classes — every
class array is empty, or the `classes` object itself is empty (`gate0ManifestDeclaresAttacks`,
`evidence.ts:1450-1456`). This is deliberately a well-formedness fault rather than a scoring outcome
(see the doc comment at `evidence.ts:1425-1449` for why): with zero declared attacks, the coverage and
substrate-row machinery pass vacuously and a substrate-free, no-signature statement about an arbitrary
subject would otherwise be mintable.

**Condition:** `aee-c-92` — the corpus manifest declares at least one attack identifier

### `manifest-expected-payloads-malformed`

`corpus.manifest.expectedPayloads` is present but malformed — either its top-level shape is not a map
of string arrays (`expectedPayloadsShapeOk` false), or `expectedPayloadsOk` fails because some key names
an attack ID the same manifest's `classes` never declares, or some commitment array is empty, unsorted,
duplicate-bearing, or not entirely lowercase 64-hex (`gate0Corpus`, `evidence.ts:1386-1389`, delegating
to `expectedPayloadsOk`, `evidence.ts:1658-1665`).

**Condition:** `aee-c-103` — `corpus.manifest.expectedPayloads` is well formed

### `coverage-missing`

`predicate.coverage` is absent entirely (`evidence.ts:1553`).

**Condition:** `aee-c-83` — the `coverage` member is required

### `coverage-incomplete`

`gate0CoverageIntegrity` (`evidence.ts:1393-1423`) fires this for either of two independent violations:

- The three coverage partitions — `assessedClasses`, the keys of `outOfScope`, and the keys of
  `routedElsewhere` — do not exhaustively and disjointly cover the manifest's declared classes: some
  class is missing from all three, appears in more than one, or an entry names a class the manifest does
  not declare (`evidence.ts:1406-1409`).
- The set of `attackId`s carried across every row does not equal the set of attack identifiers implied
  by the classes actually listed under `assessedClasses` (`evidence.ts:1411-1412`, `1422`).

**Condition:** `aee-c-82` — coverage equals the manifest exactly, at attack granularity

### `row-attack-unknown`

A row's `attackId` does not appear anywhere in the corpus manifest's declared classes
(`gate0CoverageIntegrity`, `evidence.ts:1420`).

**Condition:** `aee-c-81` — a row's `attackId` appears in the manifest

### `malformed-missing-actual-layer`

A row's `actualLayer` member is absent — this also covers the case where it was present with the wrong
JSON type, which the parser already counted as absent (`evidence.ts:1566-1568`, and the parity note at
`1159-1163`).

**Condition:** `aee-c-47` — a missing `actualLayer` is a malformed statement rather than a fail

### `clean-row-layer-not-none`

The vocabulary is otherwise well-formed, the row's `containmentObserved` is a clean (non-caught)
vocabulary label, and the row's `actualLayer` is not the literal string `"none"`
(`evidence.ts:1570-1571`).

**Condition:** `aee-c-48` — a clean row's `actualLayer` is the literal `none`

### `subject-cardinality`

The statement's `subject` array does not contain exactly one entry — checked unconditionally,
regardless of row basis (`evidence.ts:1559`).

**Condition:** `aee-c-58` — exactly one subject, on a statement of any basis

### `subject-sha256-missing`

The statement carries at least one `basis: substrate` row, and `subject[0].digest.sha256` is not a
string (`gate0SubstrateBindingInputs`, `evidence.ts:1502-1504`). This check, and every other check in
`gate0SubstrateBindingInputs`, only runs when `hasSubstrateRows(p)` is true (`evidence.ts:1577`).

**Condition:** `aee-c-59` — binding digest inputs are lowercase 64-hex sha256

### `digest-not-canonical`

Scoped the same way as `subject-sha256-missing` (statement carries a substrate row): a hex digest value
that is present is not exactly 64 lowercase hex characters. Three call sites inside
`gate0SubstrateBindingInputs`:

- `observationEnvironment.runEntropy.digest.sha256` (`evidence.ts:1490`).
- `subject[0].digest.sha256`, when it is present as a string but fails the lowercase-64-hex shape check
  (`evidence.ts:1505`).
- Any of `catchPolicy.digest.sha256`, `corpus.digest.sha256`, `networkPosture.digest.sha256`, or
  `substrate.digest.sha256` that is present but not lowercase 64-hex (`evidence.ts:1507-1515`).

**Condition:** `aee-c-59` — binding digest inputs are lowercase 64-hex sha256

### `run-entropy-missing`

The statement carries at least one `basis: substrate` row, and `observationEnvironment.runEntropy` is
absent (`gate0SubstrateBindingInputs`, `evidence.ts:1489`).

**Condition:** `aee-c-57` — `runEntropy` is required alongside any substrate row

### `issued-at-missing`

`predicate.issuedAt` is absent (`evidence.ts:1579`).

**Condition:** `aee-c-85` — `issuedAt` is required and follows the Timestamp profile

### `issued-at-malformed`

Three sites converge on this code:

- `issuedAt` is present but not a string, caught at parse time (`evidence.ts:1093`).
- `issuedAt` is a string but does not parse under the strict RFC 3339-UTC timestamp profile
  `parseTimestamp` enforces — an uppercase `T`/`Z` date-time separator and zone designator only, any
  numeric offset accepted syntactically but only a literal zero offset passing the value check
  (`evidence.ts:1479-1485`, invoked at GATE 0 proper: `evidence.ts:1580`).
- The same re-derivation fails again inside `gate1WithContext` when the statement carries substrate rows
  and the run binding must be computed from a parsed `issuedAt` (`evidence.ts:2388-2390`) — this is the
  identical parse re-run at a second call site, not a second condition.

**Condition:** `aee-c-85` — `issuedAt` is required and follows the Timestamp profile

## GATE 1 — statement-level observation-record codes

These run once over the whole `observationRecords` array, before any per-row work, inside
`checkRecordsStatementLevel` (`evidence.ts:1709-1756`) and `gate1WithContext`
(`evidence.ts:2357-2402`).

### `batch-root-missing`

`observationRecords` is present with at least one entry, every record decodes successfully, and
`predicate.batchRoot` is absent (`evidence.ts:1752`).

**Condition:** `aee-c-24` — `batchRoot` is present whenever records are

### `batch-root-mismatch`

`observationRecords` is present with at least one entry, every record decodes successfully, and the
carried `batchRoot` does not equal the recomputed RFC 6962 Merkle root over the records' leaf hashes
(`evidence.ts:1753`).

**Condition:** `aee-c-25` — RFC 6962 domain-separated hashing

### `batch-root-orphaned`

`observationRecords` is absent, or present but empty, while `predicate.batchRoot` is present anyway
(`evidence.ts:1713-1714`).

**Condition:** `aee-c-31` — `batchRoot` is omitted exactly when records are absent

### `duplicate-record`

Two or more carried observation records fold to the same leaf hash — the same canonical identity
(`hasDuplicateLeaf`, invoked at `evidence.ts:1750`).

**Condition:** `aee-c-29` — byte-identical duplicate records are invalid

### `records-absent`

The statement carries at least one `basis: substrate` row, and `observationRecords` is entirely absent
(`gate1WithContext`, `evidence.ts:2382-2383`).

**Condition:** `aee-c-31` — `batchRoot` is omitted exactly when records are absent

### `record-undecodable`

A record's `payload` string fails strict base64 decode (`strictBase64Decode` returning `null`), checked
once over the whole record set (`evidence.ts:1737-1741`) and again wherever a specific record already
flagged with `decodeErr` is analyzed for per-row purposes (`analyzePayload`, `evidence.ts:1787-1789`) —
the second site is the same underlying decode failure surfacing at a second read, not an independent
condition.

**Condition:** `aee-c-19` — a covering record's media type ends in `+json`

### `record-signatures-empty`

At least one observation record's `signatures` member carries zero entries — an absent `signatures`
member, an empty array, and a non-array value all collapse to the same "zero entries" fault
(`anyRecordSignaturesEmpty`, `evidence.ts:1781-1783`, invoked at `evidence.ts:1732`). This is checked
statement-wide, before the per-record decode loop, so its ordering against `record-undecodable` is
deterministic regardless of which record in the array carries which fault
(rationale at `evidence.ts:1722-1731`).

**Condition:** `aee-c-91` — every observation record's `signatures` carries at least one entry

## GATE 1 — per-row coverage-validity codes

These run per substrate row, inside `checkSubstrateRow` (`evidence.ts:1954-2054`), which resolves the
row's `observationRefs` to specific records and evaluates each resolved record's payload
(`analyzePayload`, `evidence.ts:1785-1832`) and its `aeeKind`-specific constraints (`evaluateKind`,
`evidence.ts:1841-1949`).

**A structural note that applies to several codes below.** Codes sourced from `analyzePayload` and
`evaluateKind` — `payload-not-ijson`, `payload-not-canonical`, `payload-media-type`,
`payload-missing-reserved`, `run-binding-mismatch`, `payload-commitment-malformed`,
`arming-covers-nothing`, and `sealed-covers-nothing` — have a second possible emission path beyond a row
resolving a bad record: `carriedRecordsCover` (`evidence.ts:2240-2258`, called from
`gate1CommitmentsSubstrate` at `evidence.ts:2318`) re-runs the identical `analyzePayload`/`evaluateKind`
pipeline over every carried record whose `aeeKind` is one of the four defined covering kinds
(`isCoveringKind`, `evidence.ts:2196-2198`), whether or not any row's `observationRefs` resolves to it.
This closes the gap where a producer carries a defective record inside a signed `batchRoot` but points
every row's refs at a healthy twin instead — see the rationale at `evidence.ts:2205-2225`.
`examination-covers-nothing` is also reachable this way. The three registered-non-covering codes
(`record-kind-unknown-covers-nothing`, `moat-drop-covers-nothing`, `uncommitted-observation-covers-nothing`)
are NOT reachable through `carriedRecordsCover`, because `isCoveringKind` excludes their kinds by
construction — those three fire only through a row's own resolution path.

### `refs-empty`

A substrate row's `observationRefs` member is either absent (`evidence.ts:1974`) or present as an empty
array (`evidence.ts:1976`).

**Condition:** `aee-c-10` — a substrate row carries a non-empty `observationRefs`

### `ref-malformed`

A substrate row's `observationRefs` is present but is not an array of non-negative integers
(`row.refsErr`, checked at `evidence.ts:1975`).

**Condition:** `aee-c-11` — every `observationRefs` entry is an in-range integer index

### `ref-out-of-range`

Two independent call sites, one condition: a row's `observationRefs` entry indexes past the end of
`observationRecords`. Checked per substrate row inside `checkSubstrateRow`
(`evidence.ts:1980-1983`), and again statement-wide over every row of any basis — including rows nothing
else in GATE 1 reads — via `anyObservationRefOutOfRange` (`evidence.ts:2063-2069`, invoked at
`evidence.ts:2369`), so an artifact-only statement with a dangling ref is still rejected even though it
never enters the substrate-row pipeline.

**Condition:** `aee-c-11` — every `observationRefs` entry is an in-range integer index

### `fail-closed-substrate-row`

A substrate row's three closed vocabularies are jointly checked, and the row fails if any is
out-of-vocabulary: `containmentObserved` is neither a caught nor a clean label, `method` is neither
`intercepted` nor `reconstructed`, or `attribution` is neither `pinned` nor `paired`
(`evidence.ts:1964-1972`).

**Condition:** `aee-c-42` — `method` is required and closed to `intercepted`/`reconstructed`

### `payload-not-ijson`

A resolved record's decoded payload bytes either fail to parse as strict I-JSON (`parseIJSON` throws,
`evidence.ts:1791-1796`) or fail the raw-byte string-scalar well-formedness scan (`checkStringScalars`,
`evidence.ts:1803-1806`).

**Condition:** `aee-c-18` — a covering payload is valid I-JSON

### `payload-not-canonical`

A resolved record's decoded payload has one of four independent problems, all inside `analyzePayload`:

- The parsed I-JSON value is not an object at all (`evidence.ts:1807-1810`).
- Re-emitting the parsed value in RFC 8785 canonical form does not byte-match the original payload
  bytes (`evidence.ts:1812-1813`).
- Canonical re-emission throws — for example, a lone UTF-16 surrogate inside a string value
  (`evidence.ts:1814-1816`).
- Some object member name anywhere in the payload contains a UTF-16 surrogate code unit, the BMP-only
  profile applied to member names rather than values (`evidence.ts:1817-1820`).

**Condition:** `aee-c-17` — a covering payload is canonical RFC 8785

### `payload-media-type`

A resolved record's `payloadType` does not end with `+json` (`evidence.ts:1821`).

**Condition:** `aee-c-19` — a covering record's media type ends in `+json`

### `payload-missing-reserved`

Two distinct absences share this code:

- A resolved record's payload lacks one of the three universally-reserved members —
  `aeeRunBinding`, `aeeKind`, or `aeeMethod` — checked in `analyzePayload` before any kind-specific
  work happens (`evidence.ts:1823-1828`).
- An `interception`-kind record (one that does carry all three universal members) has no
  `aeePayloadCommitment` member at all — the initial `failCode` value `evaluateKind` assigns the
  `interception` case, which stands unless overridden by the commitment being present-but-malformed
  (`evidence.ts:1848`, `1857`, `1867`).

**Condition:** `aee-c-20` — a covering payload carries the reserved `aee` members

### `run-binding-mismatch`

A resolved record's `aeeRunBinding` value does not equal the run binding this verifier derives for the
statement from its digest-pinned environment members (`deriveRunBinding` / `deriveStatementBinding`,
`evidence.ts:1211-1265`; compared at `evidence.ts:1830`).

**Condition:** `aee-c-62` — the run binding is anti-splice

### `method-cap-exceeded`

A row declares `method: intercepted`, but among the records it validly resolves, the weakest signed
method present is `reconstructed` — the row's claimed method is capped downward by what its covering
evidence actually establishes (`evidence.ts:2048-2050`).

**Condition:** `aee-c-23` — a row's method is capped by the weakest signed `aeeMethod`

### `caught-row-uncovered`

A row whose `containmentObserved` is a caught label (and whose `method` is not `reconstructed`) resolves
no record that validly evaluates as `aeeKind: interception` (`evidence.ts:2023`, the generic fallback
when neither a specific record failure nor a covers-nothing kind explains the gap).

**Condition:** `aee-c-12` — a caught intercepted row resolves an interception record

### `reconstructed-row-uncovered`

A row declares `method: reconstructed` but resolves no record that validly evaluates as
`aeeKind: examination` (`evidence.ts:2022`).

**Condition:** `aee-c-13` — a reconstructed row resolves an examination record

### `clean-row-uncovered`

A clean-label row resolves no record that validly evaluates as `aeeKind: arming`, or none that validly
evaluates as `aeeKind: sealed` — both are required (`evidence.ts:2024-2027`), and the code is shared
between the two requirements.

**Condition:** `aee-c-14` — a clean intercepted row resolves both arming and a covering seal

### `arming-covers-nothing`

A record's `aeeKind` is `arming`, but it fails one of the kind's constraints
(`evidence.ts:1870-1902`): an explicit `aeeBindingVersion` this verifier does not implement; a missing
`armedAt` or `aeePostureDigest`; a `method` other than `intercepted`; an `armedAt` that fails the
timestamp profile or falls after `issuedAt`; a posture digest that does not equal the statement's pinned
posture; malformed run-chaining syntax (`aeeRunSeq`/`aeePrevRunBinding`/`aeeChainScope`); or a missing
or malformed `aeeAssessedAttacks` array. See the structural note above this section for the
carried-but-unreferenced path this code also reaches through.

**Condition:** `aee-c-63` — the arming record's kind constraints

### `sealed-covers-nothing`

A record's `aeeKind` is `sealed`, but it fails one of the kind's constraints
(`evidence.ts:1904-1929`): `aeeStillArmed` missing or `false`; a missing `aeeDropCount`; a missing
`aeePostureDigest`; a `method` other than `intercepted`; a non-zero drop count with no declared bound or
one that exceeds it; a posture digest that does not match the pinned posture or (when arming postures
were collected) one of them; or a missing or malformed `aeeObservedSet`/`aeeObservedAttacks`. See the
structural note above this section for the carried-but-unreferenced path this code also reaches
through.

**Condition:** `aee-c-65` — the sealed record's covering conditions

### `examination-covers-nothing`

A record's `aeeKind` is `examination`, but its `method` is not `reconstructed`
(`evidence.ts:1931-1933`). See the structural note above this section for the carried-but-unreferenced
path this code also reaches through.

**Condition:** `aee-c-66` — an examination record's signed `aeeMethod` is `reconstructed`

### `record-kind-unknown-covers-nothing`

A record's `aeeKind` string is not one of the four defined covering kinds
(`interception`/`arming`/`sealed`/`examination`) and not one of the two registered non-covering kinds —
it falls to `evaluateKind`'s `default` arm (`evidence.ts:1945-1946`). Reachable only through a row's own
resolution path, never through `carriedRecordsCover` (see the structural note above).

**Condition:** `aee-c-71` — a record of unknown `aeeKind` covers nothing

### `moat-drop-covers-nothing`

A record's `aeeKind` is explicitly `moat-drop` — one of the two kinds the specification registers as
covering nothing whatever its payload otherwise says (`evidence.ts:1935-1940`). No member on such a
record is consulted; there is no state in which it could change the outcome. Reachable only through a
row's own resolution path (see the structural note above).

**Condition:** `aee-c-106` — a moat-drop record covers nothing in every state

### `uncommitted-observation-covers-nothing`

A record's `aeeKind` is explicitly `uncommitted-observation` — the second registered non-covering kind
(`evidence.ts:1942-1943`). Reachable only through a row's own resolution path (see the structural note
above).

**Condition:** `aee-c-107` — an uncommitted-observation record covers nothing in every state

### `payload-commitment-malformed`

An `interception`-kind record carries an `aeePayloadCommitment` member, but its array is not
well-formed: not sorted ascending and duplicate-free, empty, or containing an entry that is not
lowercase 64-hex (`commitmentArrayOk`, invoked at `evidence.ts:1858-1866`). This is distinct from
`payload-missing-reserved`'s interception case: that fires when the member is absent; this fires when it
is present and wrong.

**Condition:** `aee-c-104` — an interception record's `aeePayloadCommitment` is well formed

## GATE 1 — the statement-level requirements v0.7 adds

These hold over the whole statement, or over every row regardless of basis, rather than only over
`basis: substrate` rows — the per-row gate above skips any row that is not substrate, so a rule
belonging here would silently acquire that scope if it were checked there instead. Three run on every
statement, in `gate1CommitmentsAnyBasis` (`evidence.ts:2131-2164`); three read the derived run binding
and so only run when the statement has substrate rows, in `gate1CommitmentsSubstrate`
(`evidence.ts:2263-2355`).

### `clean-row-contradicted`

Some row's `containmentObserved` is a clean vocabulary label, yet one of the records its
`observationRefs` resolves to has `aeeKind: interception` (`evidence.ts:2140-2146`). A clean row is a
producer's assertion that nothing was intercepted for that attack; pointing it at an interception record
contradicts that assertion regardless of the row's declared basis.

**Condition:** `aee-c-94` — a clean row resolves no index to an interception record

### `interception-record-orphaned`

Some carried record's `aeeKind` is `interception`, but no row whose `containmentObserved` is a caught
label resolves an `observationRefs` index to it (`evidence.ts:2148-2160`). The converse of a caught row
being uncovered: this is an interception record nothing claims.

**Condition:** `aee-c-95` — every carried interception record is resolved by some caught row

### `sealed-record-absent`

The statement carries at least one `basis: substrate` row, and no carried record both passes every
byte-level payload check and validly evaluates as `aeeKind: sealed` under the derived run binding —
checked unconditionally, whether or not any row resolves an index to it
(`evidence.ts:2293-2312`).

**Condition:** `aee-c-96` — a statement carrying a substrate row carries a satisfying sealed record

### `observed-set-mismatch`

A sealed record bound to this run (`aeeRunBinding` equals the derived binding) carries an
`aeeObservedSet` digest that does not equal the recomputed digest over the leaf hashes of every
interception and examination record (`observedSetDigest`, compared at `evidence.ts:2279-2290`).

**Condition:** `aee-c-97` — a seal's `aeeObservedSet` equals the value recomputed over the carried records

### `observed-attack-uncaught`

A sealed record bound to this run names an attack identifier in `aeeObservedAttacks` for which no row
exists whose `containmentObserved` is a caught label — the rule reads in one direction only: a seal
*omitting* an attack obliges nothing (`evidence.ts:2320-2333`).

**Condition:** `aee-c-98` — every attack a seal names has a row whose `containmentObserved` is in the caught set

### `assessed-set-exceeds-declaration`

An arming record bound to this run declares an `aeeAssessedAttacks` set that is not a superset of the
attack identifiers implied by the statement's own `assessedClasses` coverage — a subset check, not
equality, so a run that lost coverage mid-way can still disclose the loss honestly
(`evidence.ts:2336-2353`).

**Condition:** `aee-c-99` — the carried `assessedClasses` are a subset of the arming record's `aeeAssessedAttacks`

## GATE 1 — the attribution binding rule

Three parts of one rule, written in `attributionBindings` (`evidence.ts:2171-2188`) in the order the
specification states them: existence, then the manifest's expectation, then the match. The existence
part is checked first because the other two are vacuously true over an empty set otherwise — a producer
that deletes the interception records would keep the stronger `pinned` label unless something asks
whether any remain.

### `attribution-pinned-recordless`

A row declares `attribution: pinned`, but the records its `observationRefs` resolves to include zero
that evaluate to `aeeKind: interception` (`evidence.ts:2175-2176`).

**Condition:** `aee-c-100` — a row declaring `attribution: pinned` resolves at least one interception record

### `attribution-unpinnable`

A row declares `attribution: pinned` and resolves at least one interception record, but
`corpus.manifest.expectedPayloads` carries no entry (or an empty one) for that row's `attackId`
(`evidence.ts:2177-2178`).

**Condition:** `aee-c-101` — a pinned row names an attack the manifest carries an `expectedPayloads` entry for

### `attribution-pin-unmatched`

A row declares `attribution: pinned`, resolves at least one interception record, the manifest has a
non-empty expectation for its `attackId`, but none of the resolved interception records'
`aeePayloadCommitment` values intersects that expectation (`evidence.ts:2179-2184`).

**Condition:** `aee-c-102` — every interception a pinned row resolves matches that attack's `expectedPayloads`

## Recompute-equality gate

Runs after GATE 1 passes, comparing the carried `result` against the pure recompute.

### `result-recompute-mismatch`

The carried `predicate.result` does not equal `recompute(s.predicate)` — the deterministic minimum,
under `fail < degraded < pass_indirect < pass`, of three independent conditions computed purely from the
rows, the on-wire vocabulary, and the coverage maps (`recompute`, `evidence.ts:2420-2447`; compared at
`evidence.ts:2554-2555`).

**Condition:** `aee-c-2` — `result` equals the value recomputed from the rows

## Consumer-policy step — anchor codes

The four codes in this section are checked by `anchorPolicyCodes` (`evidence.ts:2508-2520`), which runs
**after** GATE 2 (evidence tier) and writes its findings to `Report.policyCodes` — a field entirely
separate from `Report.codes`. None of the four can ever appear in `Report.codes`, none can ever be
`primaryCode`, and none ever changes `verdict`: a statement carrying one of these codes is still valid
evidence, about a corpus or substrate context the consumer never pinned or pinned to the wrong value. It
feeds only `Report.admitted`, the one consumer-facing admission result (spec `## Verification`).

### `corpus-anchor-mismatch`

The consumer's `ConsumerPolicy.expectedCorpusDigest` is set, and it does not equal the statement's
carried `observationEnvironment.corpus.digest.sha256` (`evidence.ts:2511-2513`).

**Condition:** none — structurally exempt. See [Exemptions](#exemptions).

### `substrate-anchor-mismatch`

The consumer's `ConsumerPolicy.expectedSubstrateDigest` is set, and it does not equal the statement's
carried `observationEnvironment.substrate.digest.sha256` (`evidence.ts:2515-2517`).

**Condition:** none — structurally exempt. See [Exemptions](#exemptions).

### `corpus-anchor-unpinned`

The consumer supplied no `expectedCorpusDigest` at all, and did not declare `allowUnpinnedAnchors: true`
(`evidence.ts:2514`). Pinning both anchors is a spec MUST, so silence is treated as a denial rather than
a vacuous pass — see the rationale at `evidence.ts:2489-2501`.

**Condition:** none — structurally exempt. See [Exemptions](#exemptions).

### `substrate-anchor-unpinned`

The consumer supplied no `expectedSubstrateDigest` at all, and did not declare
`allowUnpinnedAnchors: true` (`evidence.ts:2518`).

**Condition:** none — structurally exempt. See [Exemptions](#exemptions).

## Exemptions

`corpus-anchor-mismatch`, `corpus-anchor-unpinned`, `substrate-anchor-mismatch`, and
`substrate-anchor-unpinned` are the only four codes in the `Code` union with zero conformance-vector
coverage, and that absence is structural rather than a gap to close. Three reasons, all necessary:

1. **They are consumer-policy-dependent, not statement-dependent.** Every other code in this document is
   a pure function of the bytes a producer signed. These four are a function of a `ConsumerPolicy` value
   (`expectedCorpusDigest`, `expectedSubstrateDigest`, `allowUnpinnedAnchors`) supplied out of band by
   whoever calls `verifyStatement` (`evidence.ts:2542`) — the identical statement bytes produce all four
   possible outcomes (no policy code, `-mismatch`, `-unpinned`, or a match) depending on nothing the
   statement itself carries.

2. **They are recorded outside validity, on a field the conformance harness's `expected` block has no
   key for.** `anchorPolicyCodes` writes to `Report.policyCodes`, never `Report.codes`
   (`evidence.ts:2567-2572`); `verifyStatement`'s `verdict` and `codes` are already settled by the time
   anchor codes are computed. `aee-conformance`'s `vectors/gen_manifest.py` derives exactly five kinds of
   expectation from a vector — verdict, the failure-code set for a `reject` vector, the recomputed result
   for an `accept` vector, the declared readings for an `indeterminate` vector, and per-row evidence
   tiers (`gen_manifest.py:6-12`) — and none of the five is a consumer-policy admission outcome.

3. **The vector schema's `expected` block has no `admitted` key, and this is not an oversight to file a
   corpus request against.** A vector is a statement's bytes plus a `kind`; there is no schema slot
   in `MANIFEST.json` for a second input (a policy object) that a single vector would need to be paired
   with to make one of these four codes reproducible. Grepped confirmed empty on 2026-08-11: no
   `admitted` key appears in `vectors/MANIFEST.json`, any vector fixture, or `gen_manifest.py`'s output
   shape. Citing a forcing condition for one of these four would require inventing a policy-input
   schema extension the corpus does not have, which is a conformance-suite design question for the
   `aee-conformance` maintainers, not a documentation gap in this file.

`scripts/lint_failure_codes.py` hardcodes this four-code
exemption list independently of this prose section, and separately recomputes — straight from
`vectors/MANIFEST.json`, never from what this document claims — the full set of codes with zero real
vector coverage. If that recomputed set is ever not exactly these four (a fifth code loses its only
vector, or one of these four unexpectedly gains one), the lint fails rather than silently agreeing with
whichever of this prose or the corpus happens to be stale.
