Files changed: - CHANGES.md - INSTALL.md - VERSION - instructions/dev/stack-dev/SKILL.md - instructions/dev/version-parts.md - tools/CONTRACT.md - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/version_cmd.py - tools/chemenu/tests/test_docs_verify.py - tools/chemenu/tests/test_version_cmd.py - tools/chemenu/version.py
8.7 KiB
type, name, description
| type | name | description |
|---|---|---|
| types/instruction.md | version-parts | 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:
- 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 a0.xstack it is MINOR. A bump that changes it is called boundary-crossing below, because that is the termversion bumpanddocs verifyuse in their own messages. - 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, andversion bump --no-migrationexists 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
-
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,VERSIONand.wikitool-release.jsonover 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.
- Forward: an existing instance can install the new machinery by copying
-
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 atThe 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 brokeThe release artefact's name Every download script and every pin against it breaks The Python package's import name from <old> import ...outside the shipped tree breaksA 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 indexThe 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.
-
Otherwise pick the compatible part:
Change Part Fix, no interface change --patchNew capability, drop-in in both directions --minor -
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.
-
Record the break in the bump itself. A boundary-crossing bump requires
--breaking "<what breaks>", which writes a**Breaking Change:**line into the entry:tools/wikitool version bump --major \ --title "<what changed>" \ --breaking "<what stops working, and what an instance must do about it>" \ --no-migration "<why no page has to change>" # only if that is true--breakingis refused on a bump that crosses nothing, and required on one that does;docs verifychecks 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. -
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.bumpfinds it by itsmigrates_to:field. - Content need not change →
--no-migration "<reason>", 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. - Content must change → write the migration document under
-
Write the entry's body.
bumpleaves 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 exportnever 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,
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.