Files
msd-core/scripts/changeset
Tom Boucher bcf7b04864 chore(#2896): convert CONTEXT.md prose defect registry into enforced gates (#3325)
* chore(#2896): convert CONTEXT.md prose defect registry into enforced gates

Squashes the prior 4-commit sequence and fixes defects found while
resuming this branch: 5 orphaned/corrupted DEFECT fragment lines left
by an earlier botched edit, 17 "Source of truth: Memtrace `find_symbol`"
placeholders that had destroyed real file-path citations, and 3
DEFECT.GENERATIVE-* entries merged into one RULESET.GENERATIVE-FIX
predicate (policy, not an unenforced defect) to satisfy the zero
DEFECT.<NAME>.<field>= acceptance criterion.

Six mechanizable defects get real gates: DEFECT.UNBOUNDED-SUBPROCESS
(eslint-rules/require-subprocess-timeout.cjs), DEFECT.CANARY-VERSION-LEAK
(scripts/lint-canary-version-leak.cjs + version-gate.yml),
DEFECT.CHANGESET-PR-FIELD-DRIFT (findPrFieldDrift in changeset/lint.cjs),
DEFECT.FRONTMATTER-SCALAR-BROAD-GREP, DEFECT.REMOVED-BUT-NEEDED, and
DEFECT.DEFAULT-FLIP-DOCUMENTATION (new lint scripts, wired into lint:ci).
Already-enforced and unenforceable prose entries are deleted; the gate
is the record.

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

* chore(#2896): route the new lint tests' subprocess calls through the bounded process-seam helper

The 4 new test files for this PR's lint checks called cp.spawnSync/
execFileSync directly with no timeout, tripping this repo's own
existing local/no-unbounded-spawn ESLint rule. Route every one through
runNode/gitOrThrow (tests/helpers/process-seam.cjs,
tests/helpers/git-fixture.cjs) instead, matching the pattern already
used elsewhere in the suite (e.g. tests/changeset-lint.test.cjs).

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

* fix: register claude-orchestration.cjs and regenerate stale generated indexes

Pre-existing drift on next, unrelated to this PR's own change, surfaced
by running lint:ci as part of verifying #2896: two cli_modules
(claude-orchestration.cjs, write-set.cjs) landed without a manifest
regen, and CONTEXT.md's own edits in this PR staled its two generated
indexes. Adds the missing docs/INVENTORY.md row for
claude-orchestration.cjs (write-set.cjs already had one — only its
manifest entry was stale) and regenerates
docs/INVENTORY-MANIFEST.json, docs/CONTEXT-INDEX.json, and
examples/dynamic-context-management/CONTEXT-INDEX.json.

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

* fix(#2896): default-flip-documentation lint's local fallback base was main, not next

Found in review: every other base-ref fallback in this repo (see
scripts/changeset/lint.cjs's DEFAULT_BASE, #2988) defaults to `next`,
the integration branch every PR actually targets — `main` is the
release branch. This script's local fallback (used only when
GITHUB_BASE_REF is unset, i.e. never in CI, but potentially on a local
or direct invocation) diffed against the wrong ref. No test exercised
the unset-env-var path, so it shipped unnoticed; every e2e test sets
GITHUB_BASE_REF explicitly and is unaffected by this fix.

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

* fix(#2896): stale eslint comment, overclaiming CONTEXT.md wording, and an incompletely-regenerated manifest

Found by the isolated Standards code-review pass:
- eslint.config.mjs's require-subprocess-timeout comment said "'warn'
  for now... flip to 'error' once migrated" while the rule already
  shipped as 'error' with all 8 sites migrated in the same commit —
  described a state that never existed.
- The CONTEXT.md pointer block claimed the rule's bounded call sites
  "never throw", but roadmap-upgrade.cts's pre-mutation clean-tree
  check correctly still throws on failure (it gates a destructive
  real-run migration; degrading to "assume clean" would risk clobbering
  uncommitted work) — softened the claim to describe both shapes
  accurately instead of overclaiming one.
- docs/INVENTORY-MANIFEST.json's claude-orchestration.cjs/write-set.cjs
  entries from the prior "fix: register claude-orchestration.cjs..."
  commit didn't actually land — re-running the generator now includes
  them; lint:generated-sync is green.

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

* chore(#2896): backfill changeset pr field with the real PR number

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

* fix(#2896): normalize buildCorpus file paths to POSIX in lint-removed-but-needed

Windows CI caught it: path.relative(root, abs) returns backslash-
separated paths on Windows, but findSurvivingReferences's package-lock
special case does file.startsWith('.github/workflows') — a forward-
slash literal. On Windows the check silently never matched, so
tests/removed-but-needed-lint.test.cjs's real-defect-shape fixture got
exit 0 instead of the expected exit 1. Normalize at the production
source (RULESET.CONTENT-PATH-NORMALIZATION) rather than the test side.

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

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 12:55:52 -04:00
..

changeset/ — release-notes tooling

This directory holds the scripts that turn per-PR fragments in .changeset/ and git history into the project's CHANGELOG.md and GitHub release notes.

The entry point is cli.cjs. It exposes three subcommands:

Subcommand Purpose
render Render a single version's changelog section from consolidated data.
github-release-notes Build GitHub release-notes body for a ref range.
extract Pull existing CHANGELOG.md entries that fall in a version range.

The rest of this document specifies the extract contract, because it is the surface most likely to be called by external tooling (CI workflows, npm scripts, release automation) that needs a stable exit-code and output guarantee to code against.


cli.cjs extract

Extract the changelog entries for every release in a version range, reading from an existing CHANGELOG.md. The range is --from exclusive, --to inclusive.

node scripts/changeset/cli.cjs extract --from VERSION --to VERSION \
  [--changelog FILE] [--repo <dir>] [--json]

Flags

Flag Required Description
--from VERSION Yes Lower bound, exclusive — entries equal to --from are not returned.
--to VERSION Yes Upper bound, inclusive — entries equal to --to are returned.
--changelog FILE No Path to the changelog to read. Defaults to <repo>/CHANGELOG.md.
--repo <dir> No Repo root used to locate CHANGELOG.md when --changelog is omitted. Defaults to the current working directory.
--json No Emit the structured report as JSON instead of rendered markdown.

Version validation

Both --from and --to must be stable triplet semver — MAJOR.MINOR.PATCH, digits only.

  • A leading v is accepted and stripped: v1.42.0 is treated as 1.42.0.
  • Pre-release and build suffixes are rejected: 1.42.0-rc.1, 1.42.0+build, and partial versions like 1.42.x all fail validation and exit 1.

Strict validation is deliberate. Coercing a malformed bound such as 1.42.x to 1.42.0 would silently change which releases the range selects, so a malformed bound is rejected early with a structured error rather than guessed at.

Changelog entries that are themselves pre-release or non-semver (and the Unreleased section) are skipped during matching; a notice for each skipped entry is written to stderr.

Exit codes

extract resolves to one of three exit codes. The output shape depends on whether --json is passed.

Exit Meaning Default stdout --json stdout
0 One or more releases fall in the range. Rendered markdown for the matched releases. { "releases": [ ... ], "from": "...", "to": "..." }
1 Bad input: --from/--to is not stable semver, a required flag is missing, or the changelog file was not found. Nothing (a missing-flag error and usage go to stderr). { "error": "<message>", "releases": [] }
2 Bounds are valid but no release falls in the range. A no releases found in range notice on stderr. { "releases": [], "from": "...", "to": "..." }

Notes for callers:

  • Treat exit 2 as "empty range", not "failure". For a well-formed invocation it means the request was understood and simply matched nothing — do not surface it as an error. (At the argument-parsing layer, malformed argv such as an unknown flag also exits 2; pass well-formed arguments and this overlap does not arise.)
  • In default (text) mode, a failure is signalled by the exit code alone — exit 1 from invalid semver or a missing changelog writes nothing to stdout. Machine consumers should pass --json to receive the error field.

Output shape

With --json, the report is pretty-printed JSON. The releases array contains one object per matched release (version, date, and parsed sections); from and to echo the normalized bounds. On exit 1, releases is empty and an error string describes the failure.

Without --json, exit 0 prints the matched releases as markdown, ready to paste into release notes:

## [1.42.0] - 2026-01-15

### Added

- New `--json` flag on the extract command (#3796)

### Fixed

- Trailing-slash handling in config paths (#3651)

Examples

Extract everything released after 1.41.0 up to and including 1.42.0:

node scripts/changeset/cli.cjs extract --from 1.41.0 --to 1.42.0

The same range as structured JSON, reading an explicit changelog file:

node scripts/changeset/cli.cjs extract \
  --from v1.41.0 --to v1.42.0 \
  --changelog ./CHANGELOG.md --json

Handle the three outcomes in a shell consumer:

if out=$(node scripts/changeset/cli.cjs extract --from "$FROM" --to "$TO" --json); then
  echo "$out"            # exit 0 — releases found
else
  case $? in
    2) echo "no releases in range — nothing to publish" ;;  # not an error
    *) echo "extract failed: $out" >&2; exit 1 ;;            # exit 1 — bad input
  esac
fi