feat: Versionsstelle als Kompatibilitaetsfrage, Breaking-Change-Vermerk erzwungen (2.5.0)
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
This commit is contained in:
@@ -48,6 +48,9 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
|
||||
[testing-conventions.md](../testing-conventions.md) - the suite runs against a deliberately
|
||||
empty machine; what the autouse fixture already neutralizes, and what a test still has to
|
||||
establish itself. Read it before adding or changing a test.
|
||||
[version-parts.md](../version-parts.md) - which part a change bumps: the drop-in test, the
|
||||
catalogue of breaks that cross the compatibility boundary with `kb/` untouched, and what to
|
||||
put in front of the user before a breaking bump. Read it before step 3.
|
||||
More instructions are added here incrementally as stack-development needs come up - this
|
||||
list grows without needing this skill file to change shape.
|
||||
3. **Raise the version, if the change ships.** A change under `tools/`, `types/`,
|
||||
@@ -59,18 +62,27 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
|
||||
```
|
||||
|
||||
Never edit `VERSION` or the entry's heading by hand - `bump` writes both, and `docs verify`
|
||||
fails a tree where they disagree. Pick the part by what an existing instance would have to do:
|
||||
fails a tree where they disagree. Pick the part by whether the new version is a **drop-in
|
||||
replacement** for the old one - not by whether content has to be migrated:
|
||||
|
||||
| Change | Part |
|
||||
|--------|------|
|
||||
| Fix, no interface change | `--patch` |
|
||||
| New capability, backwards compatible | `--minor` |
|
||||
| **Existing content must be migrated** | `--major` |
|
||||
| New capability, still drop-in in both directions | `--minor` |
|
||||
| **Not a drop-in replacement** - any hand-work by the user or a migration script, or a downgrade that no longer works | `--major` |
|
||||
|
||||
A `--major` bump additionally needs a migration document for the new version - written per
|
||||
[migrate-corpus.md](../../migrate-corpus.md) - or `--no-migration "<reason>"` when no content
|
||||
actually has to change. `bump` refuses otherwise, and so does `docs verify`: an instance
|
||||
learning that it must migrate, with nothing telling it how, is a dead end.
|
||||
Content migration is one way to land in the last row, not the definition of it: a rename of
|
||||
the update path, the artefact, an import name, a flag or an envvar breaks a swap with `kb/`
|
||||
entirely untouched. The full test, the catalogue of such breaks, and what to put in front of
|
||||
the user first are in [version-parts.md](../version-parts.md) - **read it before choosing
|
||||
`--major`.**
|
||||
|
||||
A `--major` bump therefore needs two things recorded. `--breaking "<what stops working>"`
|
||||
is required on every boundary-crossing bump; on top of it, a migration document for the new
|
||||
version - written per [migrate-corpus.md](../../migrate-corpus.md) - or
|
||||
`--no-migration "<reason>"` when no content actually has to change. `bump` refuses without
|
||||
either, and so does `docs verify`: an instance learning that it must migrate, with nothing
|
||||
telling it how, is a dead end.
|
||||
|
||||
Then write the entry's body - `bump` deliberately leaves it empty, the same way `new` leaves
|
||||
the prose.
|
||||
@@ -90,6 +102,12 @@ stack development happens in the origin repo instead (see AGENTS.md's routing li
|
||||
- **Touches both stack code and wiki content in one session?** Apply this skill's rules to the
|
||||
code changes and the normal content skills' rules to the content changes - they are not
|
||||
mutually exclusive within a session, only per change.
|
||||
- **The change turns out not to be a drop-in replacement?** Do not bump across the boundary on
|
||||
your own initiative. Every existing instance pays for a breaking change once, by hand, so the
|
||||
user decides whether it is worth that: show them what breaks, what an instance has to do about
|
||||
it, and the alternatives (avoid the break with a shim, defer and batch it with the next one,
|
||||
or split it behind a deprecation window), then recommend one and wait for a go-ahead.
|
||||
[version-parts.md](../version-parts.md) step 4 has the full shape.
|
||||
|
||||
## Scope
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
---
|
||||
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 <old> 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 "<what breaks>"`, which writes a `**Breaking Change:**` line into the entry:
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
`--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 "<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.
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user