lib/changelog.sh + changelog-armed — the one-shape rule fails the PR that introduces the drift #159

Closed
opened 2026-07-24 15:43:52 +00:00 by dan-claude-bot · 2 comments
dan-claude-bot commented 2026-07-24 15:43:52 +00:00 (Migrated from github.com)

Part of #157. All line references pinned at f168bbf.

Context

The one-shape rule (#112 D4) is enforced in exactly one place: inside changelog_assemble, which compares fragments against each other at assembly time (L185-L192). On a development tree, changelog-armed validates fragments one at a time (L90-L98); a single fragment carrying ### Fixed is individually legal, so nothing in the per-PR path can see the drift. Four PRs (#143, #147, #150, #152) each passed CI with a grouped fragment on this flat repo, and the mix surfaced only when the 0.3.0 cut ran the assembler by hand — the assembler is deliberately not a CI step (bin/changelog-assemble L4), and stays that way.

The consequence is structural: shape drift is always found at release time, by whoever is cutting, never by the change that caused it. This issue moves the rule onto the PR, the same move #112 D9 made for per-fragment malformedness.

Spec

Decisions, per #157 D3:

  • One definition. A new function in lib/changelog.shchangelog_shape_problem <changelog> <fragments-dir> — with the same contract as the other *_problem functions: print the first reason and return 1, silence and 0 otherwise. The cross-fragment detection currently inlined in changelog_assemble moves into it (or into a helper both call); changelog_assemble keeps refusing a mixed set with the same diagnosis it prints today, delegating rather than duplicating, so guard and assembler cannot disagree.
  • Two rules, in order:
    1. Cross-fragment: the fragment set is one shape — grouped or flat, never both. The existing diagnoses (mixed set, and a single fragment mixing both shapes) survive verbatim.
    2. Anchor: when <changelog> has at least one published ## section, the set's shape must match the newest section's shape (a section is grouped iff its body contains a ### line — changelog_section is the extractor). This is what catches an all-grouped set on a flat repo, which rule 1 structurally cannot. No published section, or an empty fragment set → the rule is vacuous, not a failure.
      A refusal names the offending fragment(s), the expected shape, and where the expectation comes from (the neighbouring fragment, or the published section).
  • changelog-armed calls it in fragment mode after the per-fragment loop, on every tree. On a bare tree the set must already be empty, so the check is effectively the dev-tree guard — which is the point: it fires on the PR that introduces the drift.
  • bin/changelog-assemble calls it too, before assembling — it knows the changelog path, so it picks up the anchor rule the lib-level changelog_assemble cannot (that function takes only a directory and does not grow a new argument).
  • Consumers are unaffected until their pin bump: grouped repos anchor to their own grouped published sections and pass. D8's both-shapes tolerance (no changelog.d/ → today's rules verbatim) is untouched.
  • One changelog.d/159.md fragment for this PR, flat.

Tasks

  • Extract the shape check into changelog_shape_problem in lib/changelog.sh; changelog_assemble delegates its mixed-set refusal to the shared code.
  • Add the anchor rule (newest published section's shape) to changelog_shape_problem.
  • Wire the call into changelog-armed.sh's fragment mode and into bin/changelog-assemble.
  • Tests in test/changelog.test.sh / test/changelog-armed.test.sh covering the must-fail and must-pass cases below.
  • changelog.d/159.md, flat.

Acceptance criteria

  • A tree with flat published sections and one grouped fragment beside flat ones goes red in changelog-armed — the exact #143 case, individually legal today.
  • A tree with flat published sections and an all-grouped fragment set goes red — the case cross-fragment comparison alone cannot see.
  • A mixed set still fails bin/changelog-assemble with the same diagnosis text as today, and now fails changelog-armed with the same shared diagnosis.
  • A grouped set over grouped published sections passes; a consistent set with no published section passes; an empty directory passes; a flat set over flat sections passes.
  • The check exists in exactly one function used by both callers — no second copy of the shape logic survives in changelog_assemble or the action.
  • bash test/run.sh green.

Test plan

  • Constructed trees per case above, driven the way test/changelog-armed.test.sh already drives the action script.
  • The must-fail cases are the spec: each red case must name the offending fragment and the source of the expected shape.

Dependencies

Part of #157. Blocked by #158changelog-armed self-consumes from the PR's own tree, so this check would refuse its own PR while changelog.d/ is still mixed. Blocks #160 (#157 D6).

Part of #157. All line references pinned at [`f168bbf`](https://github.com/heavy-duty/ceremony/tree/f168bbfbacf5e15825750b338e2a06d47c567cc5). ## Context The one-shape rule (#112 D4) is enforced in exactly one place: inside [`changelog_assemble`](https://github.com/heavy-duty/ceremony/blob/f168bbfbacf5e15825750b338e2a06d47c567cc5/lib/changelog.sh#L167), which compares fragments against each other at assembly time ([L185-L192](https://github.com/heavy-duty/ceremony/blob/f168bbfbacf5e15825750b338e2a06d47c567cc5/lib/changelog.sh#L185-L192)). On a development tree, `changelog-armed` validates fragments **one at a time** ([L90-L98](https://github.com/heavy-duty/ceremony/blob/f168bbfbacf5e15825750b338e2a06d47c567cc5/actions/changelog-armed/changelog-armed.sh#L90-L98)); a single fragment carrying `### Fixed` is individually legal, so nothing in the per-PR path can see the drift. Four PRs (#143, #147, #150, #152) each passed CI with a grouped fragment on this flat repo, and the mix surfaced only when the `0.3.0` cut ran the assembler by hand — the assembler is deliberately not a CI step ([`bin/changelog-assemble` L4](https://github.com/heavy-duty/ceremony/blob/f168bbfbacf5e15825750b338e2a06d47c567cc5/bin/changelog-assemble#L4)), and stays that way. The consequence is structural: shape drift is always found at release time, by whoever is cutting, never by the change that caused it. This issue moves the rule onto the PR, the same move #112 D9 made for per-fragment malformedness. ## Spec Decisions, per #157 D3: - **One definition.** A new function in `lib/changelog.sh` — `changelog_shape_problem <changelog> <fragments-dir>` — with the same contract as the other `*_problem` functions: print the first reason and return 1, silence and 0 otherwise. The cross-fragment detection currently inlined in `changelog_assemble` moves into it (or into a helper both call); `changelog_assemble` keeps refusing a mixed set with the same diagnosis it prints today, delegating rather than duplicating, so guard and assembler cannot disagree. - **Two rules, in order:** 1. *Cross-fragment*: the fragment set is one shape — grouped or flat, never both. The existing diagnoses (mixed set, and a single fragment mixing both shapes) survive verbatim. 2. *Anchor*: when `<changelog>` has at least one published `## ` section, the set's shape must match the **newest** section's shape (a section is grouped iff its body contains a `### ` line — [`changelog_section`](https://github.com/heavy-duty/ceremony/blob/f168bbfbacf5e15825750b338e2a06d47c567cc5/lib/changelog.sh#L17) is the extractor). This is what catches an *all*-grouped set on a flat repo, which rule 1 structurally cannot. No published section, or an empty fragment set → the rule is vacuous, not a failure. A refusal names the offending fragment(s), the expected shape, and where the expectation comes from (the neighbouring fragment, or the published section). - **`changelog-armed` calls it** in fragment mode after the per-fragment loop, on every tree. On a bare tree the set must already be empty, so the check is effectively the dev-tree guard — which is the point: it fires on the PR that introduces the drift. - **`bin/changelog-assemble` calls it too**, before assembling — it knows the changelog path, so it picks up the anchor rule the lib-level `changelog_assemble` cannot (that function takes only a directory and does not grow a new argument). - **Consumers are unaffected until their pin bump**: grouped repos anchor to their own grouped published sections and pass. D8's both-shapes tolerance (no `changelog.d/` → today's rules verbatim) is untouched. - One `changelog.d/159.md` fragment for this PR, flat. ## Tasks - [ ] Extract the shape check into `changelog_shape_problem` in `lib/changelog.sh`; `changelog_assemble` delegates its mixed-set refusal to the shared code. - [ ] Add the anchor rule (newest published section's shape) to `changelog_shape_problem`. - [ ] Wire the call into `changelog-armed.sh`'s fragment mode and into `bin/changelog-assemble`. - [ ] Tests in `test/changelog.test.sh` / `test/changelog-armed.test.sh` covering the must-fail and must-pass cases below. - [ ] `changelog.d/159.md`, flat. ## Acceptance criteria - [ ] A tree with flat published sections and **one** grouped fragment beside flat ones goes red in `changelog-armed` — the exact #143 case, individually legal today. - [ ] A tree with flat published sections and an **all-grouped** fragment set goes red — the case cross-fragment comparison alone cannot see. - [ ] A mixed set still fails `bin/changelog-assemble` with the same diagnosis text as today, and now fails `changelog-armed` with the same shared diagnosis. - [ ] A grouped set over grouped published sections passes; a consistent set with no published section passes; an empty directory passes; a flat set over flat sections passes. - [ ] The check exists in exactly one function used by both callers — no second copy of the shape logic survives in `changelog_assemble` or the action. - [ ] `bash test/run.sh` green. ## Test plan - Constructed trees per case above, driven the way `test/changelog-armed.test.sh` already drives the action script. - The must-fail cases are the spec: each red case must name the offending fragment and the source of the expected shape. ## Dependencies Part of #157. Blocked by #158 — `changelog-armed` self-consumes from the PR's own tree, so this check would refuse its own PR while `changelog.d/` is still mixed. Blocks #160 (#157 D6).
github-actions[bot] commented 2026-07-24 16:06:51 +00:00 (Migrated from github.com)

Every issue named by Blocked by is closed. The sweep is moving this issue to ready.

<!-- issueflow:blockers-cleared --> Every issue named by `Blocked by` is closed. The sweep is moving this issue to `ready`.
codex-bot-andresmgsl commented 2026-07-24 16:11:50 +00:00 (Migrated from github.com)

Claimed by @codex-bot-andresmgsl. I am starting implementation now and will open a draft PR after the first commit.

Claimed by @codex-bot-andresmgsl. I am starting implementation now and will open a draft PR after the first commit.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/ceremony#159
No description provided.