You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every PR that changes behavior adds its bullet at the same place: the line after ## Unreleased in CHANGELOG.md (origin/main 6debef0634, CHANGELOG.md:3). Two open PRs that each add a bullet
there conflict with each other. The first one to merge forces the other to rebase for a
CHANGELOG-only conflict, even when the code does not overlap at all.
A second defect lives in the same file. No release cuts ## Unreleased into a version section. ## Unreleased runs from line 3 to line 896 and holds 138 bullets. The next heading is ## 0.15.0
(CHANGELOG.md:897). Tags v0.15.1 through v0.21.12 each exist, and each release commit touches only package.json and server.json. For example, git show --stat v0.21.12 lists those two files
only. The release commit comes from npm version, which runs the version lifecycle script
(package.json:169, pnpm sync:mcp-metadata && git add server.json). So CHANGELOG.md cannot
tell which release shipped which change.
The fix is to have no shared insertion point. Each PR adds its own new file. The release version
commit, which already runs a hook on every release, folds those files into a version section.
Fragments alone do not close the bug class. After the migration, the top of CHANGELOG.md is ## 0.21.1x. docs/agents/pull-requests.md says nothing about CHANGELOG.md today, so agents
learn the habit from the file itself. An agent that keeps editing CHANGELOG.md adds its bullet at
the top of that released section. That is the same shared insertion point, so the conflicts come
back, and the bullet also names the wrong release. So the rule is: no pull_request diff
modifies CHANGELOG.md. Only the npm version release commit writes it. That commit is pushed
straight to main and is never a PR (for example 01328de4110.21.12 has no associated PR). CI
enforces the rule.
Required behavior
Fragment files
A PR with a user-visible change adds changelog.d/<slug>.md. <slug> matches ^[a-z0-9][a-z0-9-]*$. By convention it is <issue-number>-<short-kebab>, taken from the branch
name (for example changelog.d/2799-macos-fullscreen-surfaces.md). The PR number is not known
before the PR opens, so it is not required in the name.
changelog.d/README.md documents the format. It is the only non-fragment file in the directory,
and it keeps the directory in place when no fragments are left.
A fragment holds one or more bullets in the existing style. A bullet starts on a line that matches:
Continuation lines are indented by two spaces. Blank lines between bullets are allowed. Anything
else fails validation, including a leading non-bullet line or an unknown kind. Example:
- Fixed (macos): `screenshot --fullscreen` on the `desktop`, `menubar`, or `frontmost-app`
surface now refuses with `INVALID_ARGS` instead of being ignored. (#2849)
PRs never edit CHANGELOG.md. Only the release version commit writes to it. The guard below
fails any PR that does.
Guard: .github/workflows/changelog-guard.yml
A new workflow that fails every PR whose diff touches CHANGELOG.md:
name: Changelog Guardon:
pull_request:
paths:
- 'CHANGELOG.md'permissions:
contents: readjobs:
refuse:
name: PRs do not edit CHANGELOG.mdruns-on: ubuntu-latesttimeout-minutes: 2steps:
- name: Refuse CHANGELOG.md editrun: | echo "::error file=CHANGELOG.md::PRs never edit CHANGELOG.md. Add a changelog.d/<issue>-<slug>.md fragment instead; see changelog.d/README.md. The release version commit assembles CHANGELOG.md." exit 1
pull_request.paths runs the workflow when at least one changed file matches. So it catches a
PR that edits CHANGELOG.md together with code, and a PR that edits only CHANGELOG.md. The
six workflows that list CHANGELOG.md in paths-ignore skip a CHANGELOG-only PR, so the unit
gate never sees that case. This separate workflow does.
It needs no checkout, no base computation and no tags. The path filter already is the diff
against the PR base.
No push trigger. The release commit reaches main by push, so the guard never fires on it.
No escape hatch (no label, no allowlist). See Non-goals.
Assembler: scripts/changelog-release.ts
Run it with node --experimental-strip-types, the same way as the other scripts/*.ts tools. It
exports a pure core and has a thin CLI:
exporttypeChangelogFragment={name: string;text: string};// name = file basenameexportfunctionparseFragment(fragment: ChangelogFragment): {kind: Kind;text: string}[];// throws on invalidexportfunctionassembleChangelog(input: {changelog: string;// current CHANGELOG.mdfragments: ChangelogFragment[];version: string;// package.json version}): string;// new CHANGELOG.md
Ordering is deterministic and does not depend on input order. The assembler sorts bullets by
kind rank (Breaking, Removed, Changed, Deprecated, Added, Fixed, Security), then by fragment
name, then by position in the fragment. It keeps bullet text verbatim, minus trailing whitespace.
Insertion. It writes ## <version>\n\n<bullets>\n\n after the # Changelog title, before
the first ## heading.
Refusals. The assembler throws with a message that names the file, and makes no writes, in
these cases:
version contains -, so a -dev marker never becomes a heading.
CHANGELOG.md already contains ## <version>. This blocks consuming the same fragments twice.
CHANGELOG.md contains ## Unreleased.
A fragment is invalid.
No fragments. The file stays unchanged, and the script prints a notice and exits 0. A release
with no user-visible change gets no section.
CLI, default mode. It reads package.json version, CHANGELOG.md and changelog.d/*.md
(excluding README.md). It writes CHANGELOG.md and deletes the fragments it consumed.
CLI, --check mode. It exits 1 when any fragment is still in changelog.d/, which means a
release that skipped assembly. It also validates every fragment.
Release flow integration (package.json)
"version": "pnpm sync:mcp-metadata && node --experimental-strip-types scripts/changelog-release.ts && git add server.json CHANGELOG.md changelog.d". npm version stages files that the hook adds, so the release commit contains the new section and
the fragment deletions. git add changelog.d stages the deletions of tracked fragments.
release:prepare gets node --experimental-strip-types scripts/changelog-release.ts --check &&
right after the existing release-mark-dev.mjs --check-release-version step.
scripts/release-mark-dev.mjs does not change. It writes package.json directly and does not
run npm version, so it never consumes fragments.
One-time migration of the current ## Unreleased block
Recompute this at your base. At 6debef0634 the latest tag is v0.21.12.
Bullets added after the latest tag (git diff <latest-tag> origin/main -- CHANGELOG.md; 12
bullets and 119 lines at 6debef0634) move verbatim into one fragment per PR. Name each fragment
after the PR number in the bullet or commit.
The remaining bullets stay in CHANGELOG.md under ## 0.15.1 – <latest-tag-version>, followed by one line: "These releases did not split the
changelog per version." Do not try to attribute them per version.
After the migration, no ## Unreleased heading remains.
CI and docs
Add 'changelog.d/**' to pull_request.paths-ignore in the six workflows that already ignore CHANGELOG.md: .github/workflows/{ci,size,ios,android,linux,macos}.yml, line 10 in each.
test/ci/root-docs-paths-ignore.test.ts: in the same loop, assert that a sample fragment path
(changelog.d/2799-example.md) is ignored by all six workflows. Update the header comment.
scripts/check-affected/model.ts needs no change, because isDocs (line 218) already classifies changelog.d/*.md as docs.
docs/agents/pull-requests.md: add one rule. A user-visible change adds a changelog.d/<issue>-<slug>.md fragment. PRs never edit CHANGELOG.md; the Changelog Guard
workflow fails a PR that does. Stay within pnpm check:agent-guidance budgets.
changelog.d/README.md states the same rule, because the guard's error message points there.
website/docs/docs/migrating-gestures.md:170: change "recorded in CHANGELOG.md under Unreleased" to "recorded in a changelog.d/ fragment".
CONTRIBUTING.md: add a two-line pointer to changelog.d/README.md, next to the release/version
section (around line 80).
Delivery order
Two PRs, in this order. The migration PR must edit CHANGELOG.md once, and the guard runs from
the PR's own merge ref, so a guard in the same PR fails that PR.
Guard PR:changelog-guard.yml, its test, and the guard sentence in docs/agents/pull-requests.md and changelog.d/README.md. It does not touch CHANGELOG.md,
so it is green on its own run. Open it right after the migration PR merges.
Completion conditions
test/ci/changelog-guard.test.ts (vitest, added to the explicit unit-core include list in vitest.config.ts next to test/ci/root-docs-paths-ignore.test.ts, line 172). It parses the
real .github/workflows/changelog-guard.yml with yaml, and handles the YAML 1.1 true key for on the same way root-docs-paths-ignore.test.ts does. It asserts:
The triggers are exactly pull_request. There is no push trigger, so the release commit on
main is never refused.
pull_request.paths matches CHANGELOG.md through matchesGlob from scripts/gate/workflows.ts, does not match changelog.d/2799-example.md, and there is no paths-ignore.
The job has a step whose run ends in exit 1 and contains changelog.d/README.md.
Mutation evidence in the Guard PR body: add push: or drop CHANGELOG.md from paths, and
this test fails.
Manual proof in the Guard PR body: a throwaway draft PR that adds one line to CHANGELOG.md
gets a failing Changelog Guard check with the pointer message. Close it after.
pnpm check:gate-manifest stays green with the new workflow. If it asks for a declaration, add
it at the declaration it names. Do not add an allowlist.
scripts/__tests__/changelog-release.test.ts (vitest, added to the explicit unit-core include
list in vitest.config.ts) covers the following:
Repository guard: the real root CHANGELOG.md has no ## Unreleased heading. This case
fails on 6debef0634. Every file in the real changelog.d/ other than README.md has a valid
name and passes parseFragment. A PR that adds a malformed fragment fails the unit-ci gate
on any PR that runs CI. This case is the content check for the push-to-main CI run, which has
no paths-ignore and so also checks what a release commit wrote. The rule that a PR does not
write CHANGELOG.md at all belongs to the Changelog Guard, not to this case.
Determinism: a fixed fragment set in two different input orders gives byte-identical output.
The expected output is a literal in the test, not derived from the implementation.
Kind order: a Fixed fragment named a-… sorts after a Breaking fragment named z-….
Refusals: each of the four refusal cases throws, and the output is unchanged.
No fragments: the input comes back unchanged.
CLI in a temporary directory: the default mode writes the section and deletes the consumed
fragments. --check exits 1 while one fragment remains and 0 when only README.md is left.
src/__tests__/npm-package-scripts.test.ts asserts the new version script string and the --check step in release:prepare. The existing exact-string assertion for release:prepare
(line 76) is updated, not removed.
test/ci/root-docs-paths-ignore.test.ts fails if any of the six workflows drops changelog.d/**.
Mutation evidence in the Migration PR body: delete the kind sort and the determinism test fails. Remove the ## <version> refusal and the double-run test fails.
pnpm check:affected --run is green on each PR. Run pnpm check:fallow --base origin/main. If fallow
reports the new script as an unused entry, add it to .fallowrc.jsonentry in the final chore(gates) commit.
Manual proof in the Migration PR body, in a scratch clone: npm version patch --no-git-tag-version with
two fragments present gives a ## x.y.z section in kind order and removes both fragments. This
is docs/tooling only, so no device run is needed.
Non-goals
No change to GitHub release notes. They stay auto-generated from PR titles.
No attribution of the 126 legacy bullets to individual versions.
No escape hatch in the Changelog Guard (no label, no allowlist). A correction to an already
released section is rare. It rides in the next release commit, or a maintainer merges over the
red check on purpose. Main has no required status checks, so the guard reports and does not
block a maintainer.
No per-PR "missing changelog fragment" check. Whether a change is user-visible stays a review
decision.
No .gitattributesmerge=union. Rejected: GitHub's mergeability check does not use repository
merge drivers, so PRs still show as conflicted. Union also silently mixes two edits to the same
existing bullet.
No third-party changelog tool (changesets and similar). It would add a dependency and a second
version-bump flow next to npm version and release:mark-dev.
No ADR. This is release tooling, and no architectural boundary moves. ADR 0019 and 0027, the R9
layering scan and the eager-closure budgets are unaffected, because the script is outside src/, is not bundled, and no package entry imports it.
Related: scripts/release-mark-dev.mjs and the "version on main never equals a published version"
rule (CONTRIBUTING.md:80). This work runs next to it and does not change it.
Coordination: every PR in flight that edits CHANGELOG.md conflicts with the Migration PR once.
Land it at a quiet point, then move those PRs' bullets into fragments when they rebase. After the
Guard PR merges, any PR that still edits CHANGELOG.md gets a failing Changelog Guard check that
points to changelog.d/README.md.
Estimate
Two PRs (see Delivery order), about ½ to 1 day in total for a Sonnet-class agent.
Net production lines: about 90, all in scripts/changelog-release.ts. There are 0 runtime src/
lines, and no runtime lines are removed.
Migration PR gross diff: about 450 to 550 lines. That is the script (about 90) and its test
(about 130); moved legacy bullets (about 120 removed from CHANGELOG.md and about 120 added as
fragments); and about 40 lines of workflow, test, package.json and doc edits. It fits the
1,000-line budget.
Guard PR gross diff: about 60 lines. That is the workflow (about 20), its test (about 35), the vitest.config.ts include line, and two doc sentences.
Purpose
Every PR that changes behavior adds its bullet at the same place: the line after
## UnreleasedinCHANGELOG.md(origin/main6debef0634,CHANGELOG.md:3). Two open PRs that each add a bulletthere conflict with each other. The first one to merge forces the other to rebase for a
CHANGELOG-only conflict, even when the code does not overlap at all.
Evidence (checked at
6debef0634):01328de411..6debef0634), 12 of 57 commits touchCHANGELOG.md, and eachadds its bullet at the top of
## Unreleased. Examples: fix(selectors): retire the stored tree after a side effect or a native read #2869, fix(apple-runner): resend a read only on the runner's busy refusal #2863, fix(ios): cut modal-contained presentations from bridge snapshots #2850, fix(ios): never launch a not-running session app from an observation #2852, fix(interaction): report an unsettled post-gesture surface instead of a definite miss #2839,fix(macos): refuse --fullscreen on every helper-captured surface #2849, fix(ios): keep alert activation out of XCTest's interruption handling #2843, fix(ios): let a Simulator open wait out a slow app discovery before observing the launch #2838, refactor(cli): drop the inert --device-hub option #2821, fix(web): refuse the press shapes a browser click cannot drive #2806, fix(ios): derive hittable from geometric actionability on the AX bridge #2775, fix(ios): derive selected from the AX bridge traits word #2768. Since 2026-08-24, 85 of 534 commits on main touch
the file.
last week record
CONFLICT (content): Merge conflict in CHANGELOG.mdabout 30 times, across morethan 10 separate agent runs. That makes it the file that conflicts most often. The most frequent
code file (
packages/platform-apple/src/deployment/runtime.ts) conflicts 5 times. Branches thathit it include
fix/2799-macos-fullscreen-surfaces(fix(macos): refuse --fullscreen on every helper-captured surface #2849),fix/2785-physical-install-hints(fix(ios): keep devicectl hints on physical install and delete the dead install paths #2857),
fix/2796-runtime-clang-no-werror(fix(ios): drop -Werror from runtime clang builds and cache the fold helper build #2858),fix/2788-synthesized-tap-fallback(fix(ios-runner): one synthesize-then-fallback helper; sequence taps use the standalone tap policy #2865)and the fix(ios): never launch a not-running session app from an observation #2852 rebase. The resolution is always the same: keep both bullets.
A second defect lives in the same file. No release cuts
## Unreleasedinto a version section.## Unreleasedruns from line 3 to line 896 and holds 138 bullets. The next heading is## 0.15.0(
CHANGELOG.md:897). Tags v0.15.1 through v0.21.12 each exist, and each release commit touches onlypackage.jsonandserver.json. For example,git show --stat v0.21.12lists those two filesonly. The release commit comes from
npm version, which runs theversionlifecycle script(
package.json:169,pnpm sync:mcp-metadata && git add server.json). SoCHANGELOG.mdcannottell which release shipped which change.
The fix is to have no shared insertion point. Each PR adds its own new file. The release version
commit, which already runs a hook on every release, folds those files into a version section.
Fragments alone do not close the bug class. After the migration, the top of
CHANGELOG.mdis## 0.21.1x.docs/agents/pull-requests.mdsays nothing aboutCHANGELOG.mdtoday, so agentslearn the habit from the file itself. An agent that keeps editing
CHANGELOG.mdadds its bullet atthe top of that released section. That is the same shared insertion point, so the conflicts come
back, and the bullet also names the wrong release. So the rule is: no
pull_requestdiffmodifies
CHANGELOG.md. Only thenpm versionrelease commit writes it. That commit is pushedstraight to main and is never a PR (for example
01328de4110.21.12has no associated PR). CIenforces the rule.
Required behavior
Fragment files
A PR with a user-visible change adds
changelog.d/<slug>.md.<slug>matches^[a-z0-9][a-z0-9-]*$. By convention it is<issue-number>-<short-kebab>, taken from the branchname (for example
changelog.d/2799-macos-fullscreen-surfaces.md). The PR number is not knownbefore the PR opens, so it is not required in the name.
changelog.d/README.mddocuments the format. It is the only non-fragment file in the directory,and it keeps the directory in place when no fragments are left.
A fragment holds one or more bullets in the existing style. A bullet starts on a line that matches:
Continuation lines are indented by two spaces. Blank lines between bullets are allowed. Anything
else fails validation, including a leading non-bullet line or an unknown kind. Example:
PRs never edit
CHANGELOG.md. Only the release version commit writes to it. The guard belowfails any PR that does.
Guard:
.github/workflows/changelog-guard.ymlA new workflow that fails every PR whose diff touches
CHANGELOG.md:pull_request.pathsruns the workflow when at least one changed file matches. So it catches aPR that edits
CHANGELOG.mdtogether with code, and a PR that edits onlyCHANGELOG.md. Thesix workflows that list
CHANGELOG.mdinpaths-ignoreskip a CHANGELOG-only PR, so the unitgate never sees that case. This separate workflow does.
against the PR base.
pushtrigger. The release commit reaches main bypush, so the guard never fires on it.Assembler:
scripts/changelog-release.tsRun it with
node --experimental-strip-types, the same way as the otherscripts/*.tstools. Itexports a pure core and has a thin CLI:
kind rank (Breaking, Removed, Changed, Deprecated, Added, Fixed, Security), then by fragment
name, then by position in the fragment. It keeps bullet text verbatim, minus trailing whitespace.
## <version>\n\n<bullets>\n\nafter the# Changelogtitle, beforethe first
##heading.these cases:
versioncontains-, so a-devmarker never becomes a heading.CHANGELOG.mdalready contains## <version>. This blocks consuming the same fragments twice.CHANGELOG.mdcontains## Unreleased.with no user-visible change gets no section.
package.jsonversion,CHANGELOG.mdandchangelog.d/*.md(excluding
README.md). It writesCHANGELOG.mdand deletes the fragments it consumed.--checkmode. It exits 1 when any fragment is still inchangelog.d/, which means arelease that skipped assembly. It also validates every fragment.
Release flow integration (
package.json)"version": "pnpm sync:mcp-metadata && node --experimental-strip-types scripts/changelog-release.ts && git add server.json CHANGELOG.md changelog.d".npm versionstages files that the hook adds, so the release commit contains the new section andthe fragment deletions.
git add changelog.dstages the deletions of tracked fragments.release:preparegetsnode --experimental-strip-types scripts/changelog-release.ts --check &&right after the existing
release-mark-dev.mjs --check-release-versionstep.scripts/release-mark-dev.mjsdoes not change. It writespackage.jsondirectly and does notrun
npm version, so it never consumes fragments.One-time migration of the current
## UnreleasedblockRecompute this at your base. At
6debef0634the latest tag isv0.21.12.git diff <latest-tag> origin/main -- CHANGELOG.md; 12bullets and 119 lines at
6debef0634) move verbatim into one fragment per PR. Name each fragmentafter the PR number in the bullet or commit.
CHANGELOG.mdunder## 0.15.1 – <latest-tag-version>, followed by one line: "These releases did not split thechangelog per version." Do not try to attribute them per version.
## Unreleasedheading remains.CI and docs
'changelog.d/**'topull_request.paths-ignorein the six workflows that already ignoreCHANGELOG.md:.github/workflows/{ci,size,ios,android,linux,macos}.yml, line 10 in each.test/ci/root-docs-paths-ignore.test.ts: in the same loop, assert that a sample fragment path(
changelog.d/2799-example.md) is ignored by all six workflows. Update the header comment.scripts/check-affected/model.tsneeds no change, becauseisDocs(line 218) already classifieschangelog.d/*.mdas docs.docs/agents/pull-requests.md: add one rule. A user-visible change adds achangelog.d/<issue>-<slug>.mdfragment. PRs never editCHANGELOG.md; the Changelog Guardworkflow fails a PR that does. Stay within
pnpm check:agent-guidancebudgets.changelog.d/README.mdstates the same rule, because the guard's error message points there.website/docs/docs/migrating-gestures.md:170: change "recorded inCHANGELOG.mdunderUnreleased" to "recorded in achangelog.d/fragment".CONTRIBUTING.md: add a two-line pointer tochangelog.d/README.md, next to the release/versionsection (around line 80).
Delivery order
Two PRs, in this order. The migration PR must edit
CHANGELOG.mdonce, and the guard runs fromthe PR's own merge ref, so a guard in the same PR fails that PR.
changelog-guard.yml, its test, and the guard sentence indocs/agents/pull-requests.mdandchangelog.d/README.md. It does not touchCHANGELOG.md,so it is green on its own run. Open it right after the migration PR merges.
Completion conditions
test/ci/changelog-guard.test.ts(vitest, added to the explicitunit-coreinclude list invitest.config.tsnext totest/ci/root-docs-paths-ignore.test.ts, line 172). It parses thereal
.github/workflows/changelog-guard.ymlwithyaml, and handles the YAML 1.1truekey foronthe same wayroot-docs-paths-ignore.test.tsdoes. It asserts:pull_request. There is nopushtrigger, so the release commit onmain is never refused.
pull_request.pathsmatchesCHANGELOG.mdthroughmatchesGlobfromscripts/gate/workflows.ts, does not matchchangelog.d/2799-example.md, and there is nopaths-ignore.runends inexit 1and containschangelog.d/README.md.push:or dropCHANGELOG.mdfrompaths, andthis test fails.
CHANGELOG.mdgets a failing Changelog Guard check with the pointer message. Close it after.
pnpm check:gate-manifeststays green with the new workflow. If it asks for a declaration, addit at the declaration it names. Do not add an allowlist.
scripts/__tests__/changelog-release.test.ts(vitest, added to the explicitunit-coreincludelist in
vitest.config.ts) covers the following:CHANGELOG.mdhas no## Unreleasedheading. This casefails on
6debef0634. Every file in the realchangelog.d/other thanREADME.mdhas a validname and passes
parseFragment. A PR that adds a malformed fragment fails theunit-cigateon any PR that runs CI. This case is the content check for the push-to-main CI run, which has
no
paths-ignoreand so also checks what a release commit wrote. The rule that a PR does notwrite
CHANGELOG.mdat all belongs to the Changelog Guard, not to this case.The expected output is a literal in the test, not derived from the implementation.
Fixedfragment nameda-…sorts after aBreakingfragment namedz-….fragments.
--checkexits 1 while one fragment remains and 0 when onlyREADME.mdis left.src/__tests__/npm-package-scripts.test.tsasserts the newversionscript string and the--checkstep inrelease:prepare. The existing exact-string assertion forrelease:prepare(line 76) is updated, not removed.
test/ci/root-docs-paths-ignore.test.tsfails if any of the six workflows dropschangelog.d/**.## <version>refusal and the double-run test fails.pnpm check:affected --runis green on each PR. Runpnpm check:fallow --base origin/main. If fallowreports the new script as an unused entry, add it to
.fallowrc.jsonentryin the finalchore(gates)commit.npm version patch --no-git-tag-versionwithtwo fragments present gives a
## x.y.zsection in kind order and removes both fragments. Thisis docs/tooling only, so no device run is needed.
Non-goals
released section is rare. It rides in the next release commit, or a maintainer merges over the
red check on purpose. Main has no required status checks, so the guard reports and does not
block a maintainer.
decision.
.gitattributesmerge=union. Rejected: GitHub's mergeability check does not use repositorymerge drivers, so PRs still show as conflicted. Union also silently mixes two edits to the same
existing bullet.
version-bump flow next to
npm versionandrelease:mark-dev.layering scan and the eager-closure budgets are unaffected, because the script is outside
src/, is not bundled, and no package entry imports it.Dependencies/related
scripts/release-mark-dev.mjsand the "version on main never equals a published version"rule (CONTRIBUTING.md:80). This work runs next to it and does not change it.
CHANGELOG.mdconflicts with the Migration PR once.Land it at a quiet point, then move those PRs' bullets into fragments when they rebase. After the
Guard PR merges, any PR that still edits
CHANGELOG.mdgets a failing Changelog Guard check thatpoints to
changelog.d/README.md.Estimate
scripts/changelog-release.ts. There are 0 runtimesrc/lines, and no runtime lines are removed.
(about 130); moved legacy bullets (about 120 removed from
CHANGELOG.mdand about 120 added asfragments); and about 40 lines of workflow, test, package.json and doc edits. It fits the
1,000-line budget.
vitest.config.tsinclude line, and two doc sentences.