Stability
From v31: at most one major every 90 days, the previous major gets security fixes for 6 months after its successor, and a removal is deprecated for at least one minor first. Each rule is refused by a script, not remembered by a person — the table at the bottom names the code.
Next major window: opens on 2027-02-08, 90 days after 31.0.0 (2026-11-10). The bump that ships a major rewrites this line (scripts/release.ts).
What is covered
A breaking change to any of these needs a major, and a major waits for its window.
| Surface | Where it is defined |
|---|---|
the eight primitive shapes: entity, policy, action, mutator, query, job, route, task | The eight primitives, PRIMITIVE_KINDS in packages/core/src/registrar.ts |
the x CLI: every command, every flag, and the --json envelope each prints | CLI reference |
every shipped X_* code: its name and its meaning. A code is never renamed or reused | Error codes |
| the package tier table | scripts/lib/tiers.ts, Project layout |
app.config.ts field names: renaming or removing one | Configuration |
every export a package's src/index.ts names and its README documents | each package's README.md |
| the Bun floor | engines in every package.json; it moves only in a scheduled major, or by the security exception |
What is not covered
| Not covered | Why |
|---|---|
an export no README documents, and any deep import (@ultimat3/x/src/...) | not a public surface; it can change in a minor |
.x/ — the build cache, generated manifests, snapshots | regenerated by the CLI; never hand-edited, never read by an app |
the exact wording of a cause: or fix: line | the code is the contract; the text improves in any release |
| a gate verdict getting stricter | a new x verify rule that reds an app waits on next like any break |
| performance figures and budgets | measurements, each dated in its committed JSON under scripts/bench/results/, never promises. bench.yml is meant to re-measure the first-hour and HTTP figures weekly and on each release, as a pull request; As of 2026-11-10 its one run measured and could not open that pull request, so the committed figures are older. Budgets: Routes and render modes |
Never "stable forever": a covered surface can change, in a scheduled major, with its entry in Upgrading.
| Bump | Means | Examples |
|---|---|---|
| major | a covered surface changed incompatibly | a removed config field, a renamed CLI flag, a changed primitive field, a narrowed tier |
| minor | additive: old code still compiles and still passes x verify | a new optional field, a new command, a new driver behind an existing interface, a new @deprecated alias |
| patch | no surface change | a bug fix, a perf change, a corrected fix: line |
Cadence
| Release | When | Branch | npm dist-tag |
|---|---|---|---|
| major | at most once per 90 days from v31, counted from the previous major's CHANGELOG.md heading date | next, merged into main when the window opens | latest |
| minor | weekly, when something shipped | main | latest |
| patch to the current major | as needed | main | latest |
| patch to the previous major | security fixes only, until 6 months after its successor shipped | release/<major>.x | maint-<major> |
| prerelease of the next major — planned, nothing ships one yet | release-tag.yml cuts only X.Y.Z release commits, so no workflow publishes from next | next | next (scripts/dist-tag.ts already names it) |
v31 is the stable line, dated 2026-11-10 by its CHANGELOG.md heading. It was the exception that starts the clock: it carried every break queued before the promise bound anyone (74, Upgrading), and scripts/release.ts does not refuse it (WINDOW_OPENS_AT_MAJOR in scripts/lib/major-window.ts). From it on, a BREAKING — entry waits on next and a removal ships its deprecation minor first.
Ruling 2026-11-09: the clock moved from v30 to v31. v30 shipped 2026-11-07 while only our own apps ran Ultimate in production, so the window protected no one yet; v31 carries the breaks of plan 101-final-hardening-before-v31, and the window protects adopters from there.
The support window
| Major | Status | Security fixes until |
|---|---|---|
the current major (npm view @ultimat3/core version) | every fix | its successor ships, then 6 months more |
| the previous major | security fixes, as patches on release/<major>.x | 6 months after the current major's ## X.0.0 date |
| anything older | none | Upgrading, one major per x upgrade run |
Convention, not enforced: release/<major>.x is cut by hand at each major and kept at that major's newest minor; no script cuts or checks it. Once it exists, a patch is latest while its major is current and maint-<major> once its successor ships. scripts/dist-tag.ts decides the tag from the registry's latest, and npm publish never runs without it.
Deprecation
A removal or a rename ships its alias, marked @deprecated, on main for at least one minor before a major takes it away. The rule compares each package's src/index.ts export names against the previous release tag: a name that is gone must have carried @deprecated there. Runtime deprecation logging is not part of this — it was deleted once, for good reason (Upgrading).
Where a break waits
| Branch | Holds |
|---|---|
main | minors and patches only while the window is closed; a BREAKING — changelog entry there is refused |
next | every breaking change waiting for the window. Convention, not enforced: it merges main weekly, by hand, so it never falls behind a fix; ci.yml gates every push to it |
release/<major>.x | the previous major's security patches |
A change with both kinds of edit splits into two pull requests: the non-breaking half to main, the break to next.
The security exception
A security fix that cannot be made non-breaking may ship a major inside the window. It is the only exception — "critical bug" is not one, or it would be used every week.
bun run scripts/release.ts --bump major --security GHSA-xxxx-xxxx-xxxx --dry-run --jsonThe advisory id (a GHSA-… or CVE-…) is recorded under the section heading in CHANGELOG.md as > **Security exception:** \`<id>\`, and the bump commit's body carries Security-Exception: <id>. The same recorded line lets a removal skip its deprecation minor.
Who decides
The maintainer bot judges whether a release is due and which kind (.dz/briefs/release-due.md), inside this window: it cannot cut a major the window refuses, because scripts/release.ts refuses it before any file moves, release.ts --check refuses it again before a tag is cut, and the gate refuses a dated section that broke it.
How each rule is enforced
| Rule | Refused by | Code |
|---|---|---|
| one major per 90 days | scripts/release.ts --bump, --check, and the gate's boundaries step | X_RELEASE_MAJOR_WINDOW |
no break on main while the window is closed | bun run changelog-check and the gate, on the pull request's base branch | X_BREAKING_ON_MAIN |
| a removal was deprecated first | bun run scripts/deprecation-window.ts and the gate | X_RELEASE_UNDEPRECATED_REMOVAL |
| every release from 30.0.0 opens with Highlights, the Release body | bun run changelog-check and the gate | X_CHANGELOG_HIGHLIGHTS_MISSING |
a maintenance publish never moves latest | scripts/dist-tag.ts in release.yml; scripts/registry-audit.ts audits latest and each maint-* tag | X_REGISTRY_LATEST_DRIFT |