Skip to content

Commit 5887be1

Browse files
dmealingclaude
andcommitted
docs(versioning)!: two contracts, two numbers — a metamodel break stops 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
1 parent 2f27062 commit 5887be1

5 files changed

Lines changed: 273 additions & 12 deletions

File tree

docs/RELEASING.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ Publish in tier order so a dependent never lands before its dependency. **`forge
136136
version against **every** package in the set (it used to check only the cli), and
137137
`bun run prerelease` skips burned numbers when choosing an iteration.
138138

139-
## Versioning policy (pre-1.0)
139+
## Versioning policy
140140

141141
**The version number's only mechanical meaning today is npm's caret rule.** For
142142
`0.y.z`, `^0.19.3` resolves `>=0.19.3 <0.20.0`, so: **PATCH is auto-adopted by every
@@ -164,10 +164,46 @@ bytes**, not "did output change."
164164
| New **attribute** on an existing type/subtype | `@intValueMap`, `@lenient`, `@maxTokens` | **PATCH** | MINOR |
165165
| New **subtype** of an existing type | `field.uri`, `index.lookup`, `attr.intMap` | **PATCH** when inert (see the vocabulary rule below); **MINOR** when it changes existing metadata's meaning/output, narrows something previously permitted, or headlines a feature | MINOR |
166166
| New top-level metadata **type** | `requirement.*`, `index.*`, `api.*` | **MINOR** | MINOR |
167-
| Wire-contract / conformance behavior change of already-valid deployments | FR-036 enforcement | **MINOR**, loud notice (pre-1.0 MINOR *is* the breaking slot) | MAJOR |
167+
| **Breaking** metamodel-vocabulary change (retire an attr/subtype/type; narrow what is permitted) | FR-037 `@readOnly`, FR-038 `@verifiedBy`, ADR-0052 `@promptStyle` re-homing | **MINOR** (pre-1.0 MINOR *is* the breaking slot) | **`metamodelVersion` MAJOR**, package MINOR (ADR-0035 Am. 2) |
168+
| Wire-contract / conformance behavior change of already-valid deployments | FR-036 enforcement | **MINOR**, loud notice (pre-1.0 MINOR *is* the breaking slot) | **`metamodelVersion` MAJOR** (Metamodel 2.0) + package MINOR — see the two-contracts rule below |
168169
| Wire behavior fixed to match the documented/conformance contract | 0.19.1 `@min` clamp | PATCH | PATCH |
169170
| No changed product file in a port | PyPI/NuGet/Maven at 0.20.14 | **Version-parity bump at the shared patch number** — publish identical content at the new version; never skip a registry (single-shared-patch policy, standing since 0.20.13) | same |
170171

172+
### The two-contracts rule (post-1.0; ADR-0035 Amendment 2)
173+
174+
**Package 1.0 does not freeze the metamodel.** After the cut, the project versions two
175+
contracts on two numbers:
176+
177+
- **Package version** (npm/PyPI/NuGet `1.x`, Maven `8.x`) promises the SOFTWARE surface —
178+
exports, CLI flags, generated-code shape, runtime helpers. A break here is `2.0.0` /
179+
`9.0.0`.
180+
- **`metamodelVersion`** (`"0.9"` today, `"1.0"` at the cut; the first key of the
181+
byte-gated `expected-registry.json`) promises the METADATA contract — registered
182+
vocabulary, canonical/interchange format, wire contract. A break here moves ITS major,
183+
and **does not force a package major.**
184+
185+
That severance is the whole point: under the pre-amendment rule one vocabulary retirement
186+
dragged npm to `2.0.0` and Maven to `9.0.0`, so the package majors became a running count
187+
of metamodel edits. Measured cadence at the time of the amendment: **19 minor lines in 87
188+
days**.
189+
190+
**When you cut a release that moves `metamodelVersion`:**
191+
192+
1. Bump `METAMODEL_VERSION` in all five ports (it is byte-gated — `registry-conformance`
193+
fails until every port agrees) and regenerate `expected-registry.json`.
194+
2. The changelog entry MUST say the metamodel version moved, and to what. Post-1.0 the
195+
caret rule is no longer a gate (`^1.0.0` accepts `1.1.0`), so **the changelog is the
196+
adopter's only signal** until the deferred loader check exists.
197+
3. Ship a migration guide under `docs/features/migrations/`, as every breaking metamodel
198+
change already does.
199+
200+
Design + deferral triggers:
201+
[`docs/superpowers/specs/2026-08-20-two-contracts-versioning-design.md`](superpowers/specs/2026-08-20-two-contracts-versioning-design.md).
202+
203+
**Cadence is a separate lever, and it is free.** Nothing forces one release per merged
204+
change; batching a fortnight of work into one coordinated cut removes most of the number
205+
pressure without touching policy at all.
206+
171207
### The vocabulary rule (corrected 2026-08-17)
172208

173209
**Adding registry vocabulary does NOT, by itself, force a MINOR.** The rule used to read

docs/compatibility-policy.md

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,45 @@ carries the compatibility promise. (This is the OpenTelemetry / Protobuf-edition
2222
model. The Java `7.x → 8.0` step is a forward major — package versions never move
2323
backward, a hard rule on every registry.)
2424

25-
## What is covered (breaking change ⇒ next MAJOR)
25+
## Two contracts, two numbers
2626

27-
After 1.0, a breaking change to any of the following requires a **new major of the
28-
Metamodel spec version** (Metamodel 2.0) and a major bump of every affected package:
27+
**Package 1.0 does not freeze the metamodel.** Those are two different promises to two
28+
different parts of your project, and each has its own number
29+
([ADR-0035 Amendment 2](../spec/decisions/ADR-0035-one-zero-stability-commitment-and-version-unification.md)):
30+
31+
| Number | Promises | A break moves |
32+
|---|---|---|
33+
| **Package version** (npm/PyPI/NuGet `1.x`, Maven `8.x`) | the SOFTWARE surface — your **build** depends on it | the package major (`2.0.0` / `9.0.0`) |
34+
| **`metamodelVersion`** (`"1.0"` at the cut) | the METADATA contract — your **model** depends on it | the metamodel major (Metamodel 2.0) |
35+
36+
Reading it the other way round: a package major means *your imports, CLI invocations or
37+
generated-code shape may need work*. A metamodel major means *your metadata may need
38+
work*. A release can move one without the other, and most releases move neither.
39+
40+
### Covered by the METAMODEL version (breaking ⇒ Metamodel 2.0)
2941

3042
- **The metamodel vocabulary** — the registered type / subtype / attribute set,
3143
enforced by `registry-conformance`. This is the durable spine.
3244
- **The canonical authoring + interchange format** — canonical JSON keyword/`@`-attr
3345
rules, sigil-free YAML, the `extends` / `@via` grammar, package `::` syntax.
3446
- **The wire / normalization contract** — the cross-port serialized form (currency
3547
minor units, pagination, the native-return-type contract, jsonb parsed-value).
48+
49+
### Covered by the PACKAGE version (breaking ⇒ package MAJOR)
50+
3651
- **The CLI command surface**`init` / `gen` / `verify` and their *documented*
3752
flags, per port (`meta`, `dotnet meta`, `mvn metaobjects:*`, `metaobjects`).
3853
- **The scaffold-and-own contract** — what `meta init` scaffolds and the `Generator`
3954
interface owned templates implement.
4055

56+
> **What this costs you, stated plainly.** Post-1.0 the caret rule stops being a gate —
57+
> `^1.0.0` accepts `1.1.0` — so a metamodel change can reach you on a routine update
58+
> without a package major to refuse it. Today the project's answer is that every adopter
59+
> is reachable and gets told; a mechanical gate (declaring which Metamodel version your
60+
> metadata targets, and having the loader check it) is deferred until that stops being
61+
> true. **Every release that moves `metamodelVersion` says so in the changelog** — that
62+
> is the signal to read.
63+
4164
## What is NOT covered (may change in a MINOR)
4265

4366
- **Generator internals and the reference templates themselves.** Generated code is
@@ -53,7 +76,9 @@ Metamodel spec version** (Metamodel 2.0) and a major bump of every affected pack
5376
The trigger is **new public surface, not code size**:
5477

5578
- **MINOR** — adds surface a consumer can newly depend on: a new generated artifact,
56-
a new CLI flag, or a newly-supported metamodel member. Additive; never breaking.
79+
a new CLI flag, or a newly-supported metamodel member. Additive; never breaking **on
80+
the software surface**. A release that breaks the *metadata* contract also moves
81+
`metamodelVersion` — the package coordinate is not where that fact lives.
5782
- **PATCH** — a bug fix or internal refactor with no new surface.
5883
- **Metamodel spec-version bump** — only when the shared vocabulary or wire/canonical
5984
contract itself changes. Most releases are per-port package moves that do *not*
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Two contracts, two numbers — versioning after 1.0
2+
3+
**Status:** design, for ratification. Amends [ADR-0035](../../../spec/decisions/ADR-0035-one-zero-stability-commitment-and-version-unification.md) §1–§2 and `docs/compatibility-policy.md`.
4+
**Date:** 2026-08-20
5+
**Scope:** policy only. No loader work, no deprecation windows, no new vocabulary.
6+
7+
## The problem, measured
8+
9+
`v0.5.0` (2026-05-22) → `0.23.2` (2026-08-17) is **19 minor lines in 87 days** across
10+
90 tags — a new minor roughly every 4–5 days. Doug's summary of where that leads —
11+
*"we'll be on 100.100.0"* — is not hyperbole; it is the current rate extended.
12+
13+
The rate itself is not the defect. Most of those minors were correct calls under the
14+
rule in force at the time. The defect is **what the rule attaches the number to.**
15+
16+
Today, one number carries two unrelated promises:
17+
18+
1. **The software surface** — the imports, CLI flags, generated-code shape and runtime
19+
helpers an adopter's *build* depends on.
20+
2. **The metamodel** — the registered vocabulary, canonical format and wire contract an
21+
adopter's *metadata* depends on.
22+
23+
ADR-0035 §1 binds them: a breaking change to the metamodel "bumps the spec version's
24+
major (Metamodel 2.0) **and forces a major on every affected package**." So one
25+
vocabulary retirement — `@readOnly`, `@verifiedBy`, `@unique` — drags npm to `2.0.0` and
26+
Maven to `9.0.0`, and the package majors become a running count of metamodel edits
27+
rather than a statement about the software.
28+
29+
That is already shaping decisions. The ADR-0052 roadmap correction (2026-08-19) had to
30+
rule that a post-1.0 `1.1` MINOR **cannot** carry FR-037's or FR-038's vocabulary
31+
retirements, because ADR-0035 §1 makes each a 2.0 event. The rule is working exactly as
32+
written; what it produces is a project that reaches 1.0 and then has to spend a major on
33+
its next housekeeping edit.
34+
35+
## The decision
36+
37+
**Split the promises, and give each its own number.**
38+
39+
| Number | Promises | Moves when |
40+
|---|---|---|
41+
| **Package version** (npm/PyPI/NuGet `1.x`, Maven `8.x`) | the SOFTWARE surface: exports, CLI flags, generated-code shape, runtime helpers | that surface changes — full SemVer, `2.0.0` / `9.0.0` for a break |
42+
| **`metamodelVersion`** (`"0.9"` today, `"1.0"` at the cut) | the METADATA contract: registered vocabulary, canonical authoring + interchange format, wire/normalization contract | that contract changes — its own major for a break |
43+
44+
**A metamodel-vocabulary break moves `metamodelVersion`, and does NOT force a package
45+
major.** That clause of ADR-0035 §1 is severed.
46+
47+
`metamodelVersion` is not new. It has shipped in all five ports since PR #145 as the
48+
first key of the byte-gated `expected-registry.json`
49+
(`RegistryManifest.METAMODEL_VERSION` / `METAMODEL_VERSION` / `metamodelVersion`). ADR-0035
50+
§2 already decoupled it from package coordinates and made it the cross-language parity
51+
signal. This design does one further thing: it makes that number **load-bearing for
52+
compatibility**, not merely descriptive of it.
53+
54+
### What a package MINOR means, restated
55+
56+
`docs/compatibility-policy.md` currently says a MINOR is "Additive; never breaking." That
57+
becomes:
58+
59+
> **MINOR** — adds surface a consumer can newly depend on. Never breaking **on the
60+
> software surface**. A release that breaks the METADATA contract also moves
61+
> `metamodelVersion`, and says so in the changelog; the package coordinate is not where
62+
> that fact lives.
63+
64+
### Cadence is a separate lever, and it is free
65+
66+
Nothing forces one release per merged change. Batching a fortnight of work into one
67+
coordinated cut costs nothing and removes most of the number pressure on its own. The
68+
19-minors-in-87-days figure is as much a cadence artifact as a policy one, and cadence
69+
needs no ADR to change.
70+
71+
## What this costs, stated plainly
72+
73+
**It trades a mechanical gate for a social one.**
74+
75+
Pre-1.0, the caret rule is a real gate: `^0.22.x` resolves `<0.23.0`, so a consumer
76+
adopts a MINOR deliberately. That is why `0.21.0` and `0.23.x` could ship metamodel
77+
changes safely. **Post-1.0 that gate disappears**`^1.0.0` accepts `1.1.0` — and the
78+
package MAJOR becomes the only coordinate a resolver will refuse to cross. Severing the
79+
"metamodel break ⇒ package major" link therefore removes the only *mechanical* protection
80+
an adopter has against auto-adopting a metadata break on a routine update.
81+
82+
What replaces it, today, is that **every adopter is reachable**: six projects, all
83+
Doug's, all upgradeable by hand. That is a true statement about 2026 and a coherent basis
84+
for the trade. It is also a premise with an expiry date, so it is written down here rather
85+
than left implicit.
86+
87+
**Why no loader check today.** The obvious mechanical replacement — the loader refusing
88+
metadata declared for an incompatible metamodel version — is deferred deliberately, and it
89+
is more expensive than it looks: `metamodelVersion` is currently a property of the
90+
LIBRARY, and there is nowhere an adopter declares *"my metadata targets Metamodel 1.0."*
91+
Adding that declaration is new vocabulary in all five ports plus a compatibility matrix —
92+
real work, gating a hazard that six reachable adopters do not have.
93+
94+
### Deferred, with triggers
95+
96+
| Deferred | Adopt when |
97+
|---|---|
98+
| **A declared metadata target + loader/verify compatibility check** — the mechanical gate above | the adopter set stops being enumerable and reachable: a metamodel break ships to someone who cannot be told in advance |
99+
| **Per-item `experimental` / `stable` markers** (the Kubernetes model) | a member needs to ship for feedback without entering the frozen set — today the reserved-not-registered treatment (ADR-0007 Am. 2, ADR-0040) already covers this |
100+
| **Editions** (the Rust model) — a per-model opt-in that pins old semantics | two metamodel majors coexist in one estate and pinning per model beats upgrading per repo |
101+
102+
## Prior art
103+
104+
- **OpenTelemetry** — spec version (1.5x) is the coordinating contract; each language SDK
105+
versions independently against it, with a compliance matrix. Already the model ADR-0035
106+
§2 adopted; this design finishes the job by letting the two numbers move independently
107+
in *both* directions.
108+
- **Rust editions** — the language breaks; the compiler version does not. A per-crate
109+
`edition` key opts in. The precedent for "a breaking change to the *language* is not a
110+
breaking change to the *tool*."
111+
- **Kubernetes**`alpha`/`beta`/`stable` per API object with a published deprecation
112+
window per tier, rather than one project-wide major. Rejected for now as the wrong shape
113+
at six adopters; retained as a trigger above.
114+
- **TypeScript** — explicitly does not follow SemVer, and says so, because every release
115+
can break inference. The honest-labelling precedent: it is better to state the real
116+
contract than to encode a false one in the number.
117+
118+
## What changes in the repo
119+
120+
Policy only — no product code.
121+
122+
1. **ADR-0035 §1** — sever "and forces a major on every affected package"; state the split
123+
and its cost; add the deferral triggers as an amendment.
124+
2. **`docs/compatibility-policy.md`** — the two-number table, the restated MINOR, and an
125+
explicit sentence that **package 1.0 does not freeze the metamodel**.
126+
3. **`docs/RELEASING.md`** — the post-1.0 column of the versioning table stops reading
127+
MAJOR for metamodel changes; add the `metamodelVersion` row.
128+
4. **`spec/roadmap.md`** — the "what the next breaking MINOR carries" section (added
129+
2026-08-19) gets the post-1.0 rule beside it.
130+
131+
## Open question for ratification
132+
133+
**Does the next breaking batch go before or after the 1.0 cut?**
134+
135+
Before is cheaper: pre-1.0 the caret rule still gives adopters a real gate, so FR-037,
136+
FR-038, ADR-0052 and ADR-0053 can all land in one deliberate MINOR with the mechanism the
137+
project already trusts. After means the first exercise of this new policy is also the
138+
first time the mechanical gate is absent.
139+
140+
This design does not settle it — it is a schedule call, not a policy one.

0 commit comments

Comments
 (0)