--- type: types/instruction.md name: version-parts description: Which part of the stack version a change bumps - the compatibility question (is the new version a drop-in replacement?) separated from the migration question (must existing content change?), plus what to do before a breaking bump. --- # Pick the version part for a stack change Two questions decide a version bump, and they are **not the same question**: 1. **Is the new version a drop-in replacement for the old one?** This is what the version number itself says. Compatibility is read off the **leftmost non-zero component** - on this stack (`2.x`) that is MAJOR, on a `0.x` stack it is MINOR. A bump that changes it is called *boundary-crossing* below, because that is the term `version bump` and `docs verify` use in their own messages. 2. **Must existing content be migrated?** This is a *consequence* a boundary crossing may or may not have. `kb/` staying untouched does not make a change compatible, and `version bump --no-migration` exists precisely because boundary-crossing bumps with an untouched corpus are a real case. Getting these backwards is how a genuinely breaking change ships as a MINOR. It happened once already (see the case study at the end), which is why this file exists. ## When to run Before every `tools/wikitool version bump` - the `stack-dev` skill's step 3 sends you here. Read it in full the first time a change looks like it might be boundary-crossing; afterwards the three-line test below is usually enough. ## Steps 1. **Apply the drop-in test.** The bump is boundary-crossing if **either** half fails: - **Forward:** an existing instance can install the new machinery by copying `tools/`, `types/`, `instructions/`, `AGENTS.md`, `VERSION` and `.wikitool-release.json` over itself, and everything that worked before still works - with **no** hand-work by the user and **no** migration script. Any step beyond the copy, however small, fails this half. - **Backward:** having installed the new version, the user can put the old one back and be where they started. A state file the old version cannot read, a rewritten corpus, a renamed stamp - anything that makes the downgrade fail or leave a broken instance - fails this half. Content migration is one way to fail the forward half, not the definition of it. 2. **Check the catalogue** when the answer still feels like a judgment call. Each of these crosses the boundary with `kb/` entirely untouched: | What changed | Why the swap is not drop-in | |---|---| | The update path - `update_url`, the release feed, the repo it points at | The instance cannot repair its own `.wikitool-release.json`: it is machine-written, and invariant 1 forbids the hand-edit. The channel that would have told it to update is the channel that broke | | The release artefact's name | Every download script and every pin against it breaks | | The Python package's import name | `from import ...` outside the shipped tree breaks | | A command, subcommand, or flag that was removed or renamed | Scripts, CI workflows and instruction files calling the old spelling break | | An environment variable's name | An instance configured through it silently loses the configuration | | The shape of a machine-read file - `.wikitool-kb.json`, `.wikitool-release.json`, a generated index | The old version cannot read what the new one wrote, so the downgrade half fails even if the upgrade half passed | | A type-spec's required fields | Existing pages stop validating - this one crosses *and* needs a content migration | The catalogue is illustrative, not exhaustive. When something is not on it, go back to step 1. 3. **Otherwise pick the compatible part:** | Change | Part | |---|---| | Fix, no interface change | `--patch` | | New capability, drop-in in both directions | `--minor` | 4. **Stop and talk to the user before a boundary-crossing bump.** It is expensive in a way the other two parts are not: every existing instance pays for it, once, by hand. Put in front of them, in this order: - **What breaks**, concretely - which file, which name, which call site. - **What each existing instance must do**, as the steps they would actually run. - **The alternatives**, so the break is a choice and not a side effect: - *Avoid it* - keep the old name as an alias, read both file shapes, accept both flag spellings. A compatibility shim carried for one release is usually cheaper than a migration everyone runs. - *Defer and batch it* - hold the break until the next boundary crossing, so instances pay once instead of twice. - *Split it* - ship the compatible half now, the breaking half later behind a deprecation window that the changelog announces in advance. - **Your recommendation**, with the trade-off named. Then wait for an explicit go-ahead. Do not bump across the boundary on your own initiative. 5. **Record the break in the bump itself.** A boundary-crossing bump requires `--breaking ""`, which writes a `**Breaking Change:**` line into the entry: ```bash tools/wikitool version bump --major \ --title "" \ --breaking "" \ --no-migration "" # only if that is true ``` `--breaking` is refused on a bump that crosses nothing, and required on one that does; `docs verify` checks the newest boundary-crossing entry still carries the line. Write it for the operator of an instance that has not read this repository: what stops working, and what they do about it. 6. **Then answer the migration question separately.** Boundary-crossing and content-migrating are independent: - Content must change → write the migration document under `instructions/migrations/` per [migrate-corpus.md](../migrate-corpus.md). `bump` finds it by its `migrates_to:` field. - Content need not change → `--no-migration ""`, which records that in the entry. Both are also needed by `docs verify`, for the same reason: an instance that learns it must migrate, with nothing telling it how, is a dead end. 7. **Write the entry's body.** `bump` leaves it empty on purpose. A boundary-crossing entry earns a paragraph that says *why this is breaking* - it is the one thing a future reader cannot reconstruct from the diff, and it is what the next session in this position will read instead of guessing. ## Decision points - **The change ships no code - only `README.md`, `INSTALL.md`, `EVALS.md`, or `.gitea/`?** No bump at all; CI's version gate is scoped to what changes behaviour. - **A break you can see coming but are not making yet?** File it as an issue and let it accumulate. Boundary crossings are cheaper in batches, and step 4's "defer" alternative is only real if someone wrote the break down. - **Unsure between MINOR and boundary-crossing?** It is boundary-crossing. The cost of an unnecessary MAJOR is one extra release note; the cost of a MINOR that actually breaks is an instance whose update path fails while its version number promised it would not. - **The break only affects this repository, not a distributed instance** - something under `instructions/dev/`, say? Then it is not a stack break at all: `dist export` never shipped it. Judge by what an *exported* instance sees. ## Scope Applies to the stack version in `VERSION` - `tools/`, `types/`, `instructions/`, `AGENTS.md` and the contracts. It says nothing about the content shape in `.wikitool-kb.json`, which is advanced by `wikitool migrate done` and described by [migrate-corpus.md](../migrate-corpus.md), and nothing about wiki content operations, which are logged in `kb/log.md` and carry no version at all. Choosing the part remains a judgment call, deliberately: `docs verify` checks that a boundary-crossing entry *documents* its break and its migration, never that the part was chosen correctly. No validator can tell a renamed flag from a new one. ## Case study: 2.0.0 The Chemenu rebranding renamed the repo, the release artefact and the Python package. No page in `kb/` changed, so the first attempt was `1.9.0` - the migration question, answered correctly, substituted for the compatibility question, which was never asked. Three things broke: every existing instance's `update_url` pointed at a repo path that no longer existed and could not be hand-repaired; the artefact name changed; the import name changed. The correct bump was `--major --no-migration`, and the `CHANGES.md` entry for `2.0.0` carries the reasoning in full under "Warum das trotzdem MAJOR ist". The error was caught by the user, not by the documentation - which is what step 4 is for.