Skip to content

feat(spec): accept an optional root version, per ADR-004 - #12

Merged
qmarcelle merged 1 commit into
mainfrom
feat/meta-235-widen-root-version
Jul 28, 2026
Merged

feat(spec): accept an optional root version, per ADR-004#12
qmarcelle merged 1 commit into
mainfrom
feat/meta-235-widen-root-version

Conversation

@qmarcelle

Copy link
Copy Markdown
Contributor

Summary

Implements the reader-preparation half of ADR-004: validate() and validateV4()
now accept an optional root version mirroring generated.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 to
the 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:

producer                        @workspacejson/cli@0.5.2
published validator version     0.4.4
root `version` emitted?         no (correct — acceptance only)
generated.specVersion           0.4
accepted by published 0.4.4     true
accepted by widened candidate   true
validateV4 published / widened  true / true

disagreeing doc, widened        false
mirrored doc, widened           true
mirrored doc, published 0.4.4   false   ← why emission waits

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.md tells 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/then subschemas keyed on each enum value, so the
invariant is checked mechanically rather than described.

generated.specVersion untouched (§5). Still required, still primary, still
what validateV4 reads. Its required constraint is not relaxed, producers do
not stop emitting it, it is not removed from properties. No new profile name is
introduced (§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.

validateLegacy had to change, and it is the interesting part of this PR

It identified the pre-v0.3 shape as "has a root version string and fails
validate()"
. That stops being sufficient the moment v0.3/v0.4 documents may
carry that key.

A document whose two declarations disagree fails validate() per §4 — and would
then have satisfied validateLegacy, being reported as a legacy v0.1/v0.2
document. scripts/validate-examples.mjs:52 accepts on strict || legacy, so
the 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 both
functions. Covered by three regression tests.

Documentation

docs/versioning.md gains the validator acceptance floor as a surface
distinct 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,
generated and health — all additionalProperties: true — and does not
hold at the root. Left uncorrected, the document would have licensed exactly the
mistake ADR-004 exists to prevent.

Verification

  • pnpm -r typecheck — clean, both packages
  • pnpm -r build
  • pnpm -r test — 221 passed (48 spec incl. 12 new, 173 rules)
  • pnpm run check:architecture
  • pnpm run check:architecture:test — 25 passed
  • pnpm run check:schema
  • pnpm run check:examples — 4/4
  • pnpm run check:docs
  • pnpm run release:verify-packs
  • pnpm run check:conformance against the real producer — 28/28 passed

Boundary check

  • No producer, host-integration or site code added (see OWNERSHIP.md)
  • No @marcelle-labs/*, private Vreko source or workspace.vreko.json reference
  • No prescriptive policy field and no daemon assumption introduced
  • The four stable read paths are unchanged — conformance suite green
  • No new ambient declare module for a standard-owned package

Release impact

  • Schema bytes changed, intentionally — root version property plus the
    allOf equality invariant. Authorized by ADR-004, ratified and in force.
  • Package name, bin, exports and files unchanged
  • Changeset added — patch on the fixed group

Notes

No version hand-bump. versioning.md records that this repository holds no
publish credential; the changeset carries the bump and the release happens from
the authoritative path. Note that src/index.ts exports a hand-maintained
version string that changesets does not rewrite — it needs updating in the same
act 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 root version, verified above
against 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.specVersion lifecycle change, no
stable-path relocation, no fragility representation change.

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.
Copilot AI review requested due to automatic review settings July 28, 2026 17:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@qmarcelle
qmarcelle merged commit 05ea429 into main Jul 28, 2026
3 checks passed
@qmarcelle
qmarcelle deleted the feat/meta-235-widen-root-version branch July 28, 2026 17:40
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.

2 participants