Skip to content

chore(changelog): land entries as per-change fragments in changelog.d/ - #16

Merged
Anarchid merged 4 commits into
mainfrom
chore/changelog-fragments
Aug 24, 2026
Merged

Anarchid merged 4 commits into
mainfrom
chore/changelog-fragments

Conversation

@Anarchid

Copy link
Copy Markdown
Contributor

Problem

The changelog policy is a structural conflict generator: every PR inserts prose at the same textual location (the top of ## Unreleased in CHANGELOG.md), so any PR that outlives another merge conflicts there. Full rationale and design discussion in anima-research/agent-framework#128 — this PR is the chronicle leg of the same ecosystem-wide rollout (same shape as the July policy rollout).

Changes

Entries now land as one uniquely-named file per change — changelog.d/<slug>.<breaking|added|changed|fixed>.md, containing the entry's markdown bullets verbatim (slug = PR number or branch name). Distinct files never conflict in git.

  • scripts/release-changelog.mjs (the npm version hook) folds pending fragments plus anything filed directly under ## Unreleased into the new ## X.Y.Z — date section (subsections in Breaking/Added/Changed/Fixed order; fragments join an existing matching subsection, including audience-qualified ### Breaking (on-disk format) headings), deletes the consumed fragments, and keeps all existing refusals. New refusals: unrecognized category suffix and non-bullet fragment content. Chronicle's script was byte-identical to agent-framework's (it is repo-agnostic — reads package.json for name-free version handling), so the new version drops in verbatim.
  • changelog.yml soft check now passes on a well-formed new fragment or a CHANGELOG.md edit (transition-friendly for in-flight PRs; no-changelog label escape unchanged). changelog.d/README.md and badly-named fragments do not satisfy it. Chronicle's existing ^src/ path condition (which here matches the Rust crate's src/) is kept as-is.
  • publish.yml is untouched — the tag-time section guard and the github-release mirror job both read the assembled versioned section, which exists at tag time exactly as before because assembly runs pre-tag (verified below).
  • Migration needs no flag day: direct ## Unreleased edits remain a supported input, merged at release time. (Chronicle's ## Unreleased is currently empty, so nothing even needs carrying over.)
  • CONTRIBUTING.md ## Changelog section rewritten; chronicle-specific prose kept verbatim (what-needs-an-entry list: napi surface, store/branch/state semantics, on-disk/wire format, recovery, packaging; the "say plainly whether existing stores keep opening" breaking guidance; the five-target cross-build in the release ritual). Breaking fragments open the bullet naming who needs to act — chronicle's example adapted to - **On-disk format:** …, matching its existing ### Breaking (on-disk format) heading style, which remains available via a direct Unreleased edit.
  • PR-template checkbox updated, changelog.d/README.md documents the format in-place (example slug 10-torn-tail.fixed.md), and this PR dogfoods the scheme with its own fragment (changelog-fragments.changed.md), so the next release exercises the pipeline.

Companion to anima-research/agent-framework#128 and siblings in membrane, context-manager, and connectome-host; safe to merge in any order — each repo's scheme is self-contained.

Tests

Release script exercised on copies in a scratch dir through all eight legs (outputs pasted from the runs):

  1. Clean cut — 4 fragments (breaking/added/fixed/dogfood-changed) + manual entries filed directly under Unreleased (including an audience-qualified ### Breaking (on-disk format) heading): breaking fragment joined the qualified heading, canonical subsection order, continuation lines preserved, fragments deleted, README.md kept. CHANGELOG.md: released '## 0.4.0 — 2026-08-24' from 4 fragment(s) plus the Unreleased section. Exit 0.
  2. Refuse-duplicate (version set to 0.3.0) — a '## 0.3.0' section already exists. Exit 1.
  3. Refuse-empty — no fragments, empty Unreleased: nothing to release as 0.4.0 — no fragments in changelog.d/ and no entries under '## Unreleased'. Exit 1.
  4. Fragments-only cut (chronicle's real state: empty Unreleased, dogfood fragment only) — clean ### Changed section, fresh empty ## Unreleased kept above, exit 0.
  5. Refuse bad category suffix (oops.md) — exit 1 with naming guidance.
  6. Refuse non-bullet fragment — exit 1.
  7. Tag guard + github-release awk (the exact commands from publish.yml) run on leg 1's assembled output with GITHUB_REF_NAME=v0.4.0: guard passes, awk extracts the 26-line section, non-empty.
  8. changelog.yml grep logic simulated on six changed-file sets: src+fragment PASS, src+CHANGELOG PASS (legacy), src-alone FAIL, src+changelog.d/README.md-only FAIL, src+badly-named-fragment FAIL, docs-only PASS.

Compatibility

No format change — docs, CI, and release tooling only; no Rust or napi code touched.

Not verified

A real npm version run end-to-end (the next actual release is the live exercise — same status as the existing tag-time guards, unexercised since #14 landed). The changelog check will run live on this PR itself.


  • Changelog fragment added — changelog.d/changelog-fragments.changed.md.

🤖 Generated with Claude Code

Concurrent PRs editing the shared '## Unreleased' section of CHANGELOG.md
conflict whenever one PR outlives another merge. Entries now land as
uniquely-named fragment files (changelog.d/<slug>.<category>.md), which
git merges without conflict; the npm-version hook folds them into the
release section and deletes them. Direct '## Unreleased' edits remain
supported and are merged at the same point, so in-flight PRs need no
rework. Tag-time publish guard and github-release job are unchanged.

Part of the ecosystem-wide rollout of the fragment scheme; reference
implementation and full rationale in agent-framework PR #128.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nts in CI

Two findings from Greptile's review of the membrane companion PR
(antra-tess/membrane#49), applied wave-wide:
a fragment whose first line was a bullet could smuggle top-level prose
or a rogue '## ' heading (a fake section boundary) into the released
changelog, and the CI check's path-only grep let a deleted or renamed
fragment satisfy the entry requirement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Anarchid

Copy link
Copy Markdown
Contributor Author

Greptile's review of the membrane companion (antra-tess/membrane#49) found two issues in the shared code, fixed here in 0479dcb: fragment validation now checks every line (top-level prose or a rogue ## heading refuses the release), and the CI check counts only added fragments (--diff-filter=A), so deleting/renaming one no longer satisfies it.

…egression tests

Codex QA pass on the membrane companion (antra-tess/membrane#49) found that
a slug taken verbatim from a 'fix/foo' branch name — which the docs
invited — created changelog.d/fix/foo.fixed.md, and the scanner skipped
the 'fix' directory without a word: a silent release-note data-loss path
that PR CI catches but direct pushes bypass. The scanner now aborts on
any entry that is not README.md or a well-formed fragment file, and the
docs prescribe filesystem-safe slugs ('/' -> '-').

The continuation-line check also treated any whitespace-prefixed line as
valid, so an indented '## ' heading still passed. Continuations must now
be indented two or more spaces, and headings/rules are refused at any
indentation.

The new regression test runs the real script in a throwaway directory:
fragment-only and mixed assembly, canonical ordering, audience-qualified
headings, deletion + README preservation, and fourteen refusal cases
asserting nothing is modified on failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Anarchid

Copy link
Copy Markdown
Contributor Author

A Codex QA pass on the membrane companion (antra-tess/membrane#49, blocking) found two more shared-code issues, fixed here in ab31cdf: a fragment slug taken verbatim from a fix/foo branch name produced a subdirectory the scanner silently skipped (now fail-closed — anything that isn't README.md or a well-formed fragment aborts the release; docs prescribe / → - slugs), and indented ## headings still slipped past the line check (now a strict grammar: two-space continuations, headings/rules refused at any indent). It also asked for automated coverage: scripts/release-changelog.test.mjs runs the real script in a throwaway directory across 18 cases, including every refusal asserting nothing is modified.

… and spaced rules

Second Codex QA round on the membrane companion (antra-tess/membrane#49,
head f72e00a):

- Refusals used console.error + process.exit(1). Where stderr is a pipe
  (asynchronous on macOS), the exit can pre-empt the write, so callers —
  including the new tests — saw exit 1 with empty stderr. Refusals now
  throw a ReleaseError caught at the entry point, which prints and sets
  process.exitCode = 1 so the process drains stdio on its own.
- The block-construct check missed thematic breaks with interior
  whitespace ('- - -') and headings used as item content ('- ## x').
  Both are now refused; content after the bullet marker is inspected.
  Six regression cases added, plus one guarding against over-refusal of
  bullets that merely resemble rules or headings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Anarchid

Copy link
Copy Markdown
Contributor Author

Round 2 of the Codex QA pass on membrane (antra-tess/membrane#49) — fixed here in 4e104c0: refusals reported via console.error + process.exit(1) lose their stderr when stderr is a pipe on macOS (async pipes; Linux pipes are sync), so refusals now throw and the entry point sets process.exitCode = 1 and returns normally; and the grammar missed spaced thematic breaks (- - -) and headings as bullet content (- ## x), both now refused (24 regression cases).

@Anarchid
Anarchid merged commit 9b7f3e3 into main Aug 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant