feat: changelog entries stay compact - one paragraph per topic, a size budget in docs verify and version release, version bump names required migration documents (#184)
CI / verify (push) Successful in 6m0s
CI / pwsh (push) Successful in 2m0s
Release / release (push) Successful in 33s

Files changed:
- CHANGES.md
- DEVELOPMENT.md
- VERSION
- instructions/dev/stack-build/SKILL.md
- instructions/dev/version-parts.md
- instructions/migrate-corpus.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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
torbenandClaude Opus 5.5 committed 2026-10-06 16:48:02 +02:00
1 parent 2ddcd6be19
commit 044943ae51
12 files changed
+718 -170

No files matched your search

+48 -29
View File
@@ -70,22 +70,37 @@ a new one, and only `version release` turns it into something the release workfl
reason on the marker line, bullets under a bare marker from the second onward - because a
long-running candidate can break compatibility more than once and each break is its own
thing to act on. The migration line does not: it answers one yes/no about the candidate as
a whole, and `--migration-required` is its retraction path. Nothing retracts a breaking
reason; a wrong one is rare enough, and the candidate is dev-local until release.
a whole, in one of two forms that never stand side by side. Where a required migration
document targets the candidate's base, every bump writes
`**Migration:** required - instructions/migrations/<file>.md` (several paths sorted and
comma-separated) - the pointer from the release notes to what an operator has to run;
otherwise `--no-migration` writes `**Migration:** none required - <reason>`, and
`--migration-required` is its retraction path. Nothing retracts a breaking reason; a wrong
one is rare enough, and the candidate is dev-local until release.
2. **The bump list**, grouped `**High/Medium/Low impact**` (empty groups omitted) - rendered by
`version bump`'s `--impact` (default `medium`), corrected after the fact by `version regrade`.
Flat and ungrouped, exactly as before this layering existed, when every bump is `medium` - the
common case, and the shape every pre-existing region still is.
common case, and the shape every pre-existing region still is. **This list is the bullet
level:** a smaller change gets nothing in the entry but its bump title.
3. **The release summary** - a short paragraph, written once, by hand, when the candidate is
ready to ship. `version release` refuses to close an entry with two or more bumps and no
summary here; a one-bump entry is exempt, since there the bump's own changeset already reads
as the summary.
4. **The changesets**, one `### <bump title>` heading per bump, in chronological order - the
detail a reader follows into from the graded list above. A changeset is a few sentences,
not the full rationale; what needs more than that belongs in the issue tracker, not here.
summary here; a one-bump entry is exempt, since there its own paragraph already reads as
the summary.
4. **One `###` section per topic, not per bump** - for the larger changes only. A topic is
what a reader would call one change; it often gathers several bumps, and its heading names
the topic rather than repeating a bump title. A later bump on the same topic rewrites that
section's paragraph instead of adding a second one. Whether a change earns a section or
stays a bullet is the build session's judgment, with no tie to `--impact`.
The list is the index into the changesets, which is why the bump list's title text and a
changeset's `###` heading are the same string.
**The budget, checked by `docs verify` and again by `version release` before it writes:** the
topmost versioned entry is at most 32 000 bytes (UTF-8) as `version notes` prints it, and every
`###` section is **exactly one paragraph** - no blank line in its body - of at most 1 000 bytes,
its `###` line excluded. A body runs to the next `### ` or `## ` line or the `---` separator;
blank lines at either end do not count. The total is there because an entry grows with every
bump: the 8.0.0 candidate collected 81 bumps, one changeset each, reached 129 KB and failed as
a release body, and no per-section rule alone would have stopped that. Older entries are not
held to it - their release notes are already published. What needs more than one paragraph -
the reasoning, the alternatives, the measurements - belongs in the issue, not in the entry.
- **`version bump` opens or continues a candidate; `version release` fixes one.** Only `release`
strips the suffix and turns the entry into a real, closed release - see its own row in
`tools/CONTRACT.md`. Nothing else does, and nothing auto-fixes a candidate on its own.
@@ -181,7 +196,8 @@ the three-line test below is usually enough.
- Content must change → write the migration document under `instructions/migrations/` per
[migrate-corpus.md](../migrate-corpus.md). The escalation bump finds it by the document's
`migrates_to:` field, matched against the candidate's **base** - a document targets the
release the candidate will become, never a `-beta.N` form of it.
release the candidate will become, never a `-beta.N` form of it - and names a required one
in the entry's migration line.
- 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
@@ -190,14 +206,15 @@ the three-line test below is usually enough.
**A `--no-migration` answer can turn out wrong later in the same candidate**, and that is not
a hand-edit: a bump escalates, a second change lands under the same running number, and now
content does have to move after all. Write the migration document first, then retract the line
with `version bump --migration-required` - it removes the `**Migration:** none required` line
the earlier bump wrote, and refuses unless a document already targets the new base. Nothing
else takes that statement back: the line is machine-written (invariant 1), `docs verify` is
satisfied by its bare presence, and the escalation checks in this step run only on the bump
that *first* crosses the boundary - so a candidate that keeps a stale `none required` line is
never reported by anything. The 5.0.0 candidate is the case: it declared `--no-migration` for
a TOC-verification change, then absorbed a schema removal that migrates 152 pages.
content does have to move after all. Write the migration document first, then run
`version bump --migration-required` - it replaces the `**Migration:** none required` line the
earlier bump wrote with the `required` line naming the document, and refuses unless a required
document already targets the new base. Any later bump would make the same replacement once
the document exists, and `docs verify` reports a crossing entry whose migration line does not
name it; the flag is the explicit form, which also refuses when there is nothing to replace.
`--no-migration` itself is refused while a required document targets the base. The 5.0.0
candidate is the case this guards: it declared `--no-migration` for a TOC-verification
change, then absorbed a schema removal that migrates 152 pages.
7. **Fix the candidate only when the user asks for a release.** Whether a candidate ships is the
user's call, never a session's: a work package being finished is not a reason, since the
@@ -215,16 +232,18 @@ the three-line test below is usually enough.
candidate collected several bump titles along the way; without one, the heading simply keeps
whichever bump last set it.
8. **Write the entry's prose - the summary, and each bump's own changeset.** `bump` leaves both
empty on purpose. The **summary** is a short paragraph (a few sentences) written once, at
release time, right below the graded bump list: what this release is about, and why, for a
reader who will not read the changesets underneath. `version release` refuses to close an
entry that collected two or more bumps and has no summary - a one-bump entry is exempt, since
there the bump's changeset already reads as one. Each **changeset**, under its own
`### <bump title>` heading, is a few sentences on what changed and why - it is the one thing a
future reader cannot reconstruct from the diff, but it is not the place for the full rationale
of a decision; that belongs in the issue tracker or the commit history, and a changeset that
is growing past a paragraph or two is a sign it belongs there instead.
8. **Write the entry's prose - a section for a larger change, nothing for a smaller one.** `bump`
writes no prose on purpose. Decide per bump: a smaller change is done once its title is in
the bump list. A larger one gets a `###` section for its topic - one paragraph, in the budget
of § The candidate model, on what changed and why, which is the one thing a future reader
cannot reconstruct from the diff. If its topic already has a section, rewrite that paragraph
to cover both bumps rather than adding another. The full rationale of a decision belongs in
the issue tracker or the commit history; a paragraph pressing against its 1 000 bytes is a
sign that part of it belongs there instead. The **summary** is a short paragraph (a few
sentences) written once, at release time, right below the graded bump list: what this
release is about, and why, for a reader who will not read the sections underneath.
`version release` refuses to close an entry that collected two or more bumps and has no
summary - a one-bump entry is exempt, since there its own paragraph already reads as one.
## Decision points