Skip to content

A202-0015: Minimal fixtures, codes that are raised rather than declared, and a grade scope that can be written

Status: Fixtures and compatibility. Stage 3 of the five stages in README.md section 3. Every change in this proposal is implemented in the schemas, the runner's rule sets, the fixture set, the manifest, and the reference implementation, and the suite passes with the changes in place.

Date: 30 July 2026

Status of this document: Informative in full. It states no requirement on an implementation. The normative text this proposal amends is carried by the documents it names, each of which marks its own normative sections.

1. Problem

The review of 30 July 2026 continued after A202-0014 landed and found the same failure class one layer down. A202-0009 closed the case where prose claims a check the machine does not make. This proposal closes the case where the machine makes a check and nobody established that the check is the one the fixture was written for.

Three negative fixtures did not test what they declared. The repository README states the rule they break: every negative fixture is minimal, and removing the single offending element must leave a document that validates cleanly. Each of these three failed it differently, and each failure is a different way for a fixture to pass while measuring nothing.

  1. negative/agreement-single-signature.json was masked. Its payload was offer-shaped, carrying offeror, offeree, session_id, and valid_until and none of accepted_offer_id, accepted_offer_hash, acceptance_id, or terms_hash; its one signature carried the purpose offer_submission; and its declared code A202-AGREEMENT-HASH-MISMATCH was raised only by coincidence, because the evaluator compared an absent terms_hash against a recomputed one and found them unequal. What the fixture claimed to exercise, an agreement carrying one signature where the canonical model's section 10 requires both, was checked by no layer that returns a code at all: MINIMUM_SIGNATURES was applied in bundle_checks and nowhere else, so the single-object path counted no signatures on any object.
  2. negative/obligation-response-hash-mismatch.json was over-determined. All three objects in the bundle carried stale content hashes, so step 1 raised three A202-EVIDENCE-HASH-MISMATCH alongside the one A202-OBLIGATION-RESPONSE-HASH-MISMATCH the manifest declared. An implementation that failed the bundle for the wrong reason passed the fixture.
  3. negative/auction-event-unregistered-stream-kind.json carried two independent offences. The stream kind was auction, which v0.1 does not define, and the event's data carried round_index and closes_at, which no stream allowlist permits. The second offence was latent rather than visible: with an unregistered stream kind neither allowlist applies, so repairing the documented offence would have produced a second refusal rather than a clean document, which is exactly the condition the minimality rule exists to forbid.

Five declared reason codes were emitted by no layer. The runner asserts a negative fixture's declared code only where the normative layer raised any code at all; where the schema alone refuses, the assertion is skipped. A202-ANNOTATION-FORGED, A202-INVITATION-SECRET-DISCLOSED, A202-AUCTION-FORMAT-UNSUPPORTED, A202-LOT-UNKNOWN, and A202-ENDPOINT-INSECURE each appeared in the manifest, resolved in a refusal table, and were returned by nothing. Their fixtures would have passed on any schema error whatever, including one introduced by an unrelated edit, and the codes an implementation must return for those five conditions were never exercised. Two further negative entries declared no code at all, so nothing was asserted about them in either direction.

The fixture plan in A202-0006 section 5 had never landed, and could not have. That proposal registered three refusal codes and named seven fixtures for them, then said the fixtures would land in a later pass. They could not land, because the object they refuse has no written shape. Section 4 of conformance-grades-v0.1.md describes the scope field in one clause, "transaction profiles, transports, and roles covered", with the example that a grade earned as a bidder says nothing about behaviour as an event operator. Neither "bidder" nor "event operator" resolves in the role scope registry that document's dependant defines. There is no schema for a grade in schemas/v0.1, so there was nothing to write a fixture against, and A202-GRADE-SCOPE-UNKNOWN, A202-GRADE-SCOPE-INVALID, and A202-GRADE-SCOPE-OVERCLAIM were three codes in a table with no case that produces one. Section 2 item 6 of the role scope document requires an implementation to pass fixtures that did not exist.

The grade registry was classified MINOR against the rule that classifies it. Section 4.2 of A202-0006 states that the unscoped grade "stops being valid rather than being reinterpreted". Section 2 of RELEASES.md defines MAJOR as removing or narrowing a permission or adding a requirement, and resolves a genuinely ambiguous case as MAJOR. A field that accepted any string and now accepts one registered identifier is a narrowing by that rule, whether or not anyone holds a grade today.

2. Change

  1. The three fixtures are repaired to minimality. negative/agreement-single-signature.json is rebuilt as a well-formed bilateral agreement: a real agreementPayload, a terms_hash that is the hash of its own terms, a content hash over its own canonical bytes, and one agreement_commitment signature where two are required. negative/obligation-response-hash-mismatch.json carries recomputed content hashes on all three objects, so the response binding is the only offence raised. negative/auction-event-unregistered-stream-kind.json carries a plain offer reference in data, so the unregistered stream kind is the only offence and moving the event to the transaction stream leaves a document that validates cleanly. Each repair was checked in the form the rule states: the fixture raises exactly its declared code, and a copy with the documented offence repaired passes both layers.
  2. The kernel path counts signatures. An object type in MINIMUM_SIGNATURES carrying fewer signatures than it requires is refused with A202-EVIDENCE-SIGNATURE-INVALID, the code section 10 of the state machine registers for a required signature that is absent. No new code is registered, because the set already has the right one and the bundle path has emitted it for this offence since A202-0004. The check skips invitation_acceptance, whose count is decided by the more specific rule that names which two signatures must be present and refuses their absence with A202-INVITATION-CLAIM-UNSIGNED; raising a second, weaker code for the same absence would report one offence as two.
  3. The five orphaned codes are raised by the normative layer. An action_envelope carrying kernel_annotations is refused with A202-ANNOTATION-FORGED, which is the rule stated in the envelope table of canonical model section 3. A payload carrying a claim secret in the clear, at any depth and on any object type, is refused with A202-INVITATION-SECRET-DISCLOSED, per section 7 of the invitation document. An event on a stream kind v0.1 does not define is refused with A202-AUCTION-FORMAT-UNSUPPORTED, per sections 8.1 and 9 of the auction document, which require an undefined event format to be refused rather than approximated. A payload naming an award unit is refused with A202-LOT-UNKNOWN, per the same two sections: v0.1 registers no award-unit object, so no such reference resolves. An agent declaring an endpoint outside HTTPS is refused with A202-ENDPOINT-INSECURE, per section 10 of the state machine. Each check cites the section it implements, and each fixture now exercises the code an implementation must return rather than only the shape a schema refuses.
  4. The assurance invariant is enforced where it is stated. Section 12 of the canonical model states it over a declared assurance level rather than over one object type, and the runner enforced it on invitation_acceptance alone. It now applies wherever an assurance level appears, which gives negative/organization-carries-assurance-level.json the code it always had: a party claiming entity_bound with an empty identity_evidence_refs has declared a level above self_asserted with nothing behind it.
  5. A negative entry declaring no code must be refused by the schema. negative/envelope-carries-carrier-metadata.json keeps no declared code, and this is not an oversight. Section 5.4 of a2a-binding-v0.1.md states normatively that no refusal code is defined for carrier framing inside the signed bytes, that none is needed because the envelope and payload shapes are closed, and that the refusal is a kernel validation refusal reported as such; section 10 of the state machine forbids inventing one. The runner instead asserts that a negative entry with no declared code was refused by the schema layer, so that an absent declaration cannot cover a fixture nothing refuses at all.
  6. The grade object has a written shape. Section 4 of the grades document gains section 4.1, which states the structure of scope, of dimensions, and of held_out_coverage, and schemas/v0.1/conformance-grade.schema.json carries it. scope is an object with role_scopes, profiles, and transports. dimensions carries all five letters, each either null or an object with a band and the fixture families the band was established_from. held_out_coverage is null or an object with the dimensions exercised, the case count, and the families the cases were drawn against. A fixture family identifier is the family's name in the Family column of sections 4.3 and 5.3 of the role scope document, lowercased with spaces replaced by hyphens. The scope example that named "bidder" and "event operator" is replaced by the registered identifiers.
  7. The three grade codes are raised by an evaluator. A grade fixture kind validates a grade against the schema and then against the rules the schema cannot express: an identifier absent from the registry in section 3.1 of the role scope document is refused with A202-GRADE-SCOPE-UNKNOWN, naming none or more than one is refused with A202-GRADE-SCOPE-INVALID, and a band or a held-out coverage claim naming a family from the other scope is refused with A202-GRADE-SCOPE-OVERCLAIM. The overclaim check runs only where exactly one registered scope was named, because with no resolved scope there is nothing to read a coverage claim against and refusing twice for one unreadable field would report one defect as two.
  8. The partition gains a conformance-grade family in each scope, sections 4.3 and 5.3 of the role scope document, and states how a family identifier is formed. The two grades in the allow direction go to the scope each names. The two overclaim negatives name the bilateral scope and are placed by that. The three scope-refusal negatives are placed in the bilateral scope and the reason is stated in the document: none of them names a scope that resolves, so none can be placed by the scope it claims, and the duty they exercise is items 2 and 5 of section 2, which binds any grade consumer. Refusing an unreadable grade requires no operator-issued object, no ordered stream, and no third participant, so the authorship test of section 5.4 does not reach them. Item 6 of section 2 now names the fixtures it requires, and they exist.
  9. A202-0006 moves to stage 3 and its compatibility classification is corrected to MAJOR, citing the ambiguity rule in section 2 of RELEASES.md. The observation that no grade has been issued stands, and is what makes the correction cost nothing.
  10. The reference package sweeps the bundle fixtures. reference/tests/test_fixture_sweep.py routes every evidence-bundle fixture in the manifest through a202_reference.verifier directly rather than through the runner subprocess, and asserts agreement with the manifest: a positive bundle produces no failed check, and a negative bundle whose offence lies inside the seven-step procedure produces the corresponding failed check. A bundle outside the procedure's remit is skipped with a stated reason and counted, and the test prints how many it swept and how many it skipped. It is informative tooling and states no requirement; the runner remains the normative layer.

3. Schema-refused fixtures still get no per-code assertion, and that is right

This proposal does not change the runner's policy that a fixture the schema alone refuses is legitimate, and eleven negative fixtures remain in that class. For each of them the manifest declares a code, the schema refuses the document, and the runner asserts nothing about the code. That is an honest gap and it is worth naming rather than closing.

It is acceptable because the schema is the normative shape layer, not a convenience. The kernel schema is the definition of the closed envelope and the closed payloads; a private field in a shared object, a multibase content hash, a private key member in a key record, and a free-text member on an event stream are refused by that definition and not by an evaluator reading around it. The declared code names what an implementation returns to its counterparty, and a static validator reports a shape error rather than a reason code, so asserting the code against the schema layer would mean writing a second evaluator whose only purpose is to restate what the schema already refuses, and whose disagreement with the schema would be a new failure mode. What this proposal does close is the case where the code was returned by nothing anywhere, which is different: there the fixture measured the shape and the code measured nothing, and the manifest said otherwise.

4. What this proposal does not change

No shared object changes shape. No state, transition, guard, or rules version changes. No role scope identifier changes meaning, and neither scope gains or loses a capability, a specification section, or an existing fixture. The runner's policy on schema-layer refusals is unchanged, and so is the fixture partition's total-and-disjoint property, which holds over the enlarged set. Profile and transport coverage inside scope keep the treatment they already had: they are carried verbatim, and this proposal states no registry for either, because section 4.2 of A202-0006 said the change does not touch them and nothing here needs it to.

5. Alternatives considered

Why not register a new code for an agreement carrying one signature? A new code would say precisely what happened, and the set already has a code that says it: section 10 of the state machine defines A202-EVIDENCE-SIGNATURE-INVALID as covering a required signature that is absent, and the bundle path has returned it for this offence for two proposals. Registering a second code for the same condition would mean two codes an implementation must handle identically, which is the reasoning the carrier declaration already settled: a caller handed several codes that must be handled the same way will eventually handle one of them differently.

Why not give negative/envelope-carries-carrier-metadata.json a code anyway? Because the binding says there is none, and says why. Inventing one would contradict a normative sentence in the document that owns the rule, and section 10 of the state machine forbids an implementation from inventing a code that resolves in no table. The finding that this entry declares nothing is correct as an observation and wrong as a defect. What was genuinely missing was any assertion at all, and that is what the schema-refusal assertion adds.

Why not split the second offence out of the auction fixture into its own fixture? The second offence is an event carrying members no stream allowlist permits, and the set already fixtures that twice, in negative/transaction-event-data-free-text-note.json and negative/session-event-data-carries-terms.json, both with the code it produces. A third would add an entry and no coverage. Removing the offence is what the minimality rule asks for, and the fixture then measures the one thing its note claims.

Why an array for role_scopes when exactly one is permitted? A single string would express the rule in the shape and would make two of the five refuse-direction fixtures unwritable: a grade naming two scopes could not be serialised at all, and a grade naming none would be a missing member rather than a refusal. Section 3.2 of the role scope document gives both the same code, which is a statement that both are one kind of error about a field that exists, and a shape that cannot express an error cannot fixture it. The array carries a maximum and no minimum, so the cardinality rule fails closed at the layer that returns the code.

Why make dimensions carry what established each band, rather than leaving it a band per letter? Because A202-GRADE-SCOPE-OVERCLAIM is otherwise uncheckable. Section 6.3 item 3 of the role scope document refuses "a band in a dimension established from fixtures outside the named scope", and a band that records only a number does not say what established it, so the refusal could be stated and never raised. That is the defect this proposal exists to remove, appearing inside the repair for it. The addition is one member on an object that was already going to be an object, and it is what makes the two overclaim fixtures distinguishable: one is caught through dimensions and one through held_out_coverage, which is the distinction A202-0006 section 5.2 drew when it wrote them.

Why a schema file rather than runner-side checks alone? Section 4 of the grades document already says a grade is emitted as a signed machine-readable object versioned alongside the kernel schemas, so the schema is what that sentence promised. It also gives the object's own design constraints a layer that enforces them: a closed top level is what makes "no overall score" checkable, and five required dimension members are what makes "absence is not a pass" checkable. Neither had any enforcement before. The three role scope refusals stay in the evaluator, because each of them must produce a reason code rather than a shape error.

Why not put the grade on the common envelope? A grade is a statement about an implementation rather than an act inside a transaction. It has no transaction, no version chain, and no place in the aggregate, and adding conformance_grade to the kernel's object_type enum would put a conformance concept inside the market-neutral kernel. The mandate is the precedent and it is exact: a standalone signed document with its own schema, referenced rather than wrapped, for the same reason.

Why place the three scope-refusal negatives in the bilateral scope rather than in the operated one, or in both? In both would break the disjointness A202-0014 established and that section 4.3 now asserts. In the operated scope would be a claim that refusing an unreadable grade needs an operator, which the authorship test in section 5.4 does not support: there is no operator-authored object, no ordered stream, and no third participant anywhere in the condition. The bilateral scope is where a capability sits when nothing puts it in the operated one, and that residual rule is the one the partition has used throughout.

Why not do nothing about A202-0006's classification? Because the classification rule is the one thing in RELEASES.md that a reader relies on without reading further, and a MINOR label on a change that invalidates every grade written before it is exactly the false statement section 2 says the scheme exists to prevent. Correcting it costs one paragraph now and nothing at all, because no grade exists; leaving it would cost the credibility of every other classification in the set.

6. Compatibility

Under RELEASES.md section 2 the set is pre-release and pre-1.0, and section 5 of that document records that no release has been made. This change set is classified MAJOR-shaped, by the rule rather than by feel.

It is MINOR-shaped where it adds: one schema file, one fixture kind, seven fixtures, two rows of the fixture partition, one subsection of the grades document, and a reference test. Nothing conformant before becomes non-conformant on account of any of those, because no implementation could have been conformant with respect to a grade object whose shape was not written down.

It is MAJOR-shaped in three places. Six refusals that no layer produced are now produced, so an implementation that returned a generic error for an agent endpoint over plain HTTP, an award-unit reference, an undefined stream kind, a disclosed claim secret, forged annotations, or a self-declared assurance level was already non-conformant against the tables that define those codes, and is now measured against them. The kernel-path signature count is a refusal at issue where there was only a schema error. And the grade's scope field narrows to a registry, which is the correction carried in section 4.1 of A202-0006 and restated here.

Nobody is owed a migration. No release exists, no grade has been issued against either scope identifier, and the repository was private when this landed. The migration surface, stated for the record, is three items: an implementation that emitted a grade with a free-form role string now names one registered identifier and records what each band was established from; an implementation that relied on the five orphaned conditions being refused only by shape now returns the registered code; and an implementation pinned to the contents of the three repaired fixtures rereads them, since all three changed bytes while keeping their names and their subject.

7. Fixture plan

Implemented, not planned.

Added. valid-conformance-grade-bilateral-scope, a grade naming a202-scope/bilateral/0.1 with all five dimensions established from bilateral families and no held-out set. valid-conformance-grade-operated-scope, the same in the operated scope, with dimension A explicitly null because the operated coverage of authority handling is the invitation-onboarded path this assessment did not exercise, which is the allow direction of the rule that an unassessed dimension is null and never omitted. negative/grade-scope-unregistered, refused with A202-GRADE-SCOPE-UNKNOWN. negative/grade-scope-absent and negative/grade-scope-two-scopes, both refused with A202-GRADE-SCOPE-INVALID. negative/grade-bilateral-claims-operated-dimension and negative/grade-bilateral-claims-invitation-coverage, both refused with A202-GRADE-SCOPE-OVERCLAIM, held separately because a check written only against dimensions would miss the second. Each of the five negatives is one mutation away from the valid bilateral grade, and repairing that mutation yields a document that validates cleanly.

Repaired. negative/agreement-single-signature, rebuilt as a true minimal single-signature agreement and redeclared with A202-EVIDENCE-SIGNATURE-INVALID. negative/obligation-response-hash-mismatch, content hashes recomputed so the response binding is the only offence. negative/auction-event-unregistered-stream-kind, event data brought inside the transaction-stream allowlist so the unregistered stream is the only offence.

Redeclared. negative/organization-carries-assurance-level gains A202-ASSURANCE-UNSUPPORTED, which the generalised invariant now raises for it.

The suite passes at the totals the manifest carries, with the reason assertion active, every declared code raised by a layer or refused by the schema under the stated policy, and the partition total and disjoint over every fixture on disk.

8. Ordering

Depends on A202-0006, whose section 5 fixture plan this proposal implements and whose compatibility classification it corrects, and on A202-0014, whose fixture partition it extends and whose total-and-disjoint property it preserves. It relies on the reason assertion A202-0009 added to the runner and strengthens it. Nothing depends on this proposal.