A new case in ui's widget variant stops every ui mod built before it from
loading: the runtime links a component only when each imported
function's types match exactly ("expected variant of 5 cases, found
4"), so the published FPS Counter and Keystrokes would break. Instead,
the game lists a mod's keys first on its page in Options > Mods, as Key
Binds rows, and a mod with keys gets a page even without ui. No WIT
type changes. docs/api/stability.md said a new case the mod sends isn't
breaking; it is, and now says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.8 KiB
API stability and versioning
The rules every Horizon API follows (the APIs): how each one is versioned, what the stability levels promise, how something is deprecated and removed, and the tests that keep old mods, scripts and tools working.
Versions
- Each API has its own version, separate from the game's release (
v0.2.14, versioning.md) and from Minecraft's. A game release says which API versions it serves. - Semantic versioning,
MAJOR.MINOR.PATCH. A line is what stays compatible:0.1,0.2, … before 1.0, then1,2, ….- Before 1.0: an addition makes the next patch (
0.1.0→0.1.1); a breaking change starts the next line (0.2.0). - From 1.0: an addition makes the next minor (
1.0→1.1); a breaking change starts the next major (2.0).
- Before 1.0: an addition makes the next patch (
- Where the version lives:
| API | The version | A user names the line they need |
|---|---|---|
| Mods | the WIT package (horizon:mods@0.1.0) |
api = "0.1" in horizon-mod.toml |
| Agent control | the control protocol's version (planned: a hello reply and MCP serverInfo) |
horizon-ctl --api 1 (planned) |
| Server control API | the path (/api/v1/) |
the path |
server.toml |
the file's keys; a key is never renamed in place | nothing: old files keep loading |
| Launcher commands | the command names and their --json output |
nothing |
| Data formats | a version or schema field in the file |
the reader rejects versions newer than it knows |
What counts as a breaking change
Breaking: removing or renaming anything; changing a type, a default, a unit or a meaning; making something optional
required; refusing input that was accepted; any change to a WIT type a function uses, a new enum or variant case
included, in either direction (the runtime links a component only when each imported function's types match exactly:
a mod built before the change no longer loads); a JSON field that changes type.
Not breaking: a new function, interface, capability, command, endpoint, tool or server.toml key; a new optional
argument or JSON field (readers must ignore fields they don't know); a new WIT function or type (mods built earlier
don't import it); faster, or fixing behaviour to what the reference already says.
Stability levels
Every part of an API has one level, shown in its reference and its surface page.
| Experimental | Preview | Stable | |
|---|---|---|---|
| Who may use it | opt-in only (a flag or a manifest field naming it); never in a release default | anyone; marked "preview" where players see it (a mod's row on the Mods page) | anyone |
| May change | in any release, without notice | only at a new line, after a deprecation | only at a new major, after a deprecation |
| Deprecation window | none | at least 30 days, with one release line serving old and new | at least 180 days, with the old major served beside the new one |
| Needs | a design note | a generated reference, a guide section, an example, compatibility tests | the same, plus one line of real use without a break |
- Promotion. Experimental → preview when it has what the table asks. Preview → stable when real users (a published mod, a host's script, an agent's tool) used it for a whole line and nothing broke. The maintainers' orchestrator proposes the promotion with the evidence (the compatibility tests, the complete docs, that line of use) and Ludovic approves it.
- Horizon-only by design. An API is stable only for what Horizon itself does; a vanilla concept it exposes (a block
id, a command, a
server.propertieskey) changes when vanilla changes it, with a version port (version-ports.md), and the changelog says so.
Deprecation and removal
- The pull request that deprecates marks it where it is defined (a
/// Deprecated: use X; goes in 0.3.doc comment, WIT's@deprecated, adeprecatedfield in a tool's schema) and adds the replacement in the same release. - The reference shows it as deprecated, and the surface page's changelog says what replaces it and when it goes.
- Using it logs one warning per mod, session or client, naming the replacement; it keeps working.
- It is removed only after the window, and only at the next line (preview) or major (stable).
- A line or major that is no longer served is refused with a clear message (
needs API 0.1; this Horizon serves 0.3 and 0.4), never loaded half-way.
Compatibility tests
Every preview and stable API has tests that run old users against the current code, in CI:
| API | The test |
|---|---|
| Mods | a test mod per served line, built from source against that line's WIT and SDK, loaded by today's runtime with expected output (crates/horizon-mods/tests/); a line is served as long as its test mod passes |
| Agent control | a transcript per protocol version: commands and the reply keys each must have |
| Server | api::ROUTES against route() (generated reference); a server.toml from each release line parses to the same settings; golden replies per endpoint |
| Launcher | the generated reference; golden --json output per command |
| Data formats | a sample file of every version a reader accepts, read by today's reader |
The generated references are the API's own diff: a pull request that changes an API changes its reference, and the reviewer applies the rules above to that diff.
Changelog
Each surface page ends with a changelog: one line per change, newest first, with the API version and the pull request. Deprecations and breaking changes are in bold. The release notes of the game release that ships a change link it.
Today
| Part | Level | Version |
|---|---|---|
Mod API: log, game, chat, hud, storage, command, events |
preview | 0.1.0 |
Mod API: ui, ui-events (a mod's screens and settings page) |
experimental | manifest experimental = ["ui"] |
Mod API: render (HUD shapes, camera shake and zoom; drawn by the game, no mod shaders) |
experimental | manifest experimental = ["render"] |
Mod API: input (claimed items' use, the mod's keys, the mouse wheel) |
experimental | manifest experimental = ["input"] |
Content mods (registries: data-driven blocks and items) |
preview | manifest api = "0.2" |
| Server mods (events, world, server commands) | not started | manifest api = "0.3", experimental first |
| Script mods (Lua run by an interpreter mod, mods.md) | not started | |
cargo horizon-mod (new, build, run), horizon-mod-sdk |
preview | 0.1.0 |
| Shader API | not started | |
horizon-ctl commands and MCP tools |
preview | none yet (unversioned) |
The game's own control-socket commands outside horizon-ctl's table (test hooks) |
experimental | |
horizon-server commands, server.toml |
stable | horizon-server --version |
| Server control API | stable | /api/v1 |
horizon-launcher commands and MCP tools |
preview | none yet (unversioned) |
| Benchmark report | stable | "version": 1 |
| Telemetry batch and consent | stable | "schema": 1, consent 2 |
horizon-graphics.txt, horizon-extras.txt, horizon-mods.txt |
preview | none |
SHA256SUMS and SHA256SUMS.sig |
stable |
The experimental mod APIs' choices
- What a mod may take from the game.
input.claim-wheelis the one input a mod consumes: while it holds the wheel, the steps are itswheelevents and the hotbar doesn't move.claim-itemstakes the use button only on the items it names. A mod's keys (register-key) never consume: vanilla's mapping on the same key still runs. Whether keys may consume too is decided beforeinputbecomes preview. input'swheelevent is a new case in a variant the game sends, so it breaks mods built before it (allowed becauseinputis experimental): aninputmod that callspollneeds a rebuild against the new WIT.