Files
chemenu/docs/version-model.md
T
torben 4e80a07ac7
CI / verify (push) Successful in 48s
Release / release (push) Successful in 36s
docs/ befuellt - Stack-Hintergrund fuer vier Themen, Pflegeklausel in AGENTS.md ergaenzt (4.3.1, #45)
Files changed:
- AGENTS.md
- CHANGES.md
- VERSION
- docs/ownership-and-templates.md
- docs/pipeline-rationale.md
- docs/version-model.md
- docs/why-gates-are-code.md
2026-09-03 19:40:39 +02:00

5.4 KiB

Why the stack version splits compatibility from migration

A stack version number looks like it answers one question. It actually answers two, and the two are independent of each other.

Two questions, not one

The first question is whether the new version is a drop-in replacement for the old one - whether an existing instance can install it, and can also go back, without anyone doing hand-work. That is what a version number is: a promise. The second question is whether the existing corpus in kb/ needs to change shape to keep working under the new version. These sound like the same question, because most of the time a change that breaks compatibility also happens to touch content, and most of the time a change that leaves content untouched also happens to be compatible. The correlation is real; it just is not a law. instructions/dev/version-parts.md carries the actual test for telling them apart and the steps that follow from it - this page is about why the split exists at all.

Why "kb/ untouched" is not proof of anything

The tempting shortcut is: if no page in kb/ had to change, the bump can't be that serious. This is exactly backwards for a class of changes that live entirely outside the corpus - a renamed release artefact, a Python import path, an environment variable, the URL an instance's own updater points at. None of those touch a single page. All of them can strand an existing instance just as thoroughly as a rewritten type-spec would. The corpus is the part of the stack that looks at itself; the compatibility question is about everything an instance depends on to keep functioning, most of which the corpus never sees.

Reading compatibility off the leftmost non-zero component

Semantic versioning gives every component a job, but only one of them is where an existing instance's tooling actually looks to decide "is this safe." On a 2.x stack that is MAJOR; on a still-pre-1.0 0.x stack, by the same convention, it's MINOR - the leftmost slot that isn't pinned to zero is the one an automated updater treats as the compatibility boundary. Bump anything to its left, or bump that slot itself, and the promise changes. Everything to the right of it can move as freely as the project likes without touching that promise. This is why the question "is it boundary-crossing" always resolves to one specific digit, not to a feeling about how big the change is.

Downgrade is half the promise

It's natural to test compatibility by only asking "does the upgrade work." The other half - "can an instance that upgraded put the old version back and land where it started" - carries equal weight, and it's the half that's easy to forget because forward motion is what everyone is testing for anyway. A state file the old version can no longer parse, a generated index in a new shape, a stamp file that got renamed: none of these have to break the upgrade to break the downgrade. An instance that can go forward but not back has already lost the property a compatible version number is supposed to guarantee.

A promise made to a machine, not only to a person

A human reading a changelog can absorb "this technically isn't compatible but it's fine, just update those two things by hand." An instance's own update mechanism cannot. It reads a version number, decides whether to pull the new release, and has no channel for nuance - which is exactly why the update path itself is one of the sharpest ways to cross the boundary invisibly: if the new version moves where updates come from, the very channel that would have told an instance to adjust is the channel that just broke. The version number isn't documentation aimed at a reader; it's an input consumed by code that has no other way to ask.

The 2.0.0 story

This isn't hypothetical for this stack. The rebranding that produced Chemenu renamed the repo, the release artefact, and the Python package - and left every page in kb/ untouched. The first instinct was a MINOR bump, on the reasoning that nothing in the corpus needed migrating. That reasoning was correct on its own terms and answered the wrong question. Three things broke underneath it: every existing instance's update_url pointed at a repo path that no longer existed and, because it's a machine-written file, couldn't be hand-repaired; the release artefact name changed, breaking every download script and pin against it; and the import name changed, breaking anything importing the package from outside the shipped tree. The corpus had nothing to say about any of this, because none of it lived in the corpus.

What caught the mistake was a person looking at the diff and asking whether it really was a drop-in replacement, not a validator. No check in docs verify or anywhere else confirms that a version part was chosen correctly - it only confirms that a boundary-crossing bump documents what it breaks. The 2.0.0 entry in CHANGES.md carries the corrected reasoning in full, and the version bump that shipped it was --major --no-migration: boundary-crossing and untouched corpus, at the same time, which is precisely the combination the two-question split exists to make visible.

Where the procedure lives

The drop-in test, the catalogue of changes that cross the boundary with no page touched, and the steps for a boundary-crossing bump - the --breaking line, the migration document or --no-migration reason, talking to the user before bumping - are one procedure, kept at one place: instructions/dev/version-parts.md.