docs(versioning)!: two contracts, two numbers — a metamodel break stops forcing a package major - #321
Merged
Merged
Conversation
…ps forcing a package major ADR-0035 §1 bound two unrelated promises to one number: the SOFTWARE surface (exports, CLI flags, generated-code shape) and the METADATA contract (registered vocabulary, canonical format, wire contract). Under that binding one vocabulary retirement drags npm to `2.0.0` and Maven to `9.0.0`, and the package majors become a running count of metamodel edits. Measured, so the consequence is not theoretical: `v0.5.0` → `0.23.2` is **19 minor lines in 87 days** across 90 tags — a minor every 4–5 days. Doug's summary of where that leads, "we'll be on 100.100.0", is the current rate extended. **Amendment 2 severs the clause.** A metamodel break moves `metamodelVersion` — which has shipped in all five ports since #145 as the first key of the byte-gated `expected-registry.json` — and the package rides a MINOR. The package version keeps the CLI surface and the scaffold-and-own contract; the metamodel version takes the vocabulary, the canonical/interchange format and the wire contract. A package MINOR is now "never breaking ON THE SOFTWARE SURFACE". This is the clause that first bit on 2026-08-19: the ADR-0052 roadmap correction had to rule that a post-1.0 `1.1` could not carry FR-037's or FR-038's retirements *because of it*. The roadmap now records what Amendment 2 changes there and — importantly — what it does NOT: the batch should still ride the pre-1.0 slot, because pre-1.0 `^0.x` is a real mechanical gate and post-1.0 `^1.0.0` accepts `1.1.0`. **The cost is written down rather than discovered later.** Post-1.0 the package MAJOR is the only coordinate a resolver refuses to cross, so severing this link removes the only mechanical protection against auto-adopting a metadata break. What replaces it today is that the adopter set is enumerable and reachable — a true statement about 2026, and a premise with an expiry date. The mechanical replacement (an adopter-declared metadata target plus a loader/verify check) is deferred with a written trigger, and costed: today `metamodelVersion` is a property of the LIBRARY, and there is nowhere an adopter declares which Metamodel version their metadata targets. Design doc records the prior art the decision rests on (OpenTelemetry spec-vs-SDK, Rust editions, Kubernetes per-item tiers, TypeScript's explicit non-semver stance) and defers per-item stability markers and editions with their own triggers. Policy only — no product code. Open for ratification: whether the breaking batch goes before or after the 1.0 cut. That is a schedule call, not a policy one. ci-local --quick green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
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.
What this is
Policy only — no product code. It amends ADR-0035 §1 so that a breaking change to the
METAMODEL moves
metamodelVersionand does not, by itself, force a package major.This is the task Doug scoped separately from PR #318; the brainstorming had reached the
approved-shape stage and this writes it up.
The problem, measured
v0.5.0(2026-05-22) →0.23.2(2026-08-17) is 19 minor lines in 87 days across 90tags — a minor every 4–5 days. Doug's summary of where that leads, "we'll be on
100.100.0", is the current rate extended.
The rate is not the defect. What the rule attaches the number to is. One number
currently carries two unrelated promises:
generated-code shape);
wire contract).
ADR-0035 §1 binds them — a metamodel break "bumps the spec version's major and forces a
major on every affected package" — so one vocabulary retirement drags npm to
2.0.0andMaven to
9.0.0, and the package majors become a running count of metamodel edits.That clause first bit on 2026-08-19: the ADR-0052 roadmap correction had to rule that a
post-1.0
1.1MINOR cannot carry FR-037's or FR-038's vocabulary retirementsbecause of it. The rule works exactly as written; what it produces is a project that
reaches 1.0 and then spends a major on its next housekeeping edit.
The decision
1.x, Maven8.x)2.0.0/9.0.0)metamodelVersion("0.9"today,"1.0"at the cut)metamodelVersionis not new — it has shipped in all five ports since #145 as the firstkey of the byte-gated
expected-registry.json, and ADR-0035 §2 already decoupled it frompackage coordinates. This does one further thing: it makes that number load-bearing for
compatibility rather than merely descriptive of it.
A package MINOR is restated as "never breaking on the software surface."
What it costs, stated so it is not discovered later
It trades a mechanical gate for a social one.
Pre-1.0 the caret rule is a real gate:
^0.22.xresolves<0.23.0. Post-1.0 that gatedisappears —
^1.0.0accepts1.1.0— so the package MAJOR is the only coordinate aresolver refuses to cross. Severing this link removes the only mechanical protection
against auto-adopting a metadata break on a routine update.
What replaces it today is that every adopter is enumerable and reachable (six
projects). That is a true statement about 2026 and a coherent basis for the trade. It is
written down because it is a premise that expires.
Why no loader check now, and what it would cost. The mechanical replacement is
deferred deliberately and is more expensive than it looks:
metamodelVersionis aproperty of the LIBRARY, and there is nowhere an adopter declares "my metadata targets
Metamodel 1.0." Adding that is new vocabulary in five ports plus a compatibility matrix —
real work, gating a hazard six reachable adopters do not have.
Deferred with written triggers: the declared target + loader check (adopt when the
adopter set stops being reachable), per-item
experimental/stablemarkers (Kubernetes),and editions (Rust).
Cadence is a separate lever and is free. Nothing forces one release per merged change;
batching removes most of the number pressure without touching policy at all.
What it does NOT change
The roadmap records this explicitly: Amendment 2 removes the "
1.1is unreachable"arithmetic, but the ADR-0052 / FR-037 / FR-038 batch should still ride the pre-1.0
slot. Landing it before the cut is landing it while the stronger mechanism still exists.
Files
docs/superpowers/specs/2026-08-20-two-contracts-versioning-design.md(design, prior art, deferral triggers)
points at it
docs/compatibility-policy.md— the two-number table, split covered-sets, therestated MINOR, and an explicit "package 1.0 does not freeze the metamodel"
docs/RELEASING.md— a two-contracts rule with the release checklist for ametamodelVersionbump; two table rows corrected; section renamed (it is no longerpre-1.0 only)
spec/roadmap.md— what the amendment changes for the next breaking MINOR, and whatit does not
Open for ratification
Does the breaking batch go before or after the 1.0 cut? Before is cheaper — the caret
rule still gives a real gate. This PR does not settle it; it is a schedule call, not a
policy one.
ci-local --quickgreen.🤖 Generated with Claude Code
https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15