Skip to content

spec(0.9.0): the extension container is granted at the MetaLogDiff root - #6

Merged
coderoast-dev merged 4 commits into
mainfrom
spec/diff-root-extensions-grant
Aug 24, 2026
Merged

coderoast-dev merged 4 commits into
mainfrom
spec/diff-root-extensions-grant

Conversation

@coderoast-dev

@coderoast-dev coderoast-dev commented Aug 24, 2026 •

Copy link
Copy Markdown
Collaborator

Additive under GOVERNANCE.md §2 — a new optional field and a new placement row. No field changes type, becomes required, or is removed. It joins the existing ## [0.9.0] — unreleased section rather than opening a release: v0.8.0 is still the last tag.

Executes adr/0003 decision 4, whose own words are the reason this is not deferrable: "Until the grant lands, the member has no legal home."

What was wrong

§7 makes extensions the only carrier of non-standard members and forbids the bare form at any depth. Its placement table named the MetaLog document root and stats.top_k[]. MetaLogDiff had no extensions member at all — so vendor data on a diff document had no legal home: the bare member is forbidden, and the only containers the spec granted were on the other document type.

Two consequences were already live in the text, not hypothetical:

  • §13.2 instructs producers to use a placement the spec had not granted. It says a producer MAY report a truncation cap in extensions.org.metalog.deltas_truncated_at — at the diff root. Following that sentence meant writing a container §7's table did not name.
  • Any producer with a per-diff vendor datum had to choose between a forbidden member and dropping the datum.

What this changes

§7's table gains a third row — the MetaLogDiff document root, at v0.9.0 — and the first two rows now name which document they belong to, since "the document root" was no longer unambiguous.

schema/metalog_diff.v0.schema.json gains the extensions property, referencing #/$defs/extensions. The grammar is duplicated into that file's $defs rather than $ref-ed across files, because both schema files are independently consumable — §8 invites downloading one alone — and a cross-file reference cannot be resolved offline. §7 now states that, and the two copies are asserted identical (below).

§13.1's example carries the container, and §13.2's deltas_truncated_at MAY now names a placement that exists.

The limit this does NOT close, stated plainly

Granting the container does not make §7's placement MUST enforceable on the diff document. The diff root is additionalProperties: true, so a bare vendor member written beside the container still validates and is invisible to §8 clause 1. The grant fixes where vendor data belongs; it adds no detection of data put elsewhere.

Two things follow, and both are in this PR:

  • §7's own claim about enforcement was false and is corrected. The placement paragraph said a bare member "is a conformance failure §8 clause 1 detects". That holds inside a closed object and fails at either document root — both are open. §7 now separates the two cases, and conformance/README.md carries the same limit in its "what this does not reach" list. No normative effect: the MUST is unchanged; only the spec's claim about what enforces it changes.
  • What the grant does add is narrower and real: the container's own reverse-DNS grammar is now enforced at this root. Before, an extensions object there was legal-but-undescribed with any keys at all.

Closing the diff root would make the placement rule mechanically decidable there for the first time. It is a breaking change under GOVERNANCE.md §2 and nothing here presumes it either way — that decision is the editor's, and this grant neither performs it nor waits on it.

Is a fixture owed?

Yes, and it is one fixture proving the grant in both directions inside a single document — conformance/fixtures/invalid/unprefixed_extension_key.diff.json:

what it holds how
the container's grammar is now in force at this root topology_delta written bare inside extensions is rejected
the grammar admits the legal form com.example.topology_delta beside it validates, and the report names only the offender
the limit above vendor_private_counter bare at the root is reported legal-but-undescribed and changes no exit code

Mutation-measured, on both ways a wrong grant could look right. Remove properties.extensions from the diff schema and exactly 1 of 14 fixtures reds, in all three directions the manifest predicts (the finding disappears, the exit drops to 0, and extensions itself joins the undescribed list). Grant the property but wire it to a free-form {"type": "object"} instead of $defs/extensions — a schema that still "grants extensions" to anyone skimming it — and the same fixture reds on the finding.

Its teeth cannot fall out the way component made another fixture's fall out in 0.8.0: no future standardisation can make an unprefixed key legal inside extensions, whatever the standard mints elsewhere.

Two commits that are not the grant, and are here because the grant found them

This PR creates a third copy of a duplicated grammar. Before adding one, the duplication itself needed a guard — and the guard reds on the tree it was written against.

--selftest now compares every $defs name present in more than one shipped schema (commit 4). The copies must agree in every keyword but description, which is the single exclusion and is deliberate: a copy has to be able to say that it is one. The set is derived from the artifacts, never enumerated. A drift is exit 2, not exit 1 — it is not a defect in anyone's documents, it is the standard's own artifacts disagreeing.

Run against the shipped v0.8.0 pair it reds, and names the member:

$defs/cube_axis: properties only in metalog.v0.schema.json: ['band_floor']

That is a live defect, and commit 3 repairs it. band_floor was added to $defs/cube_axis in metalog.v0.schema.json in v0.8.0 and not to the mirror in the diff schema. Both copies are closed objects, so the two files disagreed about what an axis is — and §13.6 requires cube_diff.axes to equal both inputs' cube.axes while §16.10 stamps a collapsed axis with band_floor. A diff of two collapsed cubes was rejected by the diff schema while both of its inputs validated. Measured on the shipped pair: {"name": "level", "kind": "categorical", "band_floor": 2} is accepted by metalog.v0.schema.json, rejected by metalog_diff.v0.schema.json on additionalProperties.

Additive: the mirror only begins admitting what its source already admits, and no document becomes invalid. It is latent rather than harmless — no published diff carries a cube_diff, so nothing has hit it yet, but the reference implementation builds a diff's axes from the same descriptor it builds a document's from, so the first diff of two collapsed cubes would have carried the rejected member.

The two commits stand or fall together: drop the repair and the guard reds. Drop both and the grant still stands on its own two commits.

Why nothing here invalidates a 0.8.0 document

0.9.0's section declares that no conformant 0.8.0 document becomes invalid. The one class that could have broken it is a diff document carrying a root extensions object with a key that is not reverse-DNS: legal against the 0.8.0 schema (open root), rejected now that the container is described.

Such a document was never conformant under 0.8.0. §7's placement rule already forbade it twice over — an extensions container at an ungranted object is itself a non-standard member written bare, and §7's key grammar was already a MUST. A 0.8.0 producer following §13.2's org.metalog. example matches the reverse-DNS pattern and stays valid.

The band_floor repair only widens what validates. Nothing else touches a shipped keyword.

Gates

Run on this branch, jsonschema 4.26.0, Draft 2020-12:

leg result
--selftest 14/14 fixtures · 5 controls armed · mirrored $defs agree: cube_axis, cube_coord, extensions
schema/metalog.v0.example.json 1 document · CONFORMANT
published determinism evidence 10 sections · 17 documents · CONFORMANT — 0 schema-invalid, 0 cap-exceeded, 0 undescribed
published diff documents (--pointer /raw --expect-documents 2) 2 documents · CONFORMANT — 0 schema-invalid, 0 undescribed

19 published documents, unchanged verdict. The derived cap/array pair set is also unchanged — 5 pairs for the MetaLog schema, the bare cell_budget seed for the diff schema — so no clause-4 behaviour moves.

For the editor

  • The 0.9.0 heading's — unreleased date is yours, as before. adr/0003's other two 0.9.0 acts — describing run_outcome (§2.x) and reservoir_delta (§13.x) — are not in this PR and append to the same CHANGELOG section.
  • conformance/README.md was still describing twelve fixtures and four controls, and declared-cap-violation never got its control-table row. Corrected here, because this PR changes those numbers again. The dated 12/12 mutation measurement from 2026-08-19 is left verbatim — it is history, and it was true when taken.
  • The README.md implementation row said the diff-root container "stays legal-but-undescribed until 0.9.0 grants that placement". That sentence goes false on merge, so it moves in the same pass — and it now distinguishes main from the last Release, per GOVERNANCE.md §7.
  • Nothing in adr/0003 is edited. Its decision 4 is executed, not amended.

coderoast-dev and others added 4 commits August 24, 2026 14:57
§7 grants `extensions` at the MetaLog document root and at `stats.top_k[]`, and
forbids a bare vendor member at any depth. `MetaLogDiff` carried no `extensions`
member at all, so vendor data on a diff had NO legal home — the bare form is
forbidden and the only placements the table named were on the other document
type. The table gains the diff root at v0.9.0.

Two consequences of the gap were already live in the text. §13.2 tells producers
they MAY report a truncation cap in `extensions.org.metalog.deltas_truncated_at`
AT THIS ROOT — a MAY naming a placement the spec had not granted — and any
producer with a per-diff vendor datum had to choose between a forbidden member
and dropping the datum. Both close here.

The grammar is DUPLICATED into metalog_diff.v0.schema.json's `$defs` rather than
`$ref`-ed across files. Both schema files are independently consumable — §8
invites downloading one alone — and a cross-file reference cannot be resolved
offline.

§7's claim about enforcement is corrected in the same pass, because the new row
sits exactly where it was false: the paragraph said a bare member "is a
conformance failure §8 clause 1 detects", which holds inside a CLOSED object and
is false at either document root. Both roots are `additionalProperties: true`, so
a bare member there validates and the conformance tool reports it as
legal-but-undescribed — a report, not a verdict. §7 now separates the two cases
and names closing a root as the breaking change it would be. The MUST is
unchanged; only the spec's claim about what enforces it changes.

Additive under GOVERNANCE §2: a new optional field and a new placement row. No
conformant 0.8.0 document becomes invalid — an `extensions` object at the diff
root was itself a bare undescribed member under §7 before this grant, so no
document that respected §7 carried one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A grant that no fixture exercises is a claim the self-test cannot check. This one
proves the v0.9.0 diff-root `extensions` placement in both directions inside a
single document: `topology_delta` written bare INSIDE the container is rejected,
because describing the container is what puts its reverse-DNS grammar in force at
this root; `com.example.topology_delta` beside it must validate, so the report has
to name the offender rather than reject the object; and `vendor_private_counter`
sits bare at the diff ROOT to hold the limit the grant does NOT close — that root
is open, so the member is legal-but-undescribed and changes no exit code.

Mutation-measured, both ways a wrong grant could look right. Remove
`properties.extensions` from the diff schema and exactly 1 of 14 fixtures reds,
in all three directions the manifest predicts: the finding disappears, the exit
drops to 0, and `extensions` itself joins the undescribed list. Grant the property
but wire it to a free-form object instead of `$defs/extensions` and the same
fixture reds on the finding alone — which is the mistake worth catching, because
that schema still "grants extensions" to a reader skimming it.

Its teeth cannot fall out the way `component` made another fixture's fall out in
0.8.0: no future standardisation can make an unprefixed key legal inside
`extensions`, whatever the standard mints elsewhere.

conformance/README.md catches up with the set it describes — twelve fixtures and
four controls were the numbers before 0.9.0 added a thirteenth and a fifth, and
`declared-cap-violation` never got its control-table row. The count of finding
species in the "why it never reports one number" heading was also still two. And
the limits section gains the one this release makes relevant: a bare vendor member
at either document root is reported, never failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`$defs/cube_axis` exists twice — once in each schema file, because the two files
are independently consumable and a cross-file `$ref` cannot be resolved offline.
v0.8.0 added `band_floor` to the copy in metalog.v0.schema.json and not to the
copy in metalog_diff.v0.schema.json, and the copies are closed objects, so the
two files disagreed about what an axis IS.

That disagreement is live, not cosmetic. §13.6 requires `cube_diff.axes` to EQUAL
both inputs' `cube.axes`, and §16.10 stamps a collapsed cube's axes with
`band_floor` — so a diff of two collapsed cubes was rejected by the diff schema
while both of its inputs validated. Measured against the shipped v0.8.0 pair: the
axis {"name":"level","kind":"categorical","band_floor":2} is accepted by
metalog.v0.schema.json and rejected by metalog_diff.v0.schema.json on
`additionalProperties`.

Additive and inert: the mirror only begins admitting what its source already
admits, no document becomes invalid, and no published document hit it — no
published diff carries a `cube_diff`. It is a schema lag in the sense
GOVERNANCE §3 gives the term: the schema was wrong, not the producers.

The next commit is what found this, and what stops the next one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two schema files, one grammar, no cross-file $ref: each file is independently
consumable (§8 invites downloading one alone) and this validator resolves only
in-document pointers, so a shared grammar is DUPLICATED. That trade buys offline
resolvability and sells drift — and the bill already came due once, silently, in
the release before this one.

So --selftest now compares every `$defs` name present in more than one shipped
schema, before any fixture runs. The copies must agree in every keyword but
`description`, which is the one exclusion and it is deliberate: a copy has to be
able to say that it IS one, and naming its source is what keeps the duplication
maintainable. The set is derived from the artifacts — a mirror added tomorrow is
checked on arrival, and one deleted stops being checked with no list to prune.

A drift is exit 2, not exit 1. It is not a defect in anyone's documents; it is the
standard's own artifacts disagreeing, and no verdict about a corpus is honest
underneath it.

Measured three ways. Against the shipped v0.8.0 pair it reds and names the member:
`$defs/cube_axis: properties only in metalog.v0.schema.json: ['band_floor']` — the
lag the previous commit repairs. Loosen the extensions mirror's reverse-DNS pattern
while leaving its property names identical and it reds on the grammar, so it is not
a property-name check wearing a grammar's name. And it passes today while all three
mirrors carry DIFFERENT descriptions, so it is not a byte comparison that would
forbid a copy from pointing at its source.

§7 and both of the diff schema's `extensions` descriptions now say what holds and
what does not: that the copies are asserted identical, and that at an open root the
schema does not tell a bare vendor member from a legal one. A reader deciding
whether to trust a duplicated grammar is owed the first; a reader deciding where to
put vendor data is owed the second, without a promise about a release nobody has
agreed to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderoast-dev
coderoast-dev merged commit 89b8a89 into main Aug 24, 2026
1 check passed
@coderoast-dev
coderoast-dev deleted the spec/diff-root-extensions-grant branch September 19, 2026 08:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant