This is the stability promise MetaObjects makes at 1.0 (Metamodel 1.0). It defines what SemVer covers, what it does not, and how versions work across the language ports. The governing decision is ADR-0035.
The durable, promised contract is the Metamodel spec version — e.g. Metamodel 1.0. Every language port advertises which spec version it implements, and the cross-port conformance corpora verify that claim byte-for-byte. Package versions are ecosystem-natural and independent:
| Package version at the 1.0 cut | Promise it carries | |
|---|---|---|
| npm / PyPI / NuGet | 1.0.0 |
implements Metamodel 1.0 |
| Maven Central (Java / Kotlin) | 8.0.0 |
implements Metamodel 1.0 |
The two package lines then move forward independently in their own registries; the
Metamodel spec version is the number that communicates cross-language parity and
carries the compatibility promise. (This is the OpenTelemetry / Protobuf-editions
model. The Java 7.x → 8.0 step is a forward major — package versions never move
backward, a hard rule on every registry.)
Package 1.0 does not freeze the metamodel. Those are two different promises to two different parts of your project, and each has its own number (ADR-0035 Amendment 2):
| Number | Promises | A break moves |
|---|---|---|
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) |
metamodelVersion ("1.0" at the cut) |
the METADATA contract — your model depends on it | the metamodel major (Metamodel 2.0) |
Reading it the other way round: a package major means your imports, CLI invocations or generated-code shape may need work. A metamodel major means your metadata may need work. A release can move one without the other, and most releases move neither.
- The metamodel vocabulary — the registered type / subtype / attribute set,
enforced by
registry-conformance. This is the durable spine. - The canonical authoring + interchange format — canonical JSON keyword/
@-attr rules, sigil-free YAML, theextends/@viagrammar, package::syntax. - The wire / normalization contract — the cross-port serialized form (currency minor units, pagination, the native-return-type contract, jsonb parsed-value).
- The CLI command surface —
init/gen/verifyand their documented flags, per port (meta,dotnet meta,mvn metaobjects:*,metaobjects). - The scaffold-and-own contract — the layout and the interfaces:
codegen/generators/, the local-import config shape,.metaobjects/, and theGeneratorinterface owned templates implement. Not which generators a fresh scaffold wires: codegen is opt-in and the scaffolded selection is empty by design (ADR-0034 Amendment 2), so changing whatmeta initstarts you with changes nothing for a project that already exists.
What this costs you, stated plainly. Post-1.0 the caret rule stops being a gate —
^1.0.0accepts1.1.0— so a metamodel change can reach you on a routine update without a package major to refuse it. Today the project's answer is that every adopter is reachable and gets told; a mechanical gate (declaring which Metamodel version your metadata targets, and having the loader check it) is deferred until that stops being true. Every release that movesmetamodelVersionsays so in the changelog — that is the signal to read.
- Generator internals and the reference templates themselves. Generated code is yours and disposable — its internals are not a public API. See own-your-codegen.
- Runtime-library helper internals. Idiomatic per port; best-effort, not promised.
- Anything explicitly marked experimental or reserved and not yet in the registry
(e.g. the reserved-but-unregistered declared-API vocabulary
api.*/operation.*/binding.*, and reserved index subtypesindex.fulltext/vector/spatial).
A library is declared design MetaObjects ships as metadata —
nodes an adopter opts into and then generates from. What its SHAPE promises depends on
the stability its manifest declares, and meta gen --list prints it:
stable— additive only within a MINOR. A field, an index, a requirement may be added; a node or field is not removed or renamed, and a physical name does not change, without a MAJOR.preview— exempt from that promise. The shape may change in a MINOR, including removals and renames. A library shipspreviewwhile its shape is still being learned from use, and is promoted on evidence: one external estate running it with the drift gate enforced (the same bar G3d set for the 1.0 cut).
Two things bound what that costs you. Copy is the expected mode — meta eject <lib>
hands you the metadata to own, and a later change to the library then reaches only the
adopters who chose to track it. And the layering means the core layer generates nothing
until you opt into db, so a shape change in a library you took for its design alone
cannot move your schema.
The library's own REQUIREMENTS carry the same reading rule as its model: live means
"the model as shipped realises this", never "your application does".
The trigger is new public surface, not code size:
- MINOR — adds surface a consumer can newly depend on: a new generated artifact,
a new CLI flag, or a newly-supported metamodel member. Additive; never breaking on
the software surface. A release that breaks the metadata contract also moves
metamodelVersion— the package coordinate is not where that fact lives. - PATCH — a bug fix or internal refactor with no new surface.
- Metamodel spec-version bump — only when the shared vocabulary or wire/canonical contract itself changes. Most releases are per-port package moves that do not touch the spec version.
Everything above answers "what happens when we change the contract." This section answers a different question: what happens when we find that the toolchain accepted something the contract never allowed, and the fix makes it stop loading?
That case is real and it recurs. The doctrine behind it — a correction of previously-wrong
acceptance is a bug fix, not a contract change — has been applied four times before 1.0.
Two of those changed only OUTPUT and are ordinary bug fixes needing no exception: the
0.19.1 @min clamp (an authored @min: 0 was silently discarded) and the 0.21.6 like
case-sensitivity fix. Two made previously-LOADING metadata stop loading, and those are
what this section governs: an index declaring both @fields and @expr, where one half was
silently thrown away (#342), and
@filterable on an array field, which emitted SQL that cannot execute
(#335). Refusing those two is not
a new rule. It is the documented rule finally being enforced.
Under ADR-0023 the registry is strict and sealed, so there is no deprecation shim: a refusal takes effect on the release that ships it. That makes it important to say exactly when this is allowed, because a category this shape can be abused to smuggle a real break past the promise.
A correction ships as a PATCH — not a Metamodel major — only when ALL THREE hold:
- It was never validly expressible. The form contradicted the documented contract or the vocabulary's own stated rules. Deciding we prefer a different design is not this; that is a break.
- It produced no correct outcome for anyone. The form silently discarded part of its own declaration, emitted output that cannot run, or behaved differently in different ports. If it did what its author reasonably expected in even one port, this exception does not apply.
- The repair is mechanical or exactly named.
meta upgrade --applycarries the estate forward, or the load error names the precise edit. If fixing it requires guessing what the author meant, it is a break.
Retiring vocabulary is never in this category, however good the reason. A retired
element worked; removing it is a Metamodel major after 1.0. The distinction is the
whole basis for this exception: retired-vocabulary.ts deliberately refuses to let a
retirement load again — "a 'helpful' shim is how a retirement quietly stops being one"
— and that reasoning is about undoing an adjudicated decision. Input that never had
a valid meaning is a different population, and admitting it here does not weaken that
doctrine.
Every such correction owes you three things: a meta upgrade --apply path where the
edit is mechanical, a load error naming the exact fix where it is not, and a CHANGELOG
entry that says previously-loading metadata stops loading. A correction that cannot
offer the first two is not eligible for this exception.
What the tooling does NOT check, stated plainly.
scripts/check-metamodel-version.mjscompares the registry manifest between releases. This class of correction usually lives in loader validation and leaves no manifest footprint —0.24.1is the worked example: its manifest diff was arequired: true → falserelaxation plus prose, which classifies as additive, in the very release where two previously-loading forms stopped loading. So the gate cannot see this class and does not pretend to. The check is a question asked at release time — does this release refuse anything it used to accept? — and it is answered by a person, in the CHANGELOG. Do not read a green version gate as a finding that nothing broke.
All ports that ship a given release implement the same Metamodel spec version,
verified by the shared conformance corpora (metamodel, render, persistence,
api-contract, registry). A port's package version tells you its own fix/feature level;
its declared metamodelVersion tells you the contract it honors. When they differ,
the spec version is authoritative for cross-language interop.
Until the 1.0 cut, the project is in 0.x (npm/PyPI/NuGet) / 7.x (Maven) and the
public API is not yet frozen — breaking changes may ship in a minor, as they did
through the 0.14–0.15 vocabulary-finalization window. The
1.0 readiness checklist tracks the remaining path; the
0.x → 1.0 migration guide consolidates the
breaking changes adopters absorb at the cut.