* test(#4068): failing-first regression guard for c8 --merge-async flag Adds a config-invariant test asserting test:coverage:unit and test:coverage:report pass --merge-async to c8, guarding against the coverage-merge OOM (release.yml finalize dry-run, exit 134/SIGABRT) regressing a third time (prior stopgap: #199). Also commits the sourced research memo backing the diagnosis. * test(#4068): rename regression test to avoid lint-test-file-count collision coverage-merge-async-flag.test.cjs's effective prefix (coverage-merge- async-flag) matched the unrelated gsd-core/bin/lib/coverage.cjs module's 2-file cap under lint-test-file-count.cjs's startsWith bucketing, failing FAIL_EXCEEDS_LIMIT (confirmed via the RED gsd-test run on 7b6e6ca9e). Renamed to c8-merge-async-flag.test.cjs -- no colliding prefix. * fix(#4068): add --merge-async to c8 coverage-merge invocations The `test:coverage:unit` script OOM-crashed (exit 134, SIGABRT) in the release.yml finalize dry-run of 1.12.0: all 1785 unit tests pass, then c8's report/merge phase crashes ~76s later against the 6144 MB heap ceiling. Root cause (verified against this repo's pinned c8@11.0.0 source, node_modules/c8/lib/report.js): the default sync merge path, Report._getMergedProcessCov(), loads every raw V8 coverage file for the whole run into memory as one array before merging. This is a recurrence of #199 (same OOM class at ~466 tests, "fixed" by raising the heap ceiling) -- the suite outgrew 6144 MB as it grew to 1785 tests, and test.yml's own coverage-gate job already independently hit and stopgap-fixed the identical class once (test.yml:614-620, 4096->8192 MB). c8 ships the upstream fix for exactly this: --merge-async (c8 v7.14.0, already inside the pinned c8@^11.0.0 range -- no dependency bump), which switches to Report._getMergedProcessCovAsync(), reading and merging one raw file at a time instead of loading them all at once. The merge arithmetic (mergeProcessCovs()) and --all's zero-coverage-file inclusion are unchanged between the two paths, so the coverage gate's accuracy is unaffected. Verified empirically (not just by source inspection): against a 223 MB synthetic raw-coverage corpus derived from this repo's actual 235 gsd-core/bin/lib/**/*.cjs files (same order of magnitude as the 358 MB figure documented in test.yml), c8's real Report class crashes with the same "Ineffective mark-compacts near heap limit" signature via the sync path at a 300 MB heap ceiling, while the async path completes at a flat ~40-50 MB peak down to a 100 MB ceiling. Full repro steps and numbers: .gsd/bug/fix-4068-coverage-merge-oom/10-diagnosis.md. Only `test:coverage:unit` and `test:coverage:report` are changed. `test:coverage` (line 150) is a non-CI dev-convenience script, never invoked as a command by any workflow. `test:coverage:unit:raw` (line 153) already runs --reporter none, which skips the merge/report phase this flag affects, entirely -- adding it there would be a no-op. Fixes #4068 * fix(#4068): add changeset fragment for the coverage-merge OOM fix The changeset fragment was generated locally (npm run changeset) but never committed -- isolated code review caught the gap (no .changeset/*.md on the branch, confirmed via git diff --name-status). * chore(#4068): backfill changeset PR number pr:0 -> pr:4069 --------- Co-authored-by: sim <sim@local>
Changeset Fragments
This directory holds per-PR CHANGELOG fragments. Every PR with user-facing changes drops one (or more) <random-name>.md files here describing its CHANGELOG entry. Fragments are consolidated into the top-level CHANGELOG.md at release time.
Why
Two PRs that both edit the ### Fixed block of CHANGELOG.md always conflict on merge — git can't pick a serialization order without human input. Two PRs that each add a fresh .changeset/<unique-name>.md never conflict because they don't share lines.
See #2975 for the full rationale.
Adding a fragment
node scripts/changeset/new.cjs \
--type Fixed \
--pr 1234 \
--body "fix the thing — explain the user-visible change in one sentence"
This writes .changeset/<adjective>-<noun>-<noun>.md with frontmatter and a body. Three random words → concurrent PRs don't collide.
Format
---
type: Fixed
pr: 1234
---
**`/gsd-foo` no longer drops trailing slashes** — explain the user-visible change.
Allowed type: values follow Keep a Changelog: Added, Changed, Deprecated, Removed, Fixed, Security.
Opting out
PRs that legitimately have no user-facing impact can add the no-changelog label. CI honors it. When unsure, add the fragment.
At release time
Promotion is automatic. The release workflow's finalize job runs:
node scripts/changeset/cli.cjs render --version vX.Y.Z --date YYYY-MM-DD --allow-empty
This reads every fragment, groups bullets by type:, replaces ## [Unreleased] with a new ## [vX.Y.Z] - YYYY-MM-DD block, opens a fresh ## [Unreleased] above, and deletes consumed fragments. The --allow-empty flag ensures a no-change release still gets a dated heading (with a _No notable changes._ placeholder). A subsequent verify step confirms the promotion landed correctly. Maintainers do not run this by hand.
Archived fragments
.changeset/archived/ holds fragments for already-shipped releases (≤ 1.3.1), retained for provenance. Their content was hand-curated into the dated ## [1.x.y] sections of CHANGELOG.md during the #690 backfill — they were never consumed by render. All changeset tooling enumerates .changeset/ non-recursively, so archived fragments are never picked up or rendered. Do not move them back to the top level.