## Problem
PR #3472 resolved a `.changeset/pre.json` rebase conflict against a copy
predating the v0.76.0 stable:
| | at `v0.77.0-beta.1` | after #3472 |
|---|---|---|
| `initialVersions["@runfusion/fusion"]` | `0.76.0` | `0.75.1` |
| consumed ledger | 67 entries | 158 (the pre-0.76.0 cycle's) |
Nothing failed at PR time. Days later `pnpm release` saw the cycle
anchored below the shipped `v0.76.0`, fired its stale-cycle re-anchor
(`pre exit` → rewrite all 36 `package.json` → `pre enter`), and proposed
**`0.77.0-beta.0`** — below the already-published `0.77.0-beta.1`. The
re-anchor guard exists to stop a beta numbering under a stable; fed a
stale anchor it caused exactly that.
`pre.json` is generated by changesets, hand-edited by nobody, and
conflicts in nearly every long-lived branch — so a wrong resolution is
invisible until release day. This moves the failure to the PR that
causes it.
## The check
`scripts/check-pre-json-anchor.mjs`, three invariants:
- **`anchor-below-stable`** — `initialVersions` must not sit below the
newest `v*` stable tag. This is the exact predicate
`evaluateBetaCycleAnchor` keys on in `release.mjs`, so green here means
the release will *not* re-anchor.
- **`ledger-regression`** — the consumed ledger must stay a **superset**
of the last `chore(release):` commit's. Deliberately not a count test:
#3472's ledger *grew* 67 → 158 while dropping all 67 real entries, so a
size comparison would have passed it.
- **`dangling-ledger-entry`** — every consumed entry keeps its
`.changeset/*.md`, which pre-mode needs to aggregate notes into the
eventual stable release.
Skips cleanly outside pre-mode (the stable track deletes `pre.json`).
## Wiring
Added to the **Lint** job and `pretest`. Job names are unchanged, so no
branch-protection update is needed. The Lint checkout takes
`fetch-depth: 200` + `fetch-tags` rather than a full 486MB clone —
releases land every few days, so that always reaches a baseline; out of
range the ledger rule reports `SKIPPED` rather than passing vacuously,
and the two local rules still run.
## Verification
- Reproducing #3472's exact `pre.json` in the tree → **exit 1** on all
three rules, with the 67 dropped entries named.
- Clean `main` → exit 0.
- 11 unit tests (`scripts/__tests__/check-pre-json-anchor.test.mjs`),
including an explicit assertion that the ledger *grew* in the regression
case.
- `eslint` clean; workflow YAML parses; job names still `Lint,
Typecheck, Build, Gate`.
No changeset: CI config only, no `@runfusion/fusion` behavior change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added automated validation for beta release-cycle metadata.
* Checks anchor versions, consumed changeset records, and corresponding
changeset files.
* Provides clear success or error messages and skips checks when release
history is unavailable or not applicable.
* **Chores**
* Pull request checks now run the beta-cycle validation automatically.
* Added comprehensive coverage for valid, invalid, and skipped
validation scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>