feat: Versionsstelle als Kompatibilitaetsfrage, Breaking-Change-Vermerk erzwungen (2.5.0)
CI / verify (push) Successful in 54s
Release / release (push) Successful in 37s

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:
torben committed 2026-09-02 09:17:36 +02:00
1 parent 7fbb9a99df
commit 31662dc3ff
11 files changed
+431 -32

No files matched your search

+32 -8
View File
@@ -10,11 +10,23 @@ ingest into a release.
**Compatibility is read off the leftmost non-zero component**, the rule Cargo's
caret ranges use: `0.1.3 -> 0.1.4` is safe, `0.1.3 -> 0.2.0` is not, and from
`1.0.0` on the same rule reads as the familiar "MAJOR means migration". Stating
it that way is what lets the 0.x era carry the migration signal at all - under
plain "MAJOR breaks" semantics every 0.x release would be indistinguishable
from every other, which is exactly the signal update detection needs. Nothing
about the mechanism changes at 1.0.0.
`1.0.0` on the same rule reads as the familiar "MAJOR breaks". Stating it that
way is what lets the 0.x era carry the signal at all - under a rule keyed to
the MAJOR component alone, every 0.x release would be indistinguishable from
every other, which is exactly the signal update detection needs. Nothing about
the mechanism changes at 1.0.0.
What that component answers is **whether the new version is a drop-in
replacement**: whether an instance can copy the new machinery over itself with
no hand-work and still put the old version back afterwards. Whether *content*
must be migrated is a **second, independent question**. It is one way to fail
the first - but a renamed release feed, artefact, import name, flag or envvar
fails it too, with `kb/` untouched, which is why `--no-migration` exists at all:
boundary-crossing bumps that migrate nothing are a real case, not an escape
hatch. Hence two markers below rather than one - `BREAKING_CHANGE_MARKER`
records the break, `MIGRATION_NONE_MARKER` records the absence of the
migration. Which part a change earns stays a judgment call made before the
bump; this module only enforces that a crossing says what it costs.
Paths are resolved through `config.ROOT` at call time rather than at import,
because the tests (and `dist export`'s own fixtures) relocate the root.
@@ -64,6 +76,11 @@ _SEMVER_RE = re.compile(r"^\s*v?(\d+)\.(\d+)\.(\d+)\s*$")
# boundary that needs no content migration. `docs verify` accepts it in place
# of a migration document, so the exact string is a contract between the two.
MIGRATION_NONE_MARKER = "**Migration:** none required"
# Written into every CHANGES.md entry whose version crosses a compatibility
# boundary, migration or not: the swap is not drop-in, and the operator of an
# existing instance has to be told what stops working. `docs verify` checks the
# newest crossing carries it, so this string too is a contract between the two.
BREAKING_CHANGE_MARKER = "**Breaking Change:**"
# A changelog entry that names a version. Entries predating versioning start
# with a date instead and are deliberately not matched - they are history, not
# a claim about which version the tree is.
@@ -332,15 +349,22 @@ def insert_changes_entry(
title: str,
author: str,
no_migration_reason: Optional[str] = None,
breaking_reason: Optional[str] = None,
) -> str:
"""Add a heading for `version` above the newest existing entry.
Only the skeleton: heading, date, author, and - when a compatibility
boundary is crossed without a migration - the line that says so. The
entry's actual content is written afterwards by whoever made the change,
which is also why `bump` refuses to invent a title.
boundary is crossed - the line saying what breaks, plus the line saying no
content has to change where that applies. The entry's actual content is
written afterwards by whoever made the change, which is also why `bump`
refuses to invent a title.
The break comes first: it is what an operator reading the release notes has
to act on, and the migration line only qualifies it.
"""
lines = [f"## {version} - {date} - {title}", "", f"**Author:** {author}", ""]
if breaking_reason:
lines += [f"{BREAKING_CHANGE_MARKER} {breaking_reason}", ""]
if no_migration_reason:
lines += [f"{MIGRATION_NONE_MARKER} - {no_migration_reason}", ""]
entry = "\n".join(lines) + "\n---\n\n"