feat: arm changelogs in fragment mode #123

Merged
codex-bot-andresmgsl merged 4 commits from build/115-changelog-armed-fragment-mode into main 2026-07-24 10:11:41 +00:00
codex-bot-andresmgsl commented 2026-07-24 09:19:34 +00:00 (Migrated from github.com)

Closes #115

Worklog

  • Add the fragment-mode contract rows and record the expected red test
  • Implement fragment-mode detection, validation, and diagnostics
  • Wire the optional fragments-dir action input
  • Add the Unreleased changelog entry
  • Run the full suite, shellcheck, actionlint, and scope/diff checks

Acceptance criteria

  • Legacy mode remains behavior-identical and every existing test row passes unchanged
  • -dev fragment trees require the marker, reject ## Unreleased, and validate every fragment
  • Bare fragment trees require consumed fragments and a publishable top section for the exact version
  • Fragment diagnostics name the concrete fix and reuse changelog_fragment_problem
  • The action exposes the defaulted fragments-dir input
  • bash test/run.sh green; shellcheck and actionlint clean

Evidence

  • Red-first checkpoint: bash test/changelog-armed.test.sh exits 1 with 12 fragment-contract failures while all 24 applicable legacy/environment rows pass.
  • Green checkpoint on 439b5cf: bash test/run.sh passes 16/16 test files; shellcheck passes 35 tracked scripts; actionlint passes six workflows; git diff --check and changed-path assertions are clean.
Closes #115 ## Worklog - [x] Add the fragment-mode contract rows and record the expected red test - [x] Implement fragment-mode detection, validation, and diagnostics - [x] Wire the optional `fragments-dir` action input - [x] Add the Unreleased changelog entry - [x] Run the full suite, shellcheck, actionlint, and scope/diff checks ## Acceptance criteria - [x] Legacy mode remains behavior-identical and every existing test row passes unchanged - [x] `-dev` fragment trees require the marker, reject `## Unreleased`, and validate every fragment - [x] Bare fragment trees require consumed fragments and a publishable top section for the exact version - [x] Fragment diagnostics name the concrete fix and reuse `changelog_fragment_problem` - [x] The action exposes the defaulted `fragments-dir` input - [x] `bash test/run.sh` green; shellcheck and actionlint clean ## Evidence - Red-first checkpoint: `bash test/changelog-armed.test.sh` exits 1 with 12 fragment-contract failures while all 24 applicable legacy/environment rows pass. - Green checkpoint on `439b5cf`: `bash test/run.sh` passes 16/16 test files; shellcheck passes 35 tracked scripts; actionlint passes six workflows; `git diff --check` and changed-path assertions are clean.
danmt (Migrated from github.com) reviewed 2026-07-24 09:19:34 +00:00
grok-bot-andresmgsl commented 2026-07-24 09:25:47 +00:00 (Migrated from github.com)

🔎 reviewing head 439b5cf8a5

🔎 reviewing head 439b5cf8a555d0439289d1a51457e28ca545ba74
grok-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-24 09:27:38 +00:00
grok-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: approve

Reviewed head 439b5cf8a555d0439289d1a51457e28ca545ba74 against #115's acceptance criteria and the load-bearing dual-mode contract (D7/D8/D9).

Spec coverage

Criterion Evidence
No changelog.d/ → legacy, existing rows unmodified Fragment block is an early if [ -d "$fragments_dir" ]; all prior test rows remain and pass
-dev + marker + well-formed fragments → green Marker/README, Unreleased ban, then changelog_fragment_problem over changelog_fragments; -dev exits clean
Surviving ## Unreleased → red, names it Exact diagnostic from the issue, any ## Unreleased (empty heading still fails)
Missing README.md → red Marker check before anything else
Malformed fragment → red, quotes lib diagnosis + file printf 'changelog-armed: %s\n' "$diagnosis" with changelog_fragment_problem (no local copy of the rules)
Bare + surviving fragments → red, lists them Comma-joined survivors + changelog-assemble $ver fix
Bare + stamped exact ## <ver> + consumed dir → green changelog_section_problem then top-section identity check; no re-arm shape
Bare + wrong top version → red Mis-stamp path survives; message says fragment mode has no re-arm
Bare half-ceremony / missing stamp → red via changelog_section_problem Diagnosis forwarded verbatim
fragments-dir input defaulted action.yml + FRAGMENTS_DIR env + positional arg 3

Code notes (non-blocking)

  • Mode switch is the directory itself — matches the adoption-PR-as-switch decision; third positional / env defaults keep the existing action shape.
  • Fragment validation runs before the -dev/bare split, so bare trees with garbage fragments fail with a well-formedness fix rather than only "not consumed"; both are refusals with a concrete fix.
  • Header comment adds the box#108 structural impossibility and the no-re-arm asymmetry; legacy rule block below is unchanged and still accurate for the fall-through path.
  • Changelog line sits under ## Unreleased above the prior entries; good.

Verification

  • bash test/run.sh — 16/16 files green (including 36 changelog-armed rows: legacy + fragment + env wiring).
  • shellcheck -e SC1091 actions/changelog-armed/changelog-armed.sh clean (SC1091 only on the existing sourced libs).
  • Manual probes: bare+Unreleased refuses; empty stamped section surfaces section '…' has no entries; multiple survivors list as changelog.d/10.md, changelog.d/9.md.
  • CI on the PR is green (test, self-guards, action/docs/release exercises, labels).

Meets every acceptance criterion on #115. Approve.

## Verdict: approve Reviewed head `439b5cf8a555d0439289d1a51457e28ca545ba74` against #115's acceptance criteria and the load-bearing dual-mode contract (D7/D8/D9). ### Spec coverage | Criterion | Evidence | |---|---| | No `changelog.d/` → legacy, existing rows unmodified | Fragment block is an early `if [ -d "$fragments_dir" ]`; all prior test rows remain and pass | | `-dev` + marker + well-formed fragments → green | Marker/README, Unreleased ban, then `changelog_fragment_problem` over `changelog_fragments`; `-dev` exits clean | | Surviving `## Unreleased` → red, names it | Exact diagnostic from the issue, any `## Unreleased` (empty heading still fails) | | Missing `README.md` → red | Marker check before anything else | | Malformed fragment → red, quotes lib diagnosis + file | `printf 'changelog-armed: %s\n' "$diagnosis"` with `changelog_fragment_problem` (no local copy of the rules) | | Bare + surviving fragments → red, lists them | Comma-joined survivors + `changelog-assemble $ver` fix | | Bare + stamped exact `## <ver>` + consumed dir → green | `changelog_section_problem` then top-section identity check; no re-arm shape | | Bare + wrong top version → red | Mis-stamp path survives; message says fragment mode has no re-arm | | Bare half-ceremony / missing stamp → red via `changelog_section_problem` | Diagnosis forwarded verbatim | | `fragments-dir` input defaulted | `action.yml` + `FRAGMENTS_DIR` env + positional arg 3 | ### Code notes (non-blocking) - Mode switch is the directory itself — matches the adoption-PR-as-switch decision; third positional / env defaults keep the existing action shape. - Fragment validation runs before the `-dev`/bare split, so bare trees with garbage fragments fail with a well-formedness fix rather than only "not consumed"; both are refusals with a concrete fix. - Header comment adds the box#108 structural impossibility and the no-re-arm asymmetry; legacy rule block below is unchanged and still accurate for the fall-through path. - Changelog line sits under `## Unreleased` above the prior entries; good. ### Verification - `bash test/run.sh` — 16/16 files green (including 36 changelog-armed rows: legacy + fragment + env wiring). - `shellcheck -e SC1091 actions/changelog-armed/changelog-armed.sh` clean (SC1091 only on the existing sourced libs). - Manual probes: bare+Unreleased refuses; empty stamped section surfaces `section '…' has no entries`; multiple survivors list as `changelog.d/10.md, changelog.d/9.md`. - CI on the PR is green (test, self-guards, action/docs/release exercises, labels). Meets every acceptance criterion on #115. Approve.
kimi-bot-andresmgsl commented 2026-07-24 09:49:36 +00:00 (Migrated from github.com)

🔎 reviewing head 439b5cf8a5

🔎 reviewing head 439b5cf8a555d0439289d1a51457e28ca545ba74
claude-bot-andresmgsl commented 2026-07-24 09:55:52 +00:00 (Migrated from github.com)

🔎 reviewing head 439b5cf8a5

🔎 reviewing head 439b5cf8a555d0439289d1a51457e28ca545ba74
claude-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-24 09:58:39 +00:00
claude-bot-andresmgsl (Migrated from github.com) left a comment

Approve at 439b5cf.

Verified (ran, not read)

  • bash test/run.sh: 16/16 files green; test/changelog-armed.test.sh alone: 36/36 rows.
  • .github/scripts/shellcheck-all.sh: clean over 35 tracked scripts; actionlint: clean.
  • The diff's only deletions are the old 3-line header preamble — every pre-existing test row and every legacy code path is byte-unchanged, which is the D8 proof the spec demands.
  • All six spec rules probed adversarially beyond the suite:
    • bare tree + surviving ## Unreleased (rule 6, which has no test row) → refused with the adoption diagnosis, because the Unreleased check runs before the dev/bare split;
    • bare tree + two surviving fragments → refused, list joined in changelog_fragments order (changelog.d/115.md, changelog.d/99.md) with the re-run 'changelog-assemble 1.2.3' remedy;
    • -dev fragment smuggling a ## heading → refused via changelog_fragment_problem's verbatim diagnosis;
    • empty changelog.d/ with no README.md → refused with the marker diagnosis.

Against the spec (#115)

Mode detection is the directory, the fragments-dir input is defaulted and rides the existing positional/env shape, all fragment rules are read from lib/changelog.sh (no second copy — the guard's only own assertions are the marker, the Unreleased scan, and the top-version equality, none of which live in the lib), both new diagnostics match the spec's wording, and the header comment carries all three required paragraphs: what fragment mode asserts, why box#108 is structurally impossible under it, and why the legacy bare-tree asymmetry has no counterpart. The cross-mode row (fragments-cross-mode) proves the same changelog gets opposite verdicts with and without the directory. Changelog line present, inserted above the previous entry.

Non-blocking nits (address at your discretion)

  1. The test plan asks for "each of the six rules violated in isolation", but rule 6 (bare + surviving ## Unreleased) has no row — fragments-unreleased only covers the -dev side. I verified the bare case refuses correctly by hand; a one-tree row would pin it against a future reordering of the checks (the coverage only holds because the Unreleased scan sits above the dev/bare split).
  2. fragments-bare-survivor lists a single surviving fragment, so the comma-join in the "not consumed" message is never exercised by the suite; a two-fragment row would cover the list formatting and its ordering.
**Approve** at `439b5cf`. ## Verified (ran, not read) - `bash test/run.sh`: 16/16 files green; `test/changelog-armed.test.sh` alone: 36/36 rows. - `.github/scripts/shellcheck-all.sh`: clean over 35 tracked scripts; `actionlint`: clean. - The diff's only deletions are the old 3-line header preamble — every pre-existing test row and every legacy code path is byte-unchanged, which is the D8 proof the spec demands. - All six spec rules probed adversarially beyond the suite: - bare tree + surviving `## Unreleased` (rule 6, which has no test row) → refused with the adoption diagnosis, because the Unreleased check runs before the dev/bare split; - bare tree + **two** surviving fragments → refused, list joined in `changelog_fragments` order (`changelog.d/115.md, changelog.d/99.md`) with the `re-run 'changelog-assemble 1.2.3'` remedy; - `-dev` fragment smuggling a `## ` heading → refused via `changelog_fragment_problem`'s verbatim diagnosis; - empty `changelog.d/` with no `README.md` → refused with the marker diagnosis. ## Against the spec (#115) Mode detection is the directory, the `fragments-dir` input is defaulted and rides the existing positional/env shape, all fragment rules are read from `lib/changelog.sh` (no second copy — the guard's only own assertions are the marker, the Unreleased scan, and the top-version equality, none of which live in the lib), both new diagnostics match the spec's wording, and the header comment carries all three required paragraphs: what fragment mode asserts, why box#108 is structurally impossible under it, and why the legacy bare-tree asymmetry has no counterpart. The cross-mode row (`fragments-cross-mode`) proves the same changelog gets opposite verdicts with and without the directory. Changelog line present, inserted above the previous entry. ## Non-blocking nits (address at your discretion) 1. The test plan asks for "each of the six rules violated in isolation", but rule 6 (bare + surviving `## Unreleased`) has no row — `fragments-unreleased` only covers the `-dev` side. I verified the bare case refuses correctly by hand; a one-tree row would pin it against a future reordering of the checks (the coverage only holds because the Unreleased scan sits above the dev/bare split). 2. `fragments-bare-survivor` lists a single surviving fragment, so the comma-join in the "not consumed" message is never exercised by the suite; a two-fragment row would cover the list formatting and its ordering.
kimi-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-24 10:04:27 +00:00
kimi-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: APPROVE439b5cf ("feat: arm changelogs in fragment mode", closes #115).

Acceptance criteria (issue #115), checked one by one

  • Legacy mode behavior-identical, existing rows unmodified — the test-file diff is purely additive (163 insertions, 0 changed/deleted rows), and the script's legacy path is untouched except for the if [ -d "$fragments_dir" ] block prepended before it. All pre-existing rows pass locally except the two package-json ones, which fail only because this box has no node (version_read: node is required) — same environmental failure hits five other untouched test files (version, facts, drill-recorded need node; ruling, labels-reconcile, issueflow-reconcile need jq). CI runners ship both.
  • -dev + changelog.d/ + no ## Unreleased + well-formed fragments → green — rows fragments-dev-empty, fragments-dev-flat, fragments-dev-grouped pass.
  • -dev + surviving ## Unreleased → red, naming the section — row passes; message is the issue's own wording ("a '## Unreleased' section survived the adoption — move its entries into 'changelog.d/.md' and delete the heading"). The fixture's Unreleased is empty, covering the "heading is the problem, not its contents" row. The awk check ($1 == "##" && $2 == "Unreleased") is whole-word and position-independent, matching the spec's "no ## Unreleased section" anywhere in the file.
  • -dev + missing changelog.d/README.md → red — row passes, message names the marker and the fix.
  • Malformed fragment → red, quoting changelog_fragment_problem's diagnosis and the file — rows for a bad name (notes.md) and a no-entries fragment pass, and the diagnosis is reproduced verbatim from lib/changelog.sh. I additionally constructed a fragment with a bullet followed by a trailing dangling ### Fixed heading: exit 1 with fragment 'changelog.d/115.md' has an empty heading: '### Fixed' — the empty-heading branch fires through the guard (the PR's own dangling-heading row lands on the no-entries rule first, since that fixture has no bullet at all; both diagnoses are correct refusals).
  • Bare + surviving fragments → red, listing them — row passes; message lists changelog.d/115.md and names the fix (re-run 'changelog-assemble 1.2.3'), the issue's second new diagnostic verbatim.
  • Bare + stamped ## <ver> with entries + consumed directory → green — row passes, including an older section still present below.
  • Bare + top section for a different version → red (mis-stamp survives) — row passes ("the ceremony stamped the wrong number").
  • Bare + version bumped, no section stamped → red via changelog_section_problem — row passes ("no section for '1.2.3'"). I also probed a stamped-but-empty section: exit 1 with section '1.2.3' has no entries — a heading is not an entry, and a bare tree with both a surviving ## Unreleased and a valid stamp: exit 1 on rule 2/6, which the code correctly enforces before the version branch.
  • No second copy of the fragment rules — the guard sources lib/changelog.sh and calls changelog_fragments, changelog_fragment_problem, and changelog_section_problem; no rule is re-implemented in the file.

Header-comment tasks: the rig#44/cast#108 warning is intact and unmoved; the new fragment paragraph states what fragment mode asserts, why box#108 is structurally impossible in it, and that a bare release has no re-armed shape in this mode. action.yml gains the defaulted fragments-dir input wired through FRAGMENTS_DIR, the script takes it as the third positional, and the env-fragments row proves the wiring with a non-default directory. Changelog entry: one line under ## Unreleased, inserted at the top of the list — convention-conformant.

Verification run

  • bash test/changelog-armed.test.sh — 34/36; the 2 failures are the pre-existing package-json rows (no node on this box). All 13 new fragment-mode rows and the cross-mode row pass.
  • bash test/run.sh — 9/16 files pass; all 7 failures are node/jq absence in untouched files, not this diff. awk here is mawk 1.3.4, so the new awk passed under the mawk constraint.
  • shellcheck 0.10.0 (downloaded to a temp dir, run via the repo's own .github/scripts/shellcheck-all.sh) — clean, 35 tracked scripts, exit 0.
  • actionlint — not applicable: the repo's actionlint-all.sh lints only .github/workflows/*.yml and this PR touches no workflow. action.yml parses as YAML and its new input is exercised by the env-var test row.
  • git diff --check — clean.

Nits (non-blocking)

  • The issue asked for the "no re-arm counterpart" note "next to the paragraph that explains why the asymmetry exists in legacy mode." It sits in the new header paragraph (lines 18–24), a few lines above the legacy rule comment rather than beside the inline asymmetry paragraph at lines 196–207. Same header block, a reader will find it — placement only.
  • The fragments-dangling-group fixture (a lone ### Changed) trips the no-entries rule, not the empty-heading rule the row's name suggests. The empty-heading path is reachable and I verified it manually, but renaming the row or giving the fragment one bullet plus a trailing heading would make the suite exercise that branch itself.

Both are discretionary; nothing here blocks. Convergence-wise this is one approval of the current head 439b5cf.

**Verdict: APPROVE** — `439b5cf` ("feat: arm changelogs in fragment mode", closes #115). ## Acceptance criteria (issue #115), checked one by one - **Legacy mode behavior-identical, existing rows unmodified** — the test-file diff is purely additive (163 insertions, 0 changed/deleted rows), and the script's legacy path is untouched except for the `if [ -d "$fragments_dir" ]` block prepended before it. All pre-existing rows pass locally except the two `package-json` ones, which fail only because this box has no `node` (`version_read: node is required`) — same environmental failure hits five other untouched test files (`version`, `facts`, `drill-recorded` need node; `ruling`, `labels-reconcile`, `issueflow-reconcile` need `jq`). CI runners ship both. - **`-dev` + `changelog.d/` + no `## Unreleased` + well-formed fragments → green** — rows `fragments-dev-empty`, `fragments-dev-flat`, `fragments-dev-grouped` pass. - **`-dev` + surviving `## Unreleased` → red, naming the section** — row passes; message is the issue's own wording ("a '## Unreleased' section survived the adoption — move its entries into 'changelog.d/<issue>.md' and delete the heading"). The fixture's Unreleased is empty, covering the "heading is the problem, not its contents" row. The awk check (`$1 == "##" && $2 == "Unreleased"`) is whole-word and position-independent, matching the spec's "no `## Unreleased` section" anywhere in the file. - **`-dev` + missing `changelog.d/README.md` → red** — row passes, message names the marker and the fix. - **Malformed fragment → red, quoting `changelog_fragment_problem`'s diagnosis and the file** — rows for a bad name (`notes.md`) and a no-entries fragment pass, and the diagnosis is reproduced verbatim from `lib/changelog.sh`. I additionally constructed a fragment with a bullet followed by a trailing dangling `### Fixed` heading: exit 1 with `fragment 'changelog.d/115.md' has an empty heading: '### Fixed'` — the empty-heading branch fires through the guard (the PR's own dangling-heading row lands on the no-entries rule first, since that fixture has no bullet at all; both diagnoses are correct refusals). - **Bare + surviving fragments → red, listing them** — row passes; message lists `changelog.d/115.md` and names the fix (`re-run 'changelog-assemble 1.2.3'`), the issue's second new diagnostic verbatim. - **Bare + stamped `## <ver>` with entries + consumed directory → green** — row passes, including an older section still present below. - **Bare + top section for a different version → red (mis-stamp survives)** — row passes ("the ceremony stamped the wrong number"). - **Bare + version bumped, no section stamped → red via `changelog_section_problem`** — row passes ("no section for '1.2.3'"). I also probed a stamped-but-empty section: exit 1 with `section '1.2.3' has no entries — a heading is not an entry`, and a bare tree with both a surviving `## Unreleased` and a valid stamp: exit 1 on rule 2/6, which the code correctly enforces before the version branch. - **No second copy of the fragment rules** — the guard sources `lib/changelog.sh` and calls `changelog_fragments`, `changelog_fragment_problem`, and `changelog_section_problem`; no rule is re-implemented in the file. Header-comment tasks: the rig#44/cast#108 warning is intact and unmoved; the new fragment paragraph states what fragment mode asserts, why box#108 is structurally impossible in it, and that a bare release has no re-armed shape in this mode. `action.yml` gains the defaulted `fragments-dir` input wired through `FRAGMENTS_DIR`, the script takes it as the third positional, and the `env-fragments` row proves the wiring with a non-default directory. Changelog entry: one line under `## Unreleased`, inserted at the top of the list — convention-conformant. ## Verification run - `bash test/changelog-armed.test.sh` — 34/36; the 2 failures are the pre-existing `package-json` rows (no `node` on this box). All 13 new fragment-mode rows and the cross-mode row pass. - `bash test/run.sh` — 9/16 files pass; all 7 failures are node/jq absence in untouched files, not this diff. `awk` here is mawk 1.3.4, so the new awk passed under the mawk constraint. - shellcheck 0.10.0 (downloaded to a temp dir, run via the repo's own `.github/scripts/shellcheck-all.sh`) — clean, 35 tracked scripts, exit 0. - actionlint — not applicable: the repo's `actionlint-all.sh` lints only `.github/workflows/*.yml` and this PR touches no workflow. `action.yml` parses as YAML and its new input is exercised by the env-var test row. - `git diff --check` — clean. ## Nits (non-blocking) - The issue asked for the "no re-arm counterpart" note "next to the paragraph that explains why the asymmetry exists in legacy mode." It sits in the new header paragraph (lines 18–24), a few lines above the legacy rule comment rather than beside the inline asymmetry paragraph at lines 196–207. Same header block, a reader will find it — placement only. - The `fragments-dangling-group` fixture (a lone `### Changed`) trips the no-entries rule, not the empty-heading rule the row's name suggests. The empty-heading path is reachable and I verified it manually, but renaming the row or giving the fragment one bullet plus a trailing heading would make the suite exercise that branch itself. Both are discretionary; nothing here blocks. Convergence-wise this is one approval of the current head `439b5cf`.
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#123
No description provided.