feat(spec): accept an optional root version, per ADR-004 - #12
Merged
Conversation
Widens the validator so a document may carry a root `version` mirroring `generated.specVersion`. Acceptance only — no producer emits the field, and ADR-004 section 8 sequences emission behind evidence that known validate-before-read consumers accept it. The root object is additionalProperties: false, so this is additive to the schema as a document and breaking for already-deployed validators. That is the whole reason acceptance ships first, and it is now measured rather than asserted: a mirrored document is accepted by this build and rejected by the published 0.4.4 validator. When the root key is present it must equal generated.specVersion. Disagreement is invalid rather than resolved by precedence, expressed as a pair of if/then subschemas keyed on each enum value so the invariant is mechanically checked. generated.specVersion is untouched: still required, still primary, still the value validateV4 reads. No new profile name is introduced. validateLegacy is corrected as a direct consequence. It identified the pre-v0.3 shape as "has a root version string and fails validate()", which stops being sufficient the moment v0.3/v0.4 documents may carry that key: a document whose two declarations disagree fails validate() and would then have been reported as legacy v0.1/v0.2. scripts/validate-examples.mjs accepts on strict || legacy, so that path would have let the examples gate pass a document section 4 requires rejecting. It now keys on the absence of generated.specVersion. Adds the six transition fixtures section 7 requires plus the perturbation pair, which is what makes the equality invariant a test rather than prose. versioning.md gains the validator acceptance floor as a surface distinct from the four stable read paths, and corrects a claim that this change disproves: "adding a new optional field is not breaking" holds inside manual, generated and health, which permit additional properties. It does not hold at the root.
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.
Summary
Implements the reader-preparation half of ADR-004:
validate()andvalidateV4()now accept an optional root
versionmirroringgenerated.specVersion.Acceptance only. No producer emits the field, and §8 sequences emission behind
evidence that known validate-before-read consumers accept it. Widening what a
reader accepts is deliberately not permission to start writing.
The claim, measured rather than asserted
The root object is
additionalProperties: false, so this change is additive tothe schema as a document and breaking for every already-deployed validator.
That is the entire reason acceptance must ship before emission, and it is now a
measurement. Running the real producer's output through both validators:
The last line is §8's argument as a fact: a mirrored document is rejected by the
currently published validator. Emitting today would break exactly the consumers
docs/conformance.mdtells to validate before reading.What changed
Equality invariant (§2). When the root key is present it must equal
generated.specVersion. Disagreement is invalid, never resolved by precedence.Expressed as a pair of
if/thensubschemas keyed on each enum value, so theinvariant is checked mechanically rather than described.
generated.specVersionuntouched (§5). Still required, still primary, stillwhat
validateV4reads. Itsrequiredconstraint is not relaxed, producers donot stop emitting it, it is not removed from
properties. No new profile name isintroduced (§6) — the document profile is still v0.4.
Fixtures (§7). All six required cases, plus the perturbation pair, which is
what turns the equality invariant into a test rather than prose.
validateLegacyhad to change, and it is the interesting part of this PRIt identified the pre-v0.3 shape as "has a root
versionstring and failsvalidate()". That stops being sufficient the moment v0.3/v0.4 documents maycarry that key.
A document whose two declarations disagree fails
validate()per §4 — and wouldthen have satisfied
validateLegacy, being reported as a legacy v0.1/v0.2document.
scripts/validate-examples.mjs:52accepts onstrict || legacy, sothe examples gate would have passed a document §4 requires rejecting.
That is precisely the failure §4 exists to prevent: silently converting a
detectable producer defect into an undetectable consumer misread. It now keys on
the absence of
generated.specVersion, so a disagreement is rejected by bothfunctions. Covered by three regression tests.
Documentation
docs/versioning.mdgains the validator acceptance floor as a surfacedistinct from the four stable read paths, as §6 requires.
It also corrects a claim this change disproves. The doc listed "adding a new
optional field" as categorically not breaking. That holds inside
manual,generatedandhealth— alladditionalProperties: true— and does nothold at the root. Left uncorrected, the document would have licensed exactly the
mistake ADR-004 exists to prevent.
Verification
pnpm -r typecheck— clean, both packagespnpm -r buildpnpm -r test— 221 passed (48 spec incl. 12 new, 173 rules)pnpm run check:architecturepnpm run check:architecture:test— 25 passedpnpm run check:schemapnpm run check:examples— 4/4pnpm run check:docspnpm run release:verify-packspnpm run check:conformanceagainst the real producer — 28/28 passedBoundary check
OWNERSHIP.md)@marcelle-labs/*, private Vreko source orworkspace.vreko.jsonreferencedeclare modulefor a standard-owned packageRelease impact
versionproperty plus theallOfequality invariant. Authorized by ADR-004, ratified and in force.bin,exportsandfilesunchangedpatchon the fixed groupNotes
No version hand-bump.
versioning.mdrecords that this repository holds nopublish credential; the changeset carries the bump and the release happens from
the authoritative path. Note that
src/index.tsexports a hand-maintainedversionstring that changesets does not rewrite — it needs updating in the sameact as the release, and that coupling is pre-existing rather than introduced here.
Producer side. No change required. The CLI already satisfies §5 and §8: it
emits
generated.specVersion: '0.4'and writes no rootversion, verified aboveagainst a real generated artifact. Its remaining obligation is the standing
receipt in its own CI, not a code change.
Not in scope. No emission, no
generated.specVersionlifecycle change, nostable-path relocation, no fragility representation change.