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-byis schema-generic (Aegis decision D103): an upper-case identifier of[A-Z0-9-], with the bareREQ-refused. imago'sFLAVOR-*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 nosymbolappears twice infeatures, and thattarget-releaseis not older thanminimum-release. - JSON Schema names ECMA-262 as the dialect of
pattern, where$matches only at the end of the input. Python'srealso lets$match before a final newline, so an unmodified Python validator accepts acorrelation-idof"abc\n"that the schema, and the verifier, refuse.
Alternatives considered¶
- 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.
- Validate inside the verifier. The verifier stays stdlib-only, because downstream gates run
it with
python3 -I, which has nojsonschema. Validation lives in the workflow and the tests. - 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.
- 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.
- 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, andrequirements-test.txtlists it.
Neutral¶
abi.module-abiis 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¶
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 eachSCHEMA_GAPSentry is observed, tests that a schema drifted in either direction is reported, therequired-bygrammar, the command line, and the fetch against a stubgh.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.tests/test_manifest.pyandmake lint-manifest:versions.jsonvalidates againstversions.schema.json,downstream.requirement_schemaincluded..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.rsandtests/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.