The quickest way to understand what this repository will and will not accept is to read the compatibility section of the README. Everything below follows from it.
npm ci
npm test # the conformance corpus against the reference adapter
npm run check:additive # the lock is current, and this change is additive
npm test is conformance/run.mjs, and it checks more than the corpus. The runner
(conformance/runner.mjs): every valid fixture accepted; every invalid fixture rejected with a
written reason and, when the adapter reports its errors, at the instance path its .expect.json
names; a valid config's sealed paths each naming a field the fixture carries; a round trip that
must not lose an unknown field at the top level or inside a nested object; the canonical-bytes
vectors of SPEC-agent-lease-manifest.md §7 and RFC 8785's own; the
narrowing vectors (shape for every adapter, behaviour when narrow is supplied); the signature
vectors when hash and verify are supplied; and the lease rules L1 to L3 when rules is
supplied. A half the adapter does not offer is reported as not checked, never as passed. Then the
schema checks (conformance/schema-checks.mjs): every schema compiles under Ajv strict mode, every
inlined copy is byte-for-byte its source dereferenced, every schema validates against
conformance/metaschema.json, schemas/index.json is valid and current, and
cdfContract.schemaSetVersion matches the package version. Then the guard's own scenarios
(conformance/guard-tests.mjs), the README's Layout block (conformance/layout-check.mjs), the
SPEC traceability map (conformance/traceability-check.mjs), the currency of the exported suite
(conformance/suite/), and the version statement: package.json (twice), the README, the
CHANGELOG (a section with a body and a link definition), schemas.lock.json, every $id and the
SPEC header agree, and nothing still names the old $id host. The version check was added
because the lock silently kept the previous version through a release.
npm run check:additive -- --baseline-ref origin/main compares the schema set against the shape
it had at that ref, digesting the baseline's schemas with the current generator; in CI the baseline
is the branch you are merging into, or the pushed-from commit. It reports named findings in the style of
buf breaking: FIELD_REMOVED, FIELD_NOW_REQUIRED, TYPE_CHANGED, ENUM_NARROWED, STABILITY_LOWERED, CONDITIONAL_REQUIRED_ADDED,
PATTERN_TIGHTENED, BOUND_TIGHTENED, CONTENT_MODEL_CLOSED, UNION_CHANGED and
SCHEMA_REMOVED. Since lock format 4 a local $ref is digested at the path that refers to it, so re-pointing a property to a stricter definition is a visible change rather than a changed string the guard ignores. PATTERN_TIGHTENED, BOUND_TIGHTENED, CONTENT_MODEL_CLOSED and UNION_CHANGED need a baseline
in lock format 2 or later (lockFormat in schemas.lock.json), and against an older one are
reported as not comparable rather than passed quietly; a union's branch types and patterns are
compared from format 3; STABILITY_LOWERED and CONDITIONAL_REQUIRED_ADDED need format 5 and are
not compared against an older baseline; and a re-pointed $ref is visible only against a format
4 or later baseline. A --baseline-ref baseline is always digested with the current generator,
so an older format arises only with an explicit --baseline lock file. Note what the check does not do: it compares schema shapes only, and will
report "Lock is current" on a lock whose version field is stale. That check lives in npm test,
as do the guard's own scenario tests (conformance/guard-tests.mjs): every finding is seen to
fire and every additive change seen to pass, because a guard nobody has watched fail is a
sentence in a README.
Sign off every commit (git commit -s): the DCO check refuses a pull request with a commit that
has no Signed-off-by trailer, and it is the Developer Certificate of Origin, not a copyright
assignment. A semantic change — a new schema, a field whose meaning is not obvious from its
name, a narrowing rule or refusal code, a reserved vocabulary, a change to what the corpus checks —
starts as a one-page proposal on docs/proposals/TEMPLATE.md, with its
Backward compatibility and Security sections answered; GOVERNANCE.md says who
decides and how. A fixture, a tightening with its evidence, a description or a tooling fix needs no
proposal. The pull-request template lists what the checks cannot see.
Every normative clause of SPEC-agent-lease-manifest.md is mapped in conformance/traceability.json.
Adding a clause (a sentence with a bold MUST, MUST NOT, SHOULD, SHOULD NOT or
MAY) fails npm test until you add its entry: run node conformance/traceability-check.mjs to
see the id and key it was given, then map it to the fixture, vector, check or rule that tests it,
or exclude it with a reason. Editing a clause changes its key and fails the same way: re-affirm the
entry by updating its key. Inserting a clause earlier in a section renumbers the ones after it,
which the check reports as stale entries plus unmapped clauses; add new clauses at the end of a
section where you can.
Every declared property carries a $comment beginning stability: stable or
stability: development (conformance/metaschema.json refuses one without it, and npm test runs
that check). stable is a promise under the additive rule. development says the field may change
within the major, and the guard lets it: a development field may be tightened or removed without
an allow-list entry, which is the point of marking it. Lowering a field from stable to
development is STABILITY_LOWERED, breaking, because it withdraws a promise. To retire a field,
append ; deprecated: <what to use instead> to its comment, which is additive, and keep the field
until the next major: FIELD_REMOVED names a deprecated field as still a field. The marker is a
draft-07 $comment rather than a custom keyword because Bowtie showed a strict validator elsewhere
refusing one, and a schema only this repository can compile is not portable. Lock format 5 records
stability, deprecated and conditionalRequired (the names an allOf if/then makes
required; adding one is CONDITIONAL_REQUIRED_ADDED).
Sometimes a schema must refuse what it used to accept: a hash field that was always meant to be
SHA-256 gains a pattern, or a path rule gains a form it should never have allowed. The guard
reports each of these as breaking, and it is right to. The way through is
compat-allowlist.json: one entry per finding, naming finding, schema, path, after (the
exact value admitted, as the guard prints it: pattern=^[0-9a-f]{64}$, maximum=16,
enum=[...]), a reason, a date, and an evidence file (normally the invalid fixture that
proves the refused form, or a recorded check of real artefacts under fixtures/evidence/ showing no
conformant writer ever produced it). after is what stops an entry outliving its tightening: the
same finding at the same path with a different value is a new tightening and needs its own entry,
so the entry that admitted a maximum of 2^53 does not quietly admit lowering it to 16. The guard
refuses an entry whose evidence file does not exist, and refuses a change that drops an entry the
baseline had: the list is history, never configuration. An entry is a claim that no reader in a customer's
repository loses a record it depends on, and it is reviewed like a schema change.
An entry's path may name a definition rather than a property. Since lock format 4 a constraint
reached through a local $ref is recorded with via, the place it lives in the definition
(#allow.read_paths[] for allow.read_paths[], as schemas.lock.json records it), and the guard
accepts an entry whose path is either the property path or that via, in the same schema. So
one entry at a definition's path covers every property in that schema that refers to it, which
is why 110 entries cover the 115 tightenings the guard reports against v1.1.0. It does not reach
across files: the copies inlined in agent-lease and lease-record are separate schemas and
carry their own entries.
SPEC-agent-lease-manifest.md is normative, in RFC 2119 language, and the schemas are not free
to disagree with it. If a change makes the two say different things, one of them is wrong and
the pull request has to say which.
Section 7 — canonical bytes — does not change within a major version. Every hash in this
contract is taken over those bytes, so a change there silently invalidates every signature anyone
has ever produced. Since 1.1 the section says canonical bytes are RFC 8785 after
undefined-removal, and the corpus carries RFC 8785's own vectors (conformance/jcs/, vendored at
a pinned commit with its licence and source recorded) beside the contract's nine; a pull request
that touches §7, canonical-vectors.json or jcs/ is a 2.0 pull request, whatever else it says.
- Change the schema.
- Add or update fixtures. A new optional field belongs in the
populatedfixture for that shape, not theminimalone. - Run
npm run lockand commitschemas.lock.json. The lock is the record of the shape before the next change; a schema change without it leaves nothing to compare against. Ifnpm run check:additive -- --baseline-ref origin/mainreports a finding, either loosen the change or add an allow-list entry with its evidence (see "Tightening a schema within 1.x"). - Run the checks.
Anything that is not additive, within 1.x. A new required field, a removed field, a
narrowed type, a closed enum where there was none, or an enum that lost a value. Customers have
these files in their repositories and older readers are still reading them. These need 2.0.0
and a documented migration.
A new value in cdi-signal.event_type, or a change to the six CDI dimension ids, as an
ordinary change. The Index is computed from that vocabulary, so adding to it changes what the
Index measures. That is a contract change and is governed by ADR-013.
A reader that throws on something it does not recognise. phase is an open string, unknown
source forms are reported rather than rejected, and unknown fields survive a round trip.
A fixture under invalid/ with no .reason file and no .expect.json beside it. "This should
fail" with no reason is untestable folklore, and "this should fail" with no place is a rejection
that can happen for the wrong reason and pass; the runner fails the build for either. The
.expect.json is { "path": "/where", "keyword": "why" }: path must match an error the
implementation reports, keyword is informative.
A signed fixture that does not verify under conformance/test-key.txt. Re-sign it with the
test key; never commit a lease signed with a real key, and never commit a real key.
Anything that widens what may reach a permanent record. See SECURITY.md.
Open an issue with the Defect template (what is wrong, what the contract says, which half is wrong) or the Proposal template for a change. For a security problem, follow SECURITY.md instead; the issue form links to the private advisory.