spec(0.9.0): the extension container is granted at the MetaLogDiff root - #6
Merged
Merged
Conversation
§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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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] — unreleasedsection rather than opening a release:v0.8.0is still the last tag.Executes
adr/0003decision 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
extensionsthe only carrier of non-standard members and forbids the bare form at any depth. Its placement table named the MetaLog document root andstats.top_k[].MetaLogDiffhad noextensionsmember 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:
extensions.org.metalog.deltas_truncated_at— at the diff root. Following that sentence meant writing a container §7's table did not name.What this changes
§7's table gains a third row — the
MetaLogDiffdocument root, atv0.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.jsongains theextensionsproperty, referencing#/$defs/extensions. The grammar is duplicated into that file's$defsrather 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_atMAY 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:
conformance/README.mdcarries 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.extensionsobject 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:topology_deltawritten bare insideextensionsis rejectedcom.example.topology_deltabeside it validates, and the report names only the offendervendor_private_counterbare at the root is reported legal-but-undescribed and changes no exit codeMutation-measured, on both ways a wrong grant could look right. Remove
properties.extensionsfrom 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, andextensionsitself 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
componentmade another fixture's fall out in 0.8.0: no future standardisation can make an unprefixed key legal insideextensions, 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.
--selftestnow compares every$defsname present in more than one shipped schema (commit 4). The copies must agree in every keyword butdescription, 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:
That is a live defect, and commit 3 repairs it.
band_floorwas added to$defs/cube_axisinmetalog.v0.schema.jsonin 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 requirescube_diff.axesto equal both inputs'cube.axeswhile §16.10 stamps a collapsed axis withband_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 bymetalog.v0.schema.json, rejected bymetalog_diff.v0.schema.jsononadditionalProperties.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
extensionsobject 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
extensionscontainer 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'sorg.metalog.example matches the reverse-DNS pattern and stays valid.The
band_floorrepair only widens what validates. Nothing else touches a shipped keyword.Gates
Run on this branch,
jsonschema 4.26.0, Draft 2020-12:--selftest$defsagree:cube_axis,cube_coord,extensionsschema/metalog.v0.example.json--pointer /raw --expect-documents 2)19 published documents, unchanged verdict. The derived cap/array pair set is also unchanged — 5 pairs for the MetaLog schema, the bare
cell_budgetseed for the diff schema — so no clause-4 behaviour moves.For the editor
0.9.0heading's— unreleaseddate is yours, as before.adr/0003's other two 0.9.0 acts — describingrun_outcome(§2.x) andreservoir_delta(§13.x) — are not in this PR and append to the same CHANGELOG section.conformance/README.mdwas still describing twelve fixtures and four controls, anddeclared-cap-violationnever got its control-table row. Corrected here, because this PR changes those numbers again. The dated12/12mutation measurement from 2026-08-19 is left verbatim — it is history, and it was true when taken.README.mdimplementation 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 distinguishesmainfrom the last Release, perGOVERNANCE.md§7.adr/0003is edited. Its decision 4 is executed, not amended.