Skip to content

ADR-0010: Requirement Documents Are Read Through the Owner's JSON Schema, Pinned by Commit and Digest

Date: 2026-09-29

Status

Accepted (2026-09-29)

This ADR adds a check to ADR-0007 and does not supersede any of it: the decoding rules of its section 2, the policy of its section 3 and the evidence levels stand. It records one thing that ADR-0007's frozen body cannot: the two divergences it names in section 2 are closed at the owner (section 4 below).

It merged as Proposed, and the owner of this repository accepted it the same day.


Context

scripts/verify_kernel_requirement.py is a hand port of the owner's decoder. Aegis-OS owns the aegis.p01-nucleus.kernel-requirement.v1 contract, and until 2026-09-29 the port was the only reading of that contract outside Aegis-OS. A change on the owner's side that the port did not follow gave no signal here.

Aegis-OS#167 (merged as 18c9791) publishes build/kernel-requirement.schema.json: JSON Schema 2020-12, generated by schemars from the owner's Rust types, a closed object at every level, with every field encoding as a pattern and every bound as minItems, maxItems or maxLength. A test in the owner's repository (crates/aegis-fabrica-defs/tests/json_schema.rs) fails when the file differs from what the types generate today, so the schema is the owner's machine-checked statement of the document's shape.

The same change decided the two questions ADR-0007 section 2 left open to the owner:

  • required-by is schema-generic (Aegis decision D103): an upper-case identifier of [A-Z0-9-], with the bare REQ- refused. imago's FLAVOR-* identifiers decode.
  • The array form is refused: every struct of the contract decodes from an object only.

It also made architectures all-of and fail-closed (D104), which is what ADR-0007 section 3 already required of this forge.

Two facts of the schema shaped what could be checked:

  • JSON Schema cannot state three rules the owner's decoder enforces (kernel.rs, payload.rs): the 16384-byte bound on the whole document, that no symbol appears twice in features, and that target-release is not older than minimum-release.
  • JSON Schema names ECMA-262 as the dialect of pattern, where $ matches only at the end of the input. Python's re also lets $ match before a final newline, so an unmodified Python validator accepts a correlation-id of "abc\n" that the schema, and the verifier, refuse.

Alternatives considered

  1. Vendor the schema and test against the copy only. Proves the fixtures, not the live documents, and does not prove that the copy is what the owner published.
  2. Validate inside the verifier. The verifier stays stdlib-only, because downstream gates run it with python3 -I, which has no jsonschema. Validation lives in the workflow and the tests.
  3. Read the owner's schema from its default branch on every run. A change on the owner's side would fail this repository's CI with no change here, and what "valid" means would move without a review. The documents are already read at the default branch, so a change of contract still shows: as documents that stop validating against the pinned schema.
  4. Pin the schema per requirement row. The schema belongs to the contract shape, which both rows share, not to a consumer. One entry, next to the rows, applies to all of them.
  5. Report a mismatch without failing. A mismatch nobody has to fix is not a signal.

Decision

1. The schema is pinned in versions.json

downstream.requirement_schema names the owner's repository and path, the commit the schema is read at, the sha256 of the file at that commit, and the copy vendored at tests/fixtures/kernel-requirement/aegis-kernel-requirement.schema.json. versions.schema.json validates its shape. No other file carries the commit or the digest (tests/test_requirement_schema.py). The procedure that refreshes the pin is in docs/streams.md, section 5.

2. make test holds the copy, the fixtures and the constants

  • the vendored file's sha256 is the pinned one;
  • both committed documents validate against it and are accepted by the verifier, imago's FLAVOR-* identifiers included;
  • the schema's field names, enumerations, bounds and closedness equal the verifier's constants, so a contract change fails by name;
  • an unknown top-level field fails the schema and is refused by the verifier as Malformed; 64 features are valid for both and 65 are invalid for both.

3. verify-requirements.yml reads every fetched document twice

Before the verifier's verdict, the workflow fetches the schema at the pinned commit (scripts/check_requirement_schema.py fetch), refuses it unless its sha256 is the pinned one, and runs scripts/check_requirement_schema.py check over every fetched document. check validates the document against the schema and decodes it with the verifier's parser, and reports one of four verdicts. Only the first passes:

Schema Verifier Verdict
valid accepts accepted by both
invalid refuses refused by both: the document is invalid
invalid accepts mismatch: drift, or a rule the port invented
valid refuses mismatch: named with the verifier's refusal kind

check also refuses a schema that is not the pinned file, whichever way it was obtained.

3.1 The schema is read as ECMA-262 reads it

scripts/check_requirement_schema.py matches a pattern with \Z in place of a closing $. Every pattern of the owner's schema is ^...$ with no escape sequence, which tests/test_requirement_schema.py holds, so this is the whole difference between the dialects. A document with a repeated field or a non-finite number is no JSON value; both readings refuse it.

4. ADR-0007's two divergences are closed

required-by and the array form were the two places where the verifier differed from the owner's decoder. Aegis-OS#167 moved the owner to the verifier's side of both (section "Context"). The verifier accepts exactly what the schema's required-by pattern accepts: a corpus test compares every string of up to six characters over a small alphabet, the REQ- boundary included. The array form is refused by both. The verifier's behaviour is unchanged; only the comments that called these divergences are.

5. One exception list, three entries

SCHEMA_GAPS in scripts/check_requirement_schema.py names the rules of the owner's decoder that JSON Schema cannot state: TooLong, DuplicateSymbol and InvertedRelease. The verifier follows the decoder here (ADR-0007 section 2, "same"), so a document that breaks one is schema-valid and verifier-refused, and the report says why the schema cannot see it. The corpus must exercise every entry, so an exception that outlives its reason fails, and any other disagreement, in either direction, fails. The list is not a place for a difference ADR-0007 does not record.


Consequences

Positive

  • A change to the contract on the owner's side reaches this gate as a failing name: a constant that differs, or a corpus case on which the two readings split.
  • A change to the port that admits more or less than the owner's schema fails on the same corpus.
  • The workflow proves the vendored copy is what the owner published at the pinned commit.

Negative

  • Moving the pin is a reviewed change to three places: the vendored file, versions.json, and whatever the contract change requires of the verifier.
  • Three rules stay with the verifier alone. The schema does not confirm them, and the owner's decoder tests remain their authority.
  • The workflow and the tests need jsonschema. The workflow already installed it, and requirements-test.txt lists it.

Neutral

  • abi.module-abi is accepted by both readings; the verifier fails a document that sets it in its policy, not its decoder (ADR-0007 section 3).
  • The check runs on the fetched documents before their dispatched digest is proven by the verifier. It reads them as data and writes nothing back.

Compliance

  1. tests/test_requirement_schema.py: the pin and the vendored file, the schema against the verifier's constants, the committed fixtures, the acceptance of issue #38, a corpus of documents (the fixtures, every field at its boundary and past it, missing, unknown and mistyped fields, repeated fields, byte-bound and unparseable inputs) that fails on any disagreement between the two readings and lists each one, a test that each SCHEMA_GAPS entry is observed, tests that a schema drifted in either direction is reported, the required-by grammar, the command line, and the fetch against a stub gh.
  2. tests/test_requirement_schema_workflow.py: the fetch and the check precede the verifier's verdict, every run block of the verifying jobs is strict, and every path that changes the check re-runs the workflow.
  3. tests/test_manifest.py and make lint-manifest: versions.json validates against versions.schema.json, downstream.requirement_schema included.
  4. .github/workflows/verify-requirements.yml: fetches and checks on dispatch, weekly, and on every pull request and push that touches the check, the pin, the schema copy, the fixtures or the workflow.

References

  • nucleus issue #38; praetor#607 (the fleet-level mechanism for this class of check).
  • Aegis-OS#167 (18c9791): build/kernel-requirement.schema.json, decisions D103 to D106, crates/aegis-fabrica-defs/tests/json_schema.rs and tests/array_form.rs.
  • JSON Schema 2020-12, Validation section 6.3.3 (pattern) and Core section 6.4 (regular expressions, the ECMA-262 dialect); ECMA-262, RegExp pattern semantics (Assertion :: $ without the multiline flag).
  • ADR-0007, sections 2 and 3.