stack: TOC-Scope auf types/ und docs/, Sprachregeln nach AGENTS.md zentralisiert, alle Templates auf Control-Plane-Sprache, --breaking akkumuliert (schliesst #99)
Files changed: - AGENTS.md - CHANGES.md - ENVIRONMENT.md.template - SOUL.md - SOUL.md.template - USER.md.template - VERSION - docs/ownership-and-templates.md - docs/version-model.md - instructions/CONTRACT.md - instructions/dev/doc-pull-through.md - instructions/dev/stack-close/SKILL.md - instructions/dev/stack-dev/SKILL.md - instructions/dev/version-parts.md - instructions/setup-instance.md - kb/CONVENTIONS.md - kb/CONVENTIONS.md.template - kb/concepts/COLLECTION.md - kb/sources/COLLECTION.md - tools/CONTRACT.md - tools/chemenu/commands/types_cmd.py - tools/chemenu/commands/version_cmd.py - tools/chemenu/tests/test_toc.py - tools/chemenu/tests/test_version_cmd.py - tools/chemenu/toc.py - tools/chemenu/version.py - types/comparison.md - types/concept.md - types/entity.md - types/lint-report.md - types/source.md
This commit is contained in:
1 parent
c0dc2129bb
commit
c64479fe02
31 files changed
+1093
-603
No files matched your search
@@ -583,9 +583,11 @@ def regrade(text: str, version: "Version", updates: dict[int, str]) -> str:
|
||||
def _set_marker_line(section: str, marker: str, line: str) -> str:
|
||||
"""Add or replace the one-line `marker ...` paragraph in `section`.
|
||||
|
||||
Used for the breaking-change and no-migration lines, which - unlike the
|
||||
bumps list - are not accumulated: a later bump that repeats `--breaking`
|
||||
restates it rather than growing a list nobody would read as history.
|
||||
Used for the no-migration line, which - unlike the bumps list and unlike
|
||||
the breaking-change paragraph below - is **not** accumulated: it answers
|
||||
one yes/no question about the candidate as a whole ("does content have to
|
||||
change?"), so a second answer replaces the first rather than joining it,
|
||||
and `_clear_marker_line` is its retraction path.
|
||||
Anchored just above the bumps region (not below it, as before Gitea #95):
|
||||
with a graded, potentially 30-line list, the line an operator most needs
|
||||
to act on stayed the deepest thing in the entry otherwise.
|
||||
@@ -599,6 +601,73 @@ def _set_marker_line(section: str, marker: str, line: str) -> str:
|
||||
return section.rstrip() + f"\n\n{line}\n"
|
||||
|
||||
|
||||
# The breaking-change paragraph, matched whole: the marker line plus any `- `
|
||||
# bullets under it. `_set_marker_line`'s `^marker.*$` reaches the first line
|
||||
# only, which is exactly wrong for a form that can carry bullets beneath it.
|
||||
_BREAKING_PARAGRAPH_RE = re.compile(
|
||||
rf"^{re.escape(BREAKING_CHANGE_MARKER)}.*(?:\n-[ \t].*)*$", re.MULTILINE
|
||||
)
|
||||
|
||||
|
||||
def breaking_reasons(section: str) -> list[str]:
|
||||
"""The breaking-change paragraph parsed back into one reason per crossing,
|
||||
in written order.
|
||||
|
||||
Two shapes read the same way, which is what lets an entry written before
|
||||
accumulation existed round-trip untouched: `**Breaking Change:** <reason>`
|
||||
is one reason, and a bare `**Breaking Change:**` followed by `- ` bullets
|
||||
is one reason per bullet. Same "flat while there is only one of them"
|
||||
trick `_bumps_block` plays with its impact groups, and for the same
|
||||
reason - the common case keeps the shape it always had.
|
||||
"""
|
||||
match = _BREAKING_PARAGRAPH_RE.search(section)
|
||||
if not match:
|
||||
return []
|
||||
lines = match.group(0).splitlines()
|
||||
head = lines[0][len(BREAKING_CHANGE_MARKER):].strip()
|
||||
reasons = [head] if head else []
|
||||
reasons += [line.strip()[2:].strip() for line in lines[1:]]
|
||||
return [reason for reason in reasons if reason]
|
||||
|
||||
|
||||
def _breaking_paragraph(reasons: list[str]) -> str:
|
||||
"""Render the breaking-change paragraph from one reason per crossing.
|
||||
|
||||
One reason stays on the marker line - byte-identical to what every entry
|
||||
written before accumulation carries. Two or more move to bullets under a
|
||||
bare marker, because a single line holding two unrelated breakages reads
|
||||
as one run-on sentence and an operator has to act on each separately.
|
||||
"""
|
||||
if len(reasons) == 1:
|
||||
return f"{BREAKING_CHANGE_MARKER} {reasons[0]}"
|
||||
bullets = "\n".join(f"- {reason}" for reason in reasons)
|
||||
return f"{BREAKING_CHANGE_MARKER}\n{bullets}"
|
||||
|
||||
|
||||
def _add_breaking_reason(section: str, reason: str) -> str:
|
||||
"""Append `reason` to the breaking-change paragraph, or start one.
|
||||
|
||||
Accumulates rather than replaces: a candidate can cross the compatibility
|
||||
boundary more than once (this is the normal shape of a long-running one),
|
||||
and each crossing is a separate thing the operator of an existing instance
|
||||
has to act on. Replacing meant the second `--breaking` silently deleted
|
||||
the first - the entry then promised a single break while shipping two.
|
||||
|
||||
Repeating a reason verbatim is a no-op, so a re-run after an interrupted
|
||||
bump converges instead of writing the same sentence twice.
|
||||
"""
|
||||
existing = breaking_reasons(section)
|
||||
if reason in existing:
|
||||
return section
|
||||
paragraph = _breaking_paragraph(existing + [reason])
|
||||
if existing:
|
||||
return _BREAKING_PARAGRAPH_RE.sub(lambda _m: paragraph, section, count=1)
|
||||
anchor = section.find(_BUMPS_OPEN)
|
||||
if anchor != -1:
|
||||
return section[:anchor] + f"{paragraph}\n\n" + section[anchor:]
|
||||
return section.rstrip() + f"\n\n{paragraph}\n"
|
||||
|
||||
|
||||
def _clear_marker_line(section: str, marker: str) -> str:
|
||||
"""Remove the one-line `marker ...` paragraph from `section`, if present.
|
||||
|
||||
@@ -633,10 +702,15 @@ def _update_open_candidate(
|
||||
impact: str = DEFAULT_IMPACT,
|
||||
) -> str:
|
||||
"""Move the topmost entry's heading to `version`/`date`/`title`, append
|
||||
`(impact, title)` to its machine-managed bump list, and set the
|
||||
`(impact, title)` to its machine-managed bump list, and record the
|
||||
breaking/no-migration lines only where this call supplies them - see
|
||||
`insert_changes_entry`.
|
||||
|
||||
The two are recorded differently on purpose: a `breaking_reason` **joins**
|
||||
whatever crossings the candidate already recorded (`_add_breaking_reason`),
|
||||
a `no_migration_reason` **replaces** the single line that answers whether
|
||||
content has to change (`_set_marker_line`).
|
||||
|
||||
`migration_required` retracts an earlier `--no-migration` line instead of
|
||||
setting one - the two are mutually exclusive on a single bump, enforced by
|
||||
the caller (`version_cmd.bump_command`), not here."""
|
||||
@@ -653,7 +727,7 @@ def _update_open_candidate(
|
||||
)
|
||||
|
||||
if breaking_reason:
|
||||
section = _set_marker_line(section, BREAKING_CHANGE_MARKER, f"{BREAKING_CHANGE_MARKER} {breaking_reason}")
|
||||
section = _add_breaking_reason(section, breaking_reason)
|
||||
if no_migration_reason:
|
||||
section = _set_marker_line(section, MIGRATION_NONE_MARKER, f"{MIGRATION_NONE_MARKER} - {no_migration_reason}")
|
||||
elif migration_required:
|
||||
@@ -684,7 +758,9 @@ def insert_changes_entry(
|
||||
candidate still open" here.
|
||||
|
||||
A fresh entry gets the skeleton only: heading, date, author, - when a
|
||||
compatibility boundary is crossed - the line saying what breaks, plus the
|
||||
compatibility boundary is crossed - the line saying what breaks (one
|
||||
crossing, so the flat one-line form; a candidate that crosses again
|
||||
accumulates bullets there, see `_add_breaking_reason`), plus the
|
||||
line saying no content has to change where that applies, and then the
|
||||
machine-managed bump list (started with this one `(impact, title)` pair,
|
||||
for a candidate). The break comes first, above the bump list rather than
|
||||
|
||||
Reference in new issue
Block a user