stack: Changelog-Eintrag geschichtet - Impact-Gruppierung, version regrade, Zusammenfassungspflicht (schliesst #95)
Files changed: - CHANGES.md - DEVELOPMENT.md - VERSION - instructions/dev/stack-dev/SKILL.md - instructions/dev/version-parts.md - tools/CONTRACT.md - tools/README.md - tools/chemenu/commands/run_budget.py - tools/chemenu/commands/version_cmd.py - tools/chemenu/tests/test_run_budget.py - tools/chemenu/tests/test_version_cmd.py - tools/chemenu/version.py
This commit is contained in:
1 parent
9a2d7d34f5
commit
1b0158fc8d
12 files changed
+690
-106
No files matched your search
@@ -125,7 +125,15 @@ def is_exempt(command: str, args: list[str]) -> bool:
|
||||
if command in SKIP_COMMANDS:
|
||||
return True
|
||||
subcommand = args[0] if args and not args[0].startswith("-") else ""
|
||||
return (command, subcommand) in SKIP_COMMAND_PATHS
|
||||
if (command, subcommand) in SKIP_COMMAND_PATHS:
|
||||
return True
|
||||
# `version regrade` only reads when called with no further arguments at
|
||||
# all - the bare listing. Any index (with `--impact`) writes CHANGES.md
|
||||
# and stays counted like `version bump`, so this cannot join
|
||||
# SKIP_COMMAND_PATHS, which only ever looks at the subcommand slot.
|
||||
if command == "version" and subcommand == "regrade":
|
||||
return len(args) == 1
|
||||
return False
|
||||
|
||||
|
||||
def _session_id() -> str:
|
||||
|
||||
@@ -11,7 +11,14 @@ number means, and `instructions/dev/version-parts.md` for the candidate model):
|
||||
holds the two together.
|
||||
- `version release` fixes that candidate: strips its `-beta.N` suffix and
|
||||
closes its changelog entry. It is the only thing that turns a candidate into
|
||||
a number a release actually consumes.
|
||||
a number a release actually consumes. Refuses if the candidate collected
|
||||
more than one bump and its entry still carries no summary above the
|
||||
changesets - see `version_mod.summary_prose`.
|
||||
- `version regrade` lists or changes the impact grade (high/medium/low) of
|
||||
the running candidate's bump titles, addressed by their position in the
|
||||
rendered list - the correction path for the judgment `version bump
|
||||
--impact` made at the time, per Gitea #95's fix for an unreadably long,
|
||||
ungraded bump list.
|
||||
- `version check` is the one command in `wikitool` that makes a network call.
|
||||
It is deliberately its own command: nothing else reaches for it implicitly,
|
||||
it needs no key, it times out, and a feed that cannot be reached is reported
|
||||
@@ -20,6 +27,7 @@ number means, and `instructions/dev/version-parts.md` for the candidate model):
|
||||
from __future__ import annotations
|
||||
|
||||
import json as _json
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
import typer
|
||||
@@ -210,6 +218,13 @@ def bump_command(
|
||||
"Requires a migration document already targeting the new base, and refuses when the entry "
|
||||
"carries no --no-migration line to retract.",
|
||||
),
|
||||
impact: Optional[str] = typer.Option(
|
||||
None,
|
||||
"--impact",
|
||||
help="high|medium|low - how much this bump matters to a reader of the release notes "
|
||||
"(default: medium). Grouped into the entry's bump list; `version regrade` corrects it "
|
||||
"later if the running candidate's own judgment changes.",
|
||||
),
|
||||
dry_run: bool = typer.Option(False, "--dry-run", help="Report the change without writing"),
|
||||
):
|
||||
"""Raise or continue the running candidate, and open or update its
|
||||
@@ -242,6 +257,10 @@ def bump_command(
|
||||
if not title.strip():
|
||||
fail("--title must not be empty - it becomes the changelog entry's heading")
|
||||
return
|
||||
if impact is not None and impact not in version_mod.IMPACT_LEVELS:
|
||||
fail(f"--impact must be one of {', '.join(version_mod.IMPACT_LEVELS)}, not {impact!r}")
|
||||
return
|
||||
chosen_impact = impact or version_mod.DEFAULT_IMPACT
|
||||
|
||||
try:
|
||||
current = version_mod.read_version()
|
||||
@@ -351,13 +370,15 @@ def bump_command(
|
||||
no_migration_reason=no_migration.strip() if no_migration else None,
|
||||
breaking_reason=breaking.strip() if breaking else None,
|
||||
migration_required=migration_required,
|
||||
impact=chosen_impact,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
impact_note = "" if impact is not None else f" (impact not given - assumed {chosen_impact})"
|
||||
success(
|
||||
f"{current} -> {new_version}{boundary}. Wrote {version_mod.VERSION_FILENAME} and "
|
||||
f"the {version_mod.CHANGES_FILENAME} entry - write its prose before publishing, and "
|
||||
f"`version release` once the candidate is ready to ship."
|
||||
f"the {version_mod.CHANGES_FILENAME} entry{impact_note} - write its prose before "
|
||||
f"publishing, and `version release` once the candidate is ready to ship."
|
||||
)
|
||||
|
||||
|
||||
@@ -382,7 +403,12 @@ def release_command(
|
||||
Commits nothing and pushes nothing (AGENTS.md invariant 5) - the following
|
||||
`publish` moves `VERSION` onto `main` and is what `release.yml` reacts to.
|
||||
Refuses when `VERSION` is already a release: there is no running candidate
|
||||
to fix."""
|
||||
to fix. Also refuses - Gitea #95 - when the candidate collected two or
|
||||
more bumps and its entry still has no summary paragraph above the
|
||||
individual changesets: a release note that is only a chronological bump
|
||||
list is exactly the thing this refusal exists to stop shipping. A
|
||||
candidate with exactly one bump is exempt - there, the bump's own
|
||||
changeset already is the summary."""
|
||||
try:
|
||||
current = version_mod.read_version()
|
||||
except VersionError as exc:
|
||||
@@ -412,6 +438,18 @@ def release_command(
|
||||
)
|
||||
return
|
||||
|
||||
section = version_mod.changes_section(text, current) or ""
|
||||
bump_count = len(version_mod.bump_entries(section))
|
||||
summary_chars = len(re.sub(r"\s+", "", version_mod.summary_prose(section)))
|
||||
if bump_count >= 2 and summary_chars < version_mod.SUMMARY_MIN_CHARS:
|
||||
fail(
|
||||
f"This candidate collected {bump_count} bumps, but its {version_mod.CHANGES_FILENAME} "
|
||||
"entry carries no summary above the individual changesets - write a short paragraph "
|
||||
"(a few sentences on what this release is about) right below the bump list before "
|
||||
"releasing. `version regrade` (no arguments) shows the bump list first, if that helps."
|
||||
)
|
||||
return
|
||||
|
||||
new_version = current.base
|
||||
|
||||
if dry_run:
|
||||
@@ -428,3 +466,77 @@ def release_command(
|
||||
f"{version_mod.CHANGES_FILENAME} entry - `publish` next, which moves VERSION onto main and "
|
||||
"is what release.yml reacts to."
|
||||
)
|
||||
|
||||
|
||||
@app.command("regrade")
|
||||
def regrade_command(
|
||||
indices: Optional[list[int]] = typer.Argument(
|
||||
None,
|
||||
help="1-based positions in the rendered bump list to regrade (see the bare listing). "
|
||||
"Omit to just list.",
|
||||
),
|
||||
impact: Optional[str] = typer.Option(
|
||||
None, "--impact", help="high|medium|low - required together with indices"
|
||||
),
|
||||
):
|
||||
"""List the running candidate's bump titles with their impact grade, or
|
||||
change one or more of them in a single call.
|
||||
|
||||
Positions are `version_mod.bump_entries`'s own rendered order - grouped
|
||||
High before Medium before Low, chronological within a grade - as it
|
||||
stands *before* this call: `wikitool version regrade 3 7 --impact high`
|
||||
regrades both against today's list in one read, not #3 first and then #7
|
||||
against whatever regrading #3 produced. Run the bare command again
|
||||
afterwards to see the result and its new numbering.
|
||||
|
||||
The bare listing is read-only and, like `version notes`, exempt from the
|
||||
Iteration Budget Gate; passing indices writes `CHANGES.md` and is counted
|
||||
like `version bump`, because that is what it does."""
|
||||
try:
|
||||
current = version_mod.read_version()
|
||||
except VersionError as exc:
|
||||
fail(str(exc))
|
||||
return
|
||||
|
||||
changes = version_mod.changes_file()
|
||||
if not changes.is_file():
|
||||
fail(f"{version_mod.CHANGES_FILENAME} is missing - there is nothing to regrade")
|
||||
return
|
||||
text = changes.read_text(encoding="utf-8")
|
||||
|
||||
top_entry = version_mod.top_changes_version(text)
|
||||
if top_entry != current:
|
||||
fail(
|
||||
f"{version_mod.CHANGES_FILENAME}'s newest entry is {top_entry}, but "
|
||||
f"{version_mod.VERSION_FILENAME} is {current} - they must agree before a regrade. "
|
||||
"Fix whichever is wrong."
|
||||
)
|
||||
return
|
||||
|
||||
section = version_mod.changes_section(text, current) or ""
|
||||
entries = version_mod.bump_entries(section)
|
||||
if not entries:
|
||||
fail(f"{current}'s {version_mod.CHANGES_FILENAME} entry has no bump list to regrade.")
|
||||
return
|
||||
|
||||
if not indices:
|
||||
for position, (level, bump_title) in enumerate(entries, start=1):
|
||||
typer.echo(f"{position}. [{level}] {bump_title}")
|
||||
return
|
||||
|
||||
if impact is None:
|
||||
fail("--impact is required when regrading - pass one of high/medium/low.")
|
||||
return
|
||||
if impact not in version_mod.IMPACT_LEVELS:
|
||||
fail(f"--impact must be one of {', '.join(version_mod.IMPACT_LEVELS)}, not {impact!r}")
|
||||
return
|
||||
|
||||
updates = {index: impact for index in indices}
|
||||
try:
|
||||
new_text = version_mod.regrade(text, current, updates)
|
||||
except VersionError as exc:
|
||||
fail(str(exc))
|
||||
return
|
||||
|
||||
changes.write_text(new_text, encoding="utf-8")
|
||||
success(f"Regraded {len(indices)} bump title(s) to {impact} impact.")
|
||||
Reference in new issue
Block a user