From 9abce0d29491a6c67d34aa4966fb712723e07217 Mon Sep 17 00:00:00 2001 From: "MK (fengmk2)" Date: Fri, 30 Oct 2026 13:22:43 +0900 Subject: [PATCH 01/10] docs(rfc): align the core version with the bundled Vite version Propose a version scheme for @voidzero-dev/vite-plus-core that encodes the bundled Vite version: the core major and minor equal Vite's, and the core patch is `vitePatch * 100 + n`. Vite+ prereleases use a `vp.` identifier. With this scheme, peer checks against the `vite` alias pass for compatible consumers and still fail for incompatible ones, so migration can drop the broad pnpm `vite` peer rule. Refs #2806, #3277 Co-Authored-By: Claude Opus 5.5 --- rfcs/core-version-alignment.md | 590 +++++++++++++++++++++++++++++++++ 1 file changed, 590 insertions(+) create mode 100644 rfcs/core-version-alignment.md diff --git a/rfcs/core-version-alignment.md b/rfcs/core-version-alignment.md new file mode 100644 index 0000000000..92310f4abc --- /dev/null +++ b/rfcs/core-version-alignment.md @@ -0,0 +1,590 @@ +# RFC: Align the Core Package Version with the Bundled Vite Version + +- Status: Proposed +- Issue: [#2806](https://github.com/voidzero-dev/vite-plus/issues/2806) +- Related: [Q4 2026 plan](https://github.com/voidzero-dev/vite-plus/issues/3277), + [Vite core module identity](./vite-core-module-identity.md), + [Core binding resolution](./core-binding-resolution.md), + [Migration rules](../docs/guide/migrate-rules.md), + [Core package bundling](../packages/core/BUNDLING.md) + +## Proposal + +Derive the version of `@voidzero-dev/vite-plus-core` from the Vite version it +bundles. The core major and minor equal the bundled Vite major and minor. The +core patch encodes the bundled Vite patch and a release counter: + +```text +stable Vite+ release: .. +Vite+ prerelease, stable Vite: -vp. +Vite+ prerelease, Vite pre: .vp. +``` + +`n` counts the stable Vite+ releases that bundle the same Vite patch, from 0 +to 98. `n = 99` is reserved for local and CI builds. `k` counts the Vite+ +prereleases on the same base, from 0. Decoding is exact: the bundled Vite +patch is `floor(core patch / 100)`. + +| Vite+ release | Bundled Vite | Core version | +| ----------------- | -------------- | ------------------- | +| `1.5.0` (first) | `8.3.5` | `8.3.500` | +| `1.5.1` | `8.3.5` | `8.3.501` | +| `1.6.0` | `8.3.6` | `8.3.600` | +| `1.7.0-rc.0` | `8.4.0` | `8.4.0-vp.0` | +| `1.7.0-rc.1` | `8.4.0` | `8.4.0-vp.1` | +| `1.7.0` | `8.4.0` | `8.4.0` | +| `1.7.1` | `8.4.1` | `8.4.100` | +| `2.0.0-beta.0` | `9.0.0-beta.3` | `9.0.0-beta.3.vp.0` | +| `2.0.0-beta.1` | `9.0.0-beta.3` | `9.0.0-beta.3.vp.1` | +| Local or CI build | `8.3.5` | `8.3.599` | + +`vite-plus` keeps its own version line (`1.x` today). The CLI keeps its exact +alias dependency `vite: npm:@voidzero-dev/vite-plus-core@`. +Projects keep the same alias in their manifests, overrides, catalogs, and +resolutions. `vp migrate` stops adding `vite` to pnpm `peerDependencyRules` +and removes the entry that earlier migrations wrote. + +## Problem + +A migrated project aliases `vite` to the core package: + +```json +{ + "devDependencies": { + "vite": "npm:@voidzero-dev/vite-plus-core@1.4.0", + "vite-plus": "1.4.0" + } +} +``` + +Package managers resolve the alias and read the target's manifest. They +compare its `version` (`1.4.0`) with each consumer's `vite` peer range. Every +current plugin range fails: `vitest@5.0.5` declares +`^6.4.0 || ^7.0.0 || ^8.0.0`, `@vitejs/plugin-react@6.1.2` declares `^8.0.0`, +`@tanstack/react-start` declares `>=8.0.0`. + +pnpm users need `peerDependencyRules.allowAny: [vite]` and +`allowedVersions: { vite: '*' }`. `vp migrate` writes these rules in +`packages/cli/src/migration/migrator/vite-plus-bootstrap.ts` +(`ensurePnpmPeerDependencyRules`), `catalog.ts` (`rewritePnpmWorkspaceYaml`), +and `orchestrators.ts`. The rules also hide real incompatibilities: a plugin +that declares `vite: ^7.0.0` installs without a warning. + +npm has no per-package suppression. The `vite-plus` package alone is +inconsistent from npm's point of view: it depends on +`vite: npm:@voidzero-dev/vite-plus-core@1.4.0` and on `vitest`, whose peer +range rejects `1.4.0`. Every `npm install` prints +`ERESOLVE overriding peer dependency` warnings, and `npm ls vite` marks the +alias `invalid`. The only workaround is `legacy-peer-deps=true`, which changes +peer resolution for the whole tree. + +The release pipeline sets one version on every package. +`.github/workflows/prepare_release.yml` runs `update_json` on +`packages/cli/package.json` and `packages/core/package.json` with the same +`VERSION`. `.github/workflows/release.yml` publishes +`@voidzero-dev/vite-plus-core@${VERSION}`, and +`.github/workflows/reusable-release-build.yml` stamps `${VERSION}` into both +manifests before the build. + +## Goals and scope + +For a project whose bundled Vite satisfies a consumer's `vite` peer range, +the package manager must report the peer as satisfied. For a consumer whose +range excludes the bundled Vite major or minor, the package manager must still +report it. Patch-level floors must resolve correctly up to the bundled patch. + +Migration must stop writing the broad `vite` peer rule and must remove the one +it wrote earlier, without touching rules that users added for other packages. + +The core version must be derivable from the bundled Vite version, and the +bundled Vite version from the core version, without a lookup table. CI must +check that the committed core version encodes `bundledVersions.vite`. + +Out of scope: + +- The `vitest` entries in the same peer rules. `vitest` is pinned to a real + upstream version (`VITEST_VERSION` in `packages/cli/src/utils/constants.ts`), + so peer checks against it are already accurate. The `vitest` rule hides + real incompatibilities in the same way; removing it is a separate change. +- The `vite-plus` version policy. This RFC does not decide whether a Vite + major upgrade is a `vite-plus` major release. +- The registry bridge for preview builds. It assigns `0.0.0-commit.` to + every workspace package and is a separate repository. + +## Design + +### Version scheme + +The scheme must make four range shapes resolve like the bundled Vite: +`^MAJOR.0.0`, `^MAJOR.MINOR.0`, `~MAJOR.MINOR.0`, and `>=MAJOR.MINOR.PATCH`. +Three options were compared against bundled Vite `8.3.5` with `vite-plus` +`1.4.0`: + +- Option A: core major = Vite major; minor and patch follow `vite-plus` + (`8.4.0`). +- Option B: core major and minor = Vite major and minor; patch is a plain + counter per Vite+ release (`8.3.0`, `8.3.1`, ...). +- Option B+ (selected): like B, but the patch encodes the Vite patch + (`8.3.500`, `8.3.501`, ...). +- Option C: core = exact Vite version plus a prerelease tag + (`8.3.5-vp.1.4.0`) or build metadata (`8.3.5+vp.1.4.0`). + +| Consumer range | Bundled `8.3.5` | A `8.4.0` | B `8.3.0` | B+ `8.3.500` | C `8.3.5-vp.1.4.0` (npm) | +| -------------- | --------------- | ------------ | ------------ | ------------ | ------------------------ | +| `^8.0.0` | pass | pass | pass | pass | fail (wrong) | +| `^8.3.0` | pass | pass | pass | pass | fail (wrong) | +| `>=8.3.2` | pass | pass | fail (wrong) | pass | fail (wrong) | +| `~8.3.0` | pass | fail (wrong) | pass | pass | fail (wrong) | +| `^7.0.0` | fail | fail | fail | fail | fail | +| `^8.4.0` | fail | pass (wrong) | fail | fail | fail | +| `>=8.3.6` | fail | pass (wrong) | fail | pass (wrong) | fail | + +Option A fails on minor ranges in both directions. The `vite-plus` minor and +the Vite minor drift apart: `v1.2.0` bundled Vite `8.3.4`, so core `8.2.0` +would reject `^8.3.0` consumers, and a later `vite-plus` `1.9.x` on Vite +`8.5.x` would accept `>=8.6.0` consumers. Both are the failure the RFC exists +to remove. + +Option B resolves minor ranges correctly. Its patch carries no information, so +a `>=8.3.2` consumer fails against `8.3.0` or `8.3.1` although the bundled +`8.3.5` satisfies it. A consumer `>=8.3.6` passes when the counter reaches +the value 6. Patch floors are less common than minor floors, but they exist, +and the counter value is unrelated to the real patch in both directions. + +Option C fails the main goal. npm's semver compares a prerelease only against +comparators with the same `major.minor.patch` tuple, so `8.3.5-vp.1.4.0` +fails `^8.0.0`, `>=8.0.0`, and `^8.3.0`. npm users keep every warning from +#2806. The experiment below confirms this behavior for npm and shows that pnpm +and Yarn are more lenient. Build metadata does not help: npm treats +`8.3.5+a` and `8.3.5+b` as the same version, so a second Vite+ release on the +same Vite patch cannot publish. `v1.2.0` and `v1.3.0` both bundled Vite +`8.3.4`, so that case is real. + +Option B+ is the selected scheme. Its residual false results are limited to +patch floors above the bundled patch inside the same minor (`>=8.3.6` passes +against `8.3.500`) and to exact pins (`8.3.5` fails against `8.3.500`). Both +are rare in `vite` peer ranges. Vite+ follows Vite patch releases within days, +so the window for the first case is short. + +The counter supports 99 stable Vite+ releases per Vite patch (`n` from 0 to +98). `n = 99` is reserved for local and CI builds (see Local and CI builds). +Vite+ shipped five minor releases between 2026-09-28 and 2026-10-23 while Vite +moved from `8.3.1` to `8.3.5`. Two releases on one Vite patch happened once +(`v1.2.0` and `v1.3.0` on `8.3.4`). + +Core versions keep sorting correctly: +`8.3.501 < 8.3.502-vp.0 < 8.3.502 < 8.3.600`. + +### Prereleases and new Vite majors + +A Vite+ prerelease does not copy its own prerelease identifiers into the core +version. A copied suffix has two problems: + +- Two Vite+ prereleases can have the same identifiers, for example + `1.7.0-rc.0` and `1.8.0-rc.0`. If no stable release ships between them on + the same Vite, both map to `8.4.0-rc.0`, and the second publish fails. +- On a stable Vite, a copied suffix looks like a Vite prerelease and can sort + out of order. A Vite+ RC on Vite `9.0.0` would publish `9.0.0-rc.0`, which + sorts below a core built on Vite `9.0.0-rc.1`. + +Every Vite+ prerelease uses the identifier `vp` and a counter `k` instead: + +- On a stable Vite `X.Y.Z`, the base is the next stable core version, which + is the version that the next stable release on that Vite would get. The core + version is that base with `-vp.` appended. `k` counts the published core + prereleases with the same base, from 0. Vite+ `1.7.0-rc.0` and `1.7.0-rc.1` + on Vite `8.4.0` publish `8.4.0-vp.0` and `8.4.0-vp.1`. A later `1.8.0-rc.0` + on the same Vite, with no stable release between them, publishes + `8.4.0-vp.2`. The next stable release publishes `8.4.0`. +- On a Vite prerelease `X.Y.Z-
`, the core version is the Vite version
+  with `.vp.` appended. `k` counts the published core prereleases for that
+  Vite prerelease. Vite `9.0.0-beta.3` gives `9.0.0-beta.3.vp.0`, then
+  `9.0.0-beta.3.vp.1`.
+
+The `vite-plus` prerelease identifiers do not appear in the core version. The
+exact alias in the published CLI manifest links the two versions, as it does
+for stable releases (see Relation to the `vite-plus` version).
+
+The identifier `vp` sorts after `alpha`, `beta`, and `rc`, so every core
+version is unique and in publish order. `semver@7.8.5` gives this order:
+
+```text
+9.0.0-beta.3 < 9.0.0-beta.3.vp.0 < 9.0.0-beta.3.vp.1 < 9.0.0-beta.4
+  < 9.0.0-rc.1.vp.0 < 9.0.0-vp.0 < 9.0.0-vp.1 < 9.0.0
+  < 9.0.100-vp.0 < 9.0.100
+```
+
+With `semver@7.8.5` and npm's strict prerelease rules, `9.0.0-beta.3.vp.0`
+satisfies `^9.0.0-0` and `^9.0.0-beta.1`. This form was not tested in pnpm,
+Yarn, or Bun.
+
+A core prerelease on a stable Vite still fails strict peer checks in npm:
+`8.4.0-vp.0` does not satisfy `^8.0.0`. Prereleases are opt-in, so this RFC
+accepts the warnings for that period. Migration keeps the pnpm peer rule for
+prerelease core versions (see Migration).
+
+A Vite+ stable release bundles a stable Vite. Only a Vite+ prerelease may
+bundle a Vite prerelease. CI rejects a core prerelease on a `vite-plus`
+version without a prerelease identifier, and a stable core on a `vite-plus`
+prerelease.
+
+Since `v1.0.0`, Vite+ published no prereleases. The last ones are
+`v1.0.0-rc.0` and `v1.0.0-rc.1`. The prerelease rule matters mostly for the
+Vite 9 beta period.
+
+A new Vite major gives a new core major (`9.0.0` for Vite `9.0.0`). The
+`vite-plus` version does not follow. A Vite major upgrade is a breaking change
+for user plugins, so a `vite-plus` major release is the expected choice, but
+that decision stays with the release policy.
+
+### Relation to the `vite-plus` version
+
+`vite-plus` stays on its own version line. The two versions are linked by the
+exact alias in the published CLI manifest. pnpm already rewrites
+`vite: workspace:@voidzero-dev/vite-plus-core@*` in
+`packages/cli/package.json` to `npm:@voidzero-dev/vite-plus-core@`
+at pack time, using the core package's own version. That mechanism needs no
+change.
+
+Code that assumes the two versions are equal must change:
+
+| Location                                                | Current behavior                                                      | Required change                                                                                                                                                 |
+| ------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `packages/cli/src/utils/constants.ts`                   | `VITE_PLUS_OVERRIDE_PACKAGES.vite` uses `VITE_PLUS_VERSION`           | Add `VITE_PLUS_CORE_VERSION = process.env.VP_CORE_VERSION \|\| corePkg.version`, read from `packages/core/package.json` at build time, and use it for the alias |
+| `packages/cli/src/resolve-core.ts`                      | Expects the core `version` to equal the installed `vite-plus` version | Parse the alias in the installed `vite-plus/package.json` `dependencies.vite`; skip the version check for a `workspace:` spec (repository layout)               |
+| `packages/cli/src/sync-versions/protocol.ts`, `plan.ts` | Maps `@voidzero-dev/vite-plus-core` to `toolchain.vitePlus`           | Add `vitePlusCore` from the `vite-plus-core` manifest node and map the core name to it                                                                          |
+| `packages/cli/src/migration/migrator/*`                 | Alias written through `VITE_PLUS_OVERRIDE_PACKAGES`                   | No change beyond the constant; the summary in `collectToolchainVersionChanges` already shows the raw Vite version                                               |
+| `ecosystem-ci/patch-project.ts`                         | `VP_OVERRIDE_PACKAGES.vite` uses the `vite-plus` tarball version      | Use the packed core version                                                                                                                                     |
+| `packages/tools/src/install-global-cli.ts`              | Maps the alias version to `voidzero-dev-vite-plus-core-.tgz` | No change; the alias already carries the core version after pack                                                                                                |
+| `crates/vp_global_cli/src/commands/version.rs`          | Reads `bundledVersions` from the installed core manifest              | No change                                                                                                                                                       |
+| `packages/cli/toolchain.config.json`                    | `vite-plus-core` node uses `core-package` as its version source       | No change; `vp toolchain` shows the core version and the bundled Vite version as separate nodes                                                                 |
+
+The `vp upgrade` command in `crates/vp_global_cli/src/commands/upgrade/`
+installs the global CLI from the published `vite-plus` package and its
+lockfile. It does not read the core version.
+
+### Where the version is computed and checked
+
+Add a small module, `packages/tools/src/core-version.ts`, with four pure
+functions and a CLI entry:
+
+- `encodeCoreVersion(vite, n)` returns the stable core version.
+- `decodeCoreVersion(core)` returns `{ vite, n, k }` or an error.
+- `nextCoreVersion({ vite, vitePlus, published })` returns the core version
+  for a release. Its inputs are the bundled Vite version, the `vite-plus`
+  version, and the published core versions. It applies the counter rules
+  below.
+- `assertCoreVersionMatchesVite(pkg, vitePlus)` asserts that `pkg.version`
+  decodes to `pkg.bundledVersions.vite`. It also asserts that the core
+  version is a prerelease only when the `vite-plus` version is a prerelease.
+
+The name `checkCoreVersion` is not used, because
+`packages/cli/src/resolve-core.ts` already defines a function with that name
+for the installed-core check.
+
+Counter rules in `nextCoreVersion`:
+
+- Stable release: `n` is the highest `n` among published stable core versions
+  with the same Vite version, plus 1, or 0 when none exists. If `n` would be
+  higher than 98, the function fails.
+- Prerelease on a stable Vite: the base is the version that the stable rule
+  gives. `k` is the highest `k` among published `-vp.*` versions, plus
+  1, or 0 when none exists.
+- Prerelease on a Vite prerelease: `k` is the highest `k` among published
+  `.vp.*` versions, plus 1, or 0 when none exists.
+
+The committed `packages/core/package.json` must always encode the committed
+`bundledVersions.vite`. Two writers keep it consistent:
+
+1. `.github/scripts/upgrade-deps.ts` already reads
+   `packages/core/package.json` to sync `postcss` and `fsevents`. After a
+   bundled Vite change it sets `version` to `encodeCoreVersion(vite, 0)`.
+   Renovate ignores `vite` (`.github/renovate.json`), so this script is the
+   only automated path that changes the bundled Vite.
+2. `prepare_release.yml` computes the release version. It reads the
+   published core versions
+   (`npm view @voidzero-dev/vite-plus-core versions --json`) and calls
+   `nextCoreVersion` with the input `vite-plus` version. It then writes the
+   result with the existing `update_json` helper and prints both versions in
+   the release PR body.
+
+A unit test in `packages/core/__tests__/` runs `assertCoreVersionMatchesVite`
+against the committed manifest. It skips the check when the version starts
+with `0.0.0-commit.`, which the preview flow stamps. `packages/core/build.ts`
+does not rewrite `version`: it runs after the release stamp and would race
+with it.
+
+### Local and CI builds
+
+`test-vp-create.yml` and `reusable-ecosystem-ci.yml` set both packages to
+`0.0.0` today (`npm pkg set version=0.0.0`), so that a correct install of the
+local build is easy to detect. Core `0.0.0` does not encode a Vite version,
+and peer checks fail against it. These flows would not test the path without a
+`vite` peer rule that users get.
+
+These flows will set core to `encodeCoreVersion(vite, 99)`, for example
+`8.3.599` for Vite `8.3.5`. They keep `0.0.0` for the CLI, which
+`ecosystem-ci/verify-install.ts` and `test-vp-create.yml` assert. The CLI
+reads the core version from `VP_CORE_VERSION` or from the core manifest, so
+`vp create` and `vp migrate` in these flows write the `8.3.599` alias and
+remove the `vite` peer rule.
+
+`nextCoreVersion` never returns `n = 99`, and `release.yml` rejects it.
+Thus a local version never reaches the registry, and the local registry
+cannot mix it up with a published version.
+
+### Release pipeline changes
+
+| File                                                                                                                             | Change                                                                                                                                                                                                                                                                                                                                                                        |
+| -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `.github/workflows/prepare_release.yml`                                                                                          | Compute the core version as described above; stop passing `VERSION` to `update_json packages/core/package.json`                                                                                                                                                                                                                                                               |
+| `.github/workflows/release.yml`                                                                                                  | Read `CORE_VERSION` from `packages/core/package.json` at job start, and reject a version with `n = 99`; use it in "Publish core dependency tier", "Wait for core to propagate", the release notes, and the Discord embed. After publish, check that the core `latest` dist-tag equals `CORE_VERSION`. The version check against `unpkg.com/vite-plus@latest` stays on the CLI |
+| `.github/workflows/reusable-release-build.yml`                                                                                   | Stamp `packages/core/package.json` only for preview versions (`0.0.0-commit.*`). A release checkout already carries the encoded version. Update the verify step to the same rule                                                                                                                                                                                              |
+| `packages/core/build.ts`                                                                                                         | Pass `cliPkgJson.version` instead of `pkgJson.version` to `rewriteRolldownBindingRequires`. The platform packages publish at the CLI version, and the guard must compare against that version (`packages/core/build-support/rewrite-rolldown-binding.ts`)                                                                                                                     |
+| `packages/cli/publish-native-addons.ts`                                                                                          | No change: it mirrors the CLI's platform pins into core by name and does not use the core version                                                                                                                                                                                                                                                                             |
+| `.github/workflows/publish-preview.yml`, `publish-preview-register.yml`                                                          | No change: the bridge serves every workspace package as `0.0.0-commit.`, and the stamp before `pnpm pack` keeps the alias on that version. The sticky comment keeps printing one version for both packages                                                                                                                                                               |
+| `.github/workflows/test-vp-create.yml`, `reusable-ecosystem-ci.yml`                                                              | Keep `0.0.0` for the CLI. Set core to `encodeCoreVersion(vite, 99)` and pass it as `VP_CORE_VERSION` where the workflow passes `VP_VERSION` (see Local and CI builds)                                                                                                                                                                                                         |
+| `packages/tools/src/local-npm-registry.ts`, `pack-local-vite-plus.ts`                                                            | No change: both serve the packed tarball's own version                                                                                                                                                                                                                                                                                                                        |
+| `.github/scripts/upgrade-deps.ts`                                                                                                | Reset the core version after a bundled Vite change                                                                                                                                                                                                                                                                                                                            |
+| `.github/renovate.json`                                                                                                          | No change                                                                                                                                                                                                                                                                                                                                                                     |
+| `.claude/skills/release-manager/SKILL.md`                                                                                        | Read both versions from the release branch; publish checks use the core version for core                                                                                                                                                                                                                                                                                      |
+| `packages/core/BUNDLING.md`                                                                                                      | Document the version scheme next to `bundledVersions`                                                                                                                                                                                                                                                                                                                         |
+| `MAINTENANCE.md`                                                                                                                 | Mention the two versions in the release description                                                                                                                                                                                                                                                                                                                           |
+| `README.md`, `packages/cli/README.md`, `docs/guide/local-cli.md`, `docs/guide/upgrade-project.md`, `docs/guide/migrate-rules.md` | Replace `` in alias examples with `` and point to `vp toolchain vite-plus-core`; describe the new pnpm peer rule behavior                                                                                                                                                                                                                    |
+
+### Package-manager behavior
+
+The experiment ran on macOS with pnpm `12.11.2`, npm `10.9.9`, Yarn
+`4.18.1` (`nodeLinker: node-modules`), and Bun `1.4.4`. Each project
+declared `vite` as an npm alias to a published package and local `file:`
+consumers with one `vite` peer range each. `commander@8.3.0`,
+`commander@8.0.0`, and `commander@9.0.0-0` stood in for future core versions;
+`@voidzero-dev/vite-plus-core@1.4.0` is the current core;
+`vite@8.3.0-beta.1` stood in for a prerelease core.
+
+pnpm, npm, and Yarn resolve the alias and compare the target's manifest
+version with the consumer's range. Their results for release versions agree:
+
+| Alias target | `^8.0.0` | `^8.3.0` | `>=8.3.2` | `~8.3.0` | `^7.0.0` | `^8.0.0-0` | `>=8.0.0-0` |
+| ------------ | -------- | -------- | --------- | -------- | -------- | ---------- | ----------- |
+| `8.3.0`      | ok       | ok       | unmet     | ok       | unmet    | ok         | ok          |
+| `8.0.0`      | ok       | unmet    | unmet     | unmet    | unmet    | ok         | ok          |
+| core `1.4.0` | unmet    | unmet    | unmet     | unmet    | unmet    | unmet      | unmet       |
+
+Prerelease targets differ by package manager:
+
+| Alias target   | Manager | `^8.0.0` | `^8.3.0` | `^8.0.0-0` | `^8.3.0-0` | `>=8.0.0-0` | `>=8.0.0` |
+| -------------- | ------- | -------- | -------- | ---------- | ---------- | ----------- | --------- |
+| `8.3.0-beta.1` | npm     | unmet    | unmet    | unmet      | ok         | unmet       | unmet     |
+| `8.3.0-beta.1` | pnpm    | ok       | unmet    | ok         | ok         | ok          | ok        |
+| `8.3.0-beta.1` | Yarn    | ok       | ok       | ok         | ok         | ok          | ok        |
+| `9.0.0-0`      | npm     | unmet    | unmet    | unmet      | unmet      | unmet       | unmet     |
+| `9.0.0-0`      | pnpm    | unmet    | unmet    | unmet      | unmet      | ok          | ok        |
+| `9.0.0-0`      | Yarn    | unmet    | unmet    | unmet      | unmet      | ok          | ok        |
+
+npm follows strict semver prerelease rules. pnpm includes prereleases when
+comparing. Yarn accepted `8.3.0-beta.1` for `^8.3.0`; the RFC records that
+result without a cause.
+
+Severity: pnpm prints `WARN Issues with peer dependencies found` and exits 0
+unless `strict-peer-dependencies` is set; `pnpm peers check` lists each
+consumer. npm prints `ERESOLVE overriding peer dependency` warnings and exited
+0 in every case here. Yarn prints `YN0060` and exits 0. Bun printed no peer
+diagnostic in any case, including a published consumer
+(`@vitejs/plugin-react@6.1.2`, peer `^8.0.0`) against core `1.4.0`. Bun
+resolved the alias and listed it as `vite@1.4.0`. Whether Bun checks peer
+ranges in other layouts is unverified.
+
+The current `vite-plus` manifest triggers the npm warning on its own, because
+`vitest` is a dependency with a `vite` peer range. With core `8.3.500` that
+range is satisfied, so a project that depends only on `vite-plus` installs
+without the warning.
+
+## Migration
+
+### Existing projects
+
+`vp migrate` on a project that already uses Vite+ runs the toolchain upgrade
+path (`docs/guide/upgrade-project.md`). The alias rewrite is unchanged in
+shape: every `npm:@voidzero-dev/vite-plus-core@1.x` spec in dependencies,
+`overrides`, `resolutions`, pnpm and Bun catalogs, and `pnpm-workspace.yaml`
+becomes `npm:@voidzero-dev/vite-plus-core@`. The existing
+`isProtocolPinnedSpec` rules keep `file:`, `link:`, and `workspace:` pins.
+
+Peer rules change only for pnpm:
+
+- `ensurePnpmPeerDependencyRules` in `vite-plus-bootstrap.ts`,
+  `rewritePnpmWorkspaceYaml` in `catalog.ts`, and the writers in
+  `orchestrators.ts` and `catalog.ts` stop adding `vite` to `allowAny` and
+  `allowedVersions` when the target core version is a stable encoded version
+  (major 8 or higher, no prerelease identifier). They remove an existing
+  `vite` entry from both maps. They delete an empty `peerDependencyRules`
+  object or YAML map. Other entries stay, including `vitest`.
+- The writers cannot tell whether an earlier migration or the user wrote a
+  `vite` entry, so they remove every `vite` entry. The only purpose of that
+  entry is to accept the `1.x` alias. `vp migrate` prints one line that names
+  the removed rule, and `docs/guide/migrate-rules.md` describes the removal.
+- When the target core version is not a stable encoded version (a preview
+  `0.0.0-commit.` or a Vite+ prerelease such as `8.4.0-vp.0`), the
+  writers keep the current behavior and write the `vite` rule.
+- `pnpmPeerDependencyRulesSatisfyVitePlus` reports a project as pending when
+  a `vite` entry exists and the target core is a stable encoded version, or
+  when the entry is missing and the target core is not.
+
+npm, Yarn, and Bun have no peer suppression to remove. Their alias handling
+is unchanged: npm keeps the direct dependency and `overrides.vite` on the same
+spec (`EOVERRIDE` rule), `npm-reinstall.ts` keeps moving stale `vite`
+installs aside by package name, Yarn keeps `resolutions.vite`, and Bun keeps
+the mirrored direct `vite` dependency.
+
+The final install step refreshes lockfiles. Every package manager sees the
+alias target version change from `1.x` to `8.x` and re-resolves it; no
+lockfile edit is needed beyond the existing npm stale-install handling.
+
+After migration, a plugin with an incompatible range (`vite: ^7.0.0`) produces
+a peer warning for the first time. This is the intended result. The migrate
+rules document must say so.
+
+### Downgrade
+
+A project on core `8.3.500` can move back to `vite-plus@1.4.0` by running
+that CLI's `vp migrate`. The older CLI rewrites the alias to
+`npm:@voidzero-dev/vite-plus-core@1.4.0` and re-adds the pnpm `vite` peer
+rule, because its logic is self-contained. Its `resolveCore` check compares
+the core version with its own version, so it reports a mismatch until the
+alias is realigned. The newer CLI reports the same for a project still on
+`1.4.0`: "Expected `@voidzero-dev/vite-plus-core@8.3.500`, found `1.4.0`.
+Run `vp migrate`." Exact pins make the registry ordering irrelevant for the
+downgrade.
+
+### Fixtures and documentation
+
+Snapshot fixtures under `crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/`
+that record `peerDependencyRules.allowAny: [vite, vitest]` or the alias
+version need re-recording with `UPDATE_SNAPSHOTS=1`. The redaction in
+`crates/vp_cli_snapshots/tests/cli_snapshots/redact.rs` already masks
+`@voidzero-dev/vite-plus-core@` with a prerelease-aware pattern, so
+local-flavor snapshots do not depend on the chosen encoding.
+
+## Alternatives
+
+### Core major = Vite major, minor and patch from `vite-plus`
+
+Simplest for the pipeline: one `sed` with a different first component. It
+fails minor ranges in both directions, as shown in the comparison table. The
+release dates in this repository already show the drift.
+
+### Plain release counter in the patch
+
+Correct for minor ranges and easier to read (`8.3.7`). It misreports patch
+floors: `>=8.3.2` fails against `8.3.0` although the bundled Vite is `8.3.5`.
+The encoded form keeps patch floors correct up to the bundled patch.
+
+The encoded form does not make every core version look different from a Vite
+version. On Vite patch 0, the core patch is `n` alone (`8.4.0`, `8.4.1`,
+...), and core `8.4.1` looks like Vite `8.4.1`. Decoding still gives the
+correct Vite version (`floor(1 / 100) = 0`). Peer checks stay in the residual
+error class of the selected scheme: `>=8.4.1` passes against core `8.4.1`.
+
+### Exact Vite version with a prerelease tag or build metadata
+
+Fails npm peer checks and cannot publish two Vite+ releases on one Vite
+patch. See the comparison.
+
+### Publish core only when the bundled Vite changes
+
+Core would use the exact Vite version. Core also bundles Rolldown, tsdown,
+and the Vite+ build of the Vite source, so a Vite+ release that changes any of
+these without a Vite change could not publish a new core.
+
+### Keep the version and ask consumers to widen peer ranges
+
+Plugins cannot know about a `1.x` alias target. The npm warning comes from
+`vite-plus`'s own dependency tree, so no consumer change removes it.
+
+### Put the bundled Vite version in `peerDependenciesMeta` or a custom field
+
+Package managers do not read a custom field for peer resolution. Only the
+manifest `version` takes part in the check.
+
+## Risks
+
+- Confusion with upstream Vite. `npm ls vite` prints
+  `vite@npm:@voidzero-dev/vite-plus-core@8.3.500`, and readers may take
+  `8.3.500` for a Vite version. On Vite patch 0, the core version (`8.4.1`)
+  looks exactly like a Vite version. The decoding rule is one division, and
+  `vp toolchain` prints the real Vite version. Documentation must show both.
+- Bundled Vite downgrade. If Vite+ goes back to an older Vite version, the
+  core version goes down, for example from `8.4.0` to `8.3.501`. The counter
+  rules still give a unique version, and exact pins make the lower version
+  install correctly. `release.yml` publishes core with `--tag latest`. How
+  npm sets `latest` when the published version is lower than the current
+  `latest` is unverified. The dist-tag check after publish (see Release
+  pipeline changes) makes a wrong tag visible.
+- Registry ordering. `8.3.500` sorts above every published `1.x` version, so
+  `latest` moves forward as usual and a range on core would jump majors.
+  Vite+ documents exact pins only. The stale `alpha` and `test` dist-tags
+  point at `0.x` versions and can be removed or left alone.
+- Peer warnings appear for real incompatibilities after migration. Projects
+  that relied on the broad rule to hide a `^7.0.0` plugin see a warning or,
+  with `strict-peer-dependencies`, an install failure. The migrate output
+  should name the change.
+- Vite+ prereleases still warn in npm: `8.4.0-vp.0` does not satisfy
+  `^8.0.0`. Accepted; see Prereleases.
+- Preview builds still warn in npm. The bridge forces `0.0.0-commit.`,
+  and the release-manager skill already lists this class of artifact as a
+  non-blocker. Migration keeps the pnpm rule for preview cores. Thus preview
+  builds cannot show the new peer behavior, and the preview check in
+  `.claude/skills/test-pkg-pr-new-migrate/` does not test it. Verification
+  before a release uses the local registry with the `n = 99` local version
+  (see Local and CI builds).
+- Tools that read the `vite` version. Vite's `version` export is rewritten in
+  `packages/core/build.ts` to import `../../package.json` relative to Vite's
+  `src/node/constants.ts`, which is Vite's own manifest, so the export keeps
+  the real Vite version. This was checked by reading the build script; the
+  built output was not inspected in this worktree. Code that reads
+  `node_modules/vite/package.json` directly, such as
+  `readInstalledRawViteVersion` in `vite-plus-bootstrap.ts`, keys on
+  `name === '@voidzero-dev/vite-plus-core'` and reads `bundledVersions.vite`.
+  Third-party code that reads `version` from that file now sees `8.3.500`
+  instead of `1.4.0`, which is closer to the truth; whether any such code
+  gates features on it is unverified.
+- Binding guards. The Rolldown binding guard in core compares the installed
+  platform package version with a baked version. Today it bakes the core
+  version, which equals the CLI version. The build must bake the CLI version
+  after this change, or every release build fails the guard.
+- Counter lookup needs the registry. `prepare_release.yml` already needs the
+  network for `cargo check`. A registry failure must stop the job with a
+  clear error rather than guess `n = 0`.
+- Pipelines that pass one `VERSION` for every package. The list in Release
+  pipeline changes covers the files found by searching for
+  `vite-plus-core`; a missed `${VERSION}` reference would publish or wait for
+  a core version that does not exist. `wait-for-npm-packages.ts` then fails
+  the job, so the error is visible.
+
+## Other aliased packages
+
+Only `vite` is aliased in user projects. `vitest` is pinned to its real
+upstream version, so peer checks against it are accurate. Rolldown and tsdown
+are bundled into core and exposed as `vite/rolldown` and `vite/pack`; they are
+not aliased, and consumers do not declare peers on them through Vite+. The
+removed `@voidzero-dev/vite-plus-test` alias no longer exists. No other
+package has the same problem, and none is in scope.
+
+## Decisions from review
+
+- Local and CI builds use the reserved `n = 99`, and the release counter stops
+  at 98 (see Local and CI builds).
+- Vite+ prereleases use the `vp.` identifier instead of a copy of the
+  `vite-plus` prerelease identifiers (see Prereleases and new Vite majors).
+- The `vitest` peer rule is removed in a follow-up with its own fixture
+  updates. `catalog.ts` already removes it when the project does not use
+  Vitest. The follow-up covers projects that use Vitest.
+- The counter comes from a registry lookup in `prepare_release.yml`, not from
+  a committed counter. The committed manifest only has to encode the bundled
+  Vite version.
+
+## Open questions
+
+1. Whether a Vite major upgrade is a `vite-plus` major release.
+   Recommendation: yes, decided in the release policy, not here.

From e12697a10e724280d3d0368e1d1c61235eae07a0 Mon Sep 17 00:00:00 2001
From: "MK (fengmk2)" 
Date: Fri, 30 Oct 2026 14:04:52 +0900
Subject: [PATCH 02/10] feat(core): align the core version with the bundled
 Vite version

Encode the bundled Vite version in the @voidzero-dev/vite-plus-core
version, as rfcs/core-version-alignment.md describes. The core major and
minor equal Vite's, and the patch is `vitePatch * 100 + n`. Core 8.3.500
bundles Vite 8.3.5. With the `vite` alias, package managers now check
plugin peer ranges against the bundled Vite.

- Add packages/tools/src/core-version.ts to encode, decode, select, and
  check core versions. Vite+ prereleases use a `vp.` identifier, and
  local CI builds use the reserved counter 99.
- Select the core version from the registry in prepare_release, publish
  core at its own version, and check its latest dist-tag after publish.
- Bake the CLI version into the Rolldown binding guards, because the
  platform packages publish at the CLI version.
- Resolve the expected core version from the CLI's `vite` alias, and map
  the core package to its own version in sync-versions.
- Stop writing the pnpm `vite` peer rule for a stable core, remove an
  existing one, and keep it for preview and prerelease cores.
- Reset the core version when upgrade-deps changes the bundled Vite.

Refs #2806, #3277

Co-Authored-By: Claude Opus 5.5 
---
 .claude/skills/release-manager/SKILL.md       |  16 +-
 .../scripts/__tests__/upgrade-deps.spec.ts    |  39 +++
 .github/scripts/upgrade-deps.ts               |  28 ++
 .github/workflows/prepare_release.yml         |  16 +-
 .github/workflows/release.yml                 |  31 +-
 .github/workflows/reusable-ecosystem-ci.yml   |  10 +-
 .github/workflows/reusable-release-build.yml  |  15 +-
 .github/workflows/test-vp-create.yml          |  18 +-
 MAINTENANCE.md                                |   2 +
 README.md                                     |   8 +-
 .../core_module_identity/check-api.mjs        |   7 +-
 .../core_module_identity/check-npm-layout.mjs |   6 +-
 .../core_module_identity/pin-core-alias.mjs   |   4 +-
 docs/guide/local-cli.md                       |  15 +-
 docs/guide/migrate-rules.md                   |   7 +
 docs/guide/upgrade-project.md                 |   2 +-
 ecosystem-ci/patch-project.ts                 |   8 +-
 ecosystem-ci/paths.ts                         |  19 +-
 packages/cli/BUNDLING.md                      |  22 +-
 packages/cli/README.md                        |   8 +-
 .../cli/src/__tests__/resolve-core.spec.ts    |  60 +++-
 .../vite-peer-dependency-rule.spec.ts         | 275 +++++++++++++++++
 packages/cli/src/migration/bin.ts             |   5 +
 .../cli/src/migration/migrator/catalog.ts     | 106 +++++--
 .../src/migration/migrator/orchestrators.ts   |  36 ++-
 packages/cli/src/migration/migrator/shared.ts |  33 ++-
 .../migration/migrator/vite-plus-bootstrap.ts |  59 ++--
 .../migration/migrator/vitest-ecosystem.ts    |  65 ++++
 packages/cli/src/migration/report.ts          |   2 +
 packages/cli/src/resolve-core.ts              |  32 +-
 .../src/sync-versions/__tests__/plan.spec.ts  |   9 +-
 .../sync-versions/__tests__/protocol.spec.ts  |  12 +-
 packages/cli/src/sync-versions/plan.ts        |   7 +-
 packages/cli/src/sync-versions/protocol.ts    |   1 +
 .../cli/src/utils/__tests__/constants.spec.ts |  49 ++-
 packages/cli/src/utils/constants.ts           |  10 +-
 packages/core/BUNDLING.md                     |  24 +-
 packages/core/__tests__/core-version.spec.ts  |  12 +
 .../build-support/rewrite-rolldown-binding.ts |   2 +-
 packages/core/build.ts                        |   4 +-
 packages/core/package.json                    |   2 +-
 .../tools/src/__tests__/core-version.spec.ts  | 149 ++++++++++
 packages/tools/src/core-version.ts            | 278 ++++++++++++++++++
 43 files changed, 1348 insertions(+), 165 deletions(-)
 create mode 100644 packages/cli/src/migration/__tests__/vite-peer-dependency-rule.spec.ts
 create mode 100644 packages/core/__tests__/core-version.spec.ts
 create mode 100644 packages/tools/src/__tests__/core-version.spec.ts
 create mode 100644 packages/tools/src/core-version.ts

diff --git a/.claude/skills/release-manager/SKILL.md b/.claude/skills/release-manager/SKILL.md
index ef7c7356f5..c9f37f3a25 100644
--- a/.claude/skills/release-manager/SKILL.md
+++ b/.claude/skills/release-manager/SKILL.md
@@ -48,6 +48,8 @@ gh workflow run prepare_release.yml --repo voidzero-dev/vite-plus -f version=X.Y
 
 The workflow bumps `packages/cli/package.json`, `packages/core/package.json`, `packages/prompts/package.json`, `packages/cli/binding/Cargo.toml`, and `crates/vp_global_cli/Cargo.toml`, refreshes `Cargo.lock`, and opens a PR titled `release: vX.Y.Z` from branch `release/vX.Y.Z`. The PR body ends with `Merging this PR will trigger the release workflow.` and that line must survive every later edit.
 
+`@voidzero-dev/vite-plus-core` does not use `X.Y.Z`. Its version encodes the bundled Vite version (for example, `8.3.500` for Vite `8.3.5`; see [Core Version](../../../packages/core/BUNDLING.md#core-version)). The workflow selects it from the registry and names it in the first line of the PR body. In the steps below, `` is that version. Read it from the release branch with `node -p "require('./packages/core/package.json').version"`.
+
 ## 2. Sync release versions (required every release)
 
 ### NAPI binding
@@ -411,14 +413,15 @@ Auto-merge being enabled is not a completed merge. Confirm `mergedAt` and the me
 
 **When the `Release` job fails partway, fix the cause, then use "Re-run failed jobs", never "Re-run all jobs".** A full rerun repeats `check`, which now sees the new version on `unpkg.com/vite-plus@latest` and skips the whole release. A failed-jobs rerun reuses `check` and the build artifacts, and every npm publish step is idempotent: `.github/scripts/publish-npm-package.ts` skips versions the registry already serves (`@X.Y.Z is already published; skipping upload.`), so only the unpublished packages upload. Confirm that from the rerun log. The rerun waits on the `release` environment again, and whoever started the rerun cannot approve it.
 
-**A successful publish command does not mean the packages are installable.** `pnpm publish` prints `✅ Published package @X.Y.Z` as soon as the registry accepts the request, and the registry can then take tens of minutes to actually serve that version. This has shipped a broken release: `vite-plus@X.Y.Z` went live on `latest` with an exact dependency on `@voidzero-dev/vite-plus-core@X.Y.Z` that was invisible for about 35 minutes, so every `npm install vite-plus` failed with `ETARGET` and both `publish-docker` and `Deploy docs` failed on `ERR_PNPM_NO_MATCHING_VERSION`. The downstream job failures are the symptom, not the cause; do not re-run them until the registry has the package.
+**A successful publish command does not mean the packages are installable.** `pnpm publish` prints `✅ Published package @X.Y.Z` as soon as the registry accepts the request, and the registry can then take tens of minutes to actually serve that version. This has shipped a broken release: `vite-plus@X.Y.Z` went live on `latest` with an exact dependency on its `@voidzero-dev/vite-plus-core` version that was invisible for about 35 minutes, so every `npm install vite-plus` failed with `ETARGET` and both `publish-docker` and `Deploy docs` failed on `ERR_PNPM_NO_MATCHING_VERSION`. The downstream job failures are the symptom, not the cause; do not re-run them until the registry has the package.
 
 Check visibility directly, not through `npm view`, which caches:
 
 ```bash
-for pkg in '@voidzero-dev%2Fvite-plus-core' 'vite-plus'; do
-  curl -s -H 'Cache-Control: no-cache' "https://registry.npmjs.org/$pkg?t=$(date +%s)" |
-    python3 -c "import json,sys;d=json.load(sys.stdin);print('$pkg', d['dist-tags'].get('latest'), 'X.Y.Z' in d['versions'])"
+for entry in '@voidzero-dev%2Fvite-plus-core ' 'vite-plus X.Y.Z'; do
+  set -- $entry
+  curl -s -H 'Cache-Control: no-cache' "https://registry.npmjs.org/$1?t=$(date +%s)" |
+    python3 -c "import json,sys;d=json.load(sys.stdin);print('$1', d['dist-tags'].get('latest'), '$2' in d['versions'])"
 done
 ```
 
@@ -484,7 +487,8 @@ Local clients can also mislead in the other direction. npm and pnpm cache a look
 
    ```bash
    npm view vite-plus version                       # X.Y.Z
-   npm view @voidzero-dev/vite-plus-core version    # X.Y.Z
+   npm view @voidzero-dev/vite-plus-core version    # , which encodes the bundled Vite version
+   npm view vite-plus@X.Y.Z dependencies.vite       # npm:@voidzero-dev/vite-plus-core@
    npm view create-vite-plus version                # X.Y.Z
    npm view @voidzero-dev/vite-plus-cli-darwin-arm64 version   # X.Y.Z, spot-check a native platform package
    npm view vite-plus dist-tags.latest              # X.Y.Z
@@ -511,7 +515,7 @@ Local clients can also mislead in the other direction. npm and pnpm cache a look
 
    Require the target version directory, the expected `current` link, and `vp --version` output; a success message alone is insufficient. `Already up to date` passes only when the selected installation is already on the target version.
 
-   The Docker check must run `vp --version` inside the image, not just pull it: the output must report `vp vX.Y.Z`. Outside a project that output lists no bundled tools, so inspect the installed image package tree under `~/.vite-plus/X.Y.Z/node_modules/.pnpm` and confirm the bundled tool packages and versions match the changelog's Bundled Versions table. `tsdown` will be absent from that tree because it is bundled into `@voidzero-dev/vite-plus-core`; verify it with `npm view @voidzero-dev/vite-plus-core@X.Y.Z bundledVersions --json` instead. If no local Docker runtime is available, confirm `publish-docker` succeeded and inspect the GHCR manifest for both `linux/amd64` and `linux/arm64`. For the current stable release, confirm the version tag and `latest` have the same digest. Record each architecture's `vp --version` output from the Docker build logs when available (`gh api --allow-escape-sequences repos/voidzero-dev/vite-plus/actions/jobs//logs`; without the flag, `gh` refuses logs containing terminal escape sequences), and distinguish that evidence from a local run:
+   The Docker check must run `vp --version` inside the image, not just pull it: the output must report `vp vX.Y.Z`. Outside a project that output lists no bundled tools, so inspect the installed image package tree under `~/.vite-plus/X.Y.Z/node_modules/.pnpm` and confirm the bundled tool packages and versions match the changelog's Bundled Versions table. `tsdown` will be absent from that tree because it is bundled into `@voidzero-dev/vite-plus-core`; verify it with `npm view @voidzero-dev/vite-plus-core@ bundledVersions --json` instead. If no local Docker runtime is available, confirm `publish-docker` succeeded and inspect the GHCR manifest for both `linux/amd64` and `linux/arm64`. For the current stable release, confirm the version tag and `latest` have the same digest. Record each architecture's `vp --version` output from the Docker build logs when available (`gh api --allow-escape-sequences repos/voidzero-dev/vite-plus/actions/jobs//logs`; without the flag, `gh` refuses logs containing terminal escape sequences), and distinguish that evidence from a local run:
 
    ```bash
    TOKEN=$(curl -s "https://ghcr.io/token?scope=repository:voidzero-dev/vite-plus:pull" \
diff --git a/.github/scripts/__tests__/upgrade-deps.spec.ts b/.github/scripts/__tests__/upgrade-deps.spec.ts
index 75b7567350..d1ad856851 100644
--- a/.github/scripts/__tests__/upgrade-deps.spec.ts
+++ b/.github/scripts/__tests__/upgrade-deps.spec.ts
@@ -188,9 +188,11 @@ function createSyncFixture() {
   const metaDir = join(root, 'meta');
   const versionsPath = join(metaDir, 'versions.json');
   writeJson(corePath, {
+    version: '8.3.501',
     dependencies: { 'yuku-parser': '^0.9.3', postcss: '^8.5.6' },
     optionalDependencies: { fsevents: '~2.3.3' },
     devDependencies: { 'rolldown-plugin-dts': '^0.28.0' },
+    bundledVersions: { vite: '8.3.5' },
   });
   writeJson(join(root, 'store/tsdown/package.json'), { name: 'tsdown', version: '0.23.0' });
   writeJson(pluginPath, {
@@ -216,6 +218,7 @@ function createSyncFixture() {
     dependencies: { 'yuku-codegen': '^0.8.1', 'yuku-parser': '^0.8.1' },
   });
   writeJson(vitePath, {
+    version: '8.3.5',
     dependencies: { postcss: '^8.5.28' },
     optionalDependencies: { fsevents: '~2.4.0' },
   });
@@ -239,9 +242,12 @@ test('pins the bundled parser, syncs Vite ranges, and preserves upgrade metadata
   const { corePath, versionsPath, metaDir, run } = fixture;
   run();
   expect(readJson(corePath)).toEqual({
+    // The bundled Vite did not change, so the release counter stays.
+    version: '8.3.501',
     dependencies: { 'yuku-parser': '0.10.3', postcss: '^8.5.28' },
     optionalDependencies: { fsevents: '~2.4.0' },
     devDependencies: { 'rolldown-plugin-dts': '^0.28.0' },
+    bundledVersions: { vite: '8.3.5' },
   });
   expect(readJson(versionsPath)).toEqual({
     oxlint: { old: '1.86.0', new: '1.87.0' },
@@ -271,6 +277,39 @@ test('pins the bundled parser, syncs Vite ranges, and preserves upgrade metadata
   expect(readFileSync(corePath, 'utf8')).toBe(before);
 });
 
+test('restarts the core version counter when the bundled Vite changes', () => {
+  const fixture = createSyncFixture();
+  const { corePath, versionsPath, metaDir, run } = fixture;
+  writeJson(fixture.vitePath, { ...readJson(fixture.vitePath), version: '8.4.2' });
+  run();
+  expect(readJson(corePath).version).toBe('8.4.200');
+  expect(readJson(corePath).bundledVersions).toEqual({ vite: '8.4.2' });
+  expect(readJson(versionsPath)['@voidzero-dev/vite-plus-core']).toEqual({
+    old: '8.3.501',
+    new: '8.4.200',
+  });
+  expect(readFileSync(join(metaDir, 'pr-body.md'), 'utf8')).toContain(
+    '| `@voidzero-dev/vite-plus-core` | `8.3.501` | `8.4.200` |',
+  );
+
+  // A second pass keeps the recorded old version and the encoded version.
+  run();
+  expect(readJson(corePath).version).toBe('8.4.200');
+  expect(readJson(versionsPath)['@voidzero-dev/vite-plus-core']).toEqual({
+    old: '8.3.501',
+    new: '8.4.200',
+  });
+});
+
+test('fails without writing core when the Vite source has no version', () => {
+  const fixture = createSyncFixture();
+  const { version: _, ...vite } = readJson(fixture.vitePath);
+  writeJson(fixture.vitePath, vite);
+  const before = readFileSync(fixture.corePath, 'utf8');
+  expect(fixture.run).toThrow('Missing version');
+  expect(readFileSync(fixture.corePath, 'utf8')).toBe(before);
+});
+
 test('updates the parser pin when its installed version changes within the same range', () => {
   const { corePath, parserPath, versionsPath, run } = createSyncFixture();
   run();
diff --git a/.github/scripts/upgrade-deps.ts b/.github/scripts/upgrade-deps.ts
index 0927b3a9d2..434ee3c50b 100644
--- a/.github/scripts/upgrade-deps.ts
+++ b/.github/scripts/upgrade-deps.ts
@@ -2,6 +2,7 @@ import { execFileSync } from 'node:child_process';
 import fs from 'node:fs';
 import path from 'node:path';
 
+import { decodeCoreVersion, encodeCoreVersion } from '../../packages/tools/src/core-version.ts';
 import {
   findLatestStableVersionForMajor,
   getBundledDependencyRanges,
@@ -533,10 +534,37 @@ function syncBundledDependencies(): void {
     recordChange(name, changes.get(name)?.old ?? oldRange, range);
     pkg[field][name] = range;
   }
+  syncCoreVersion(pkg);
   fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2) + '\n');
   writeMetaFiles();
 }
 
+// The core version encodes the bundled Vite version (rfcs/core-version-alignment.md).
+// When the bundled Vite changes, restart the release counter at 0. The release
+// workflow selects the next unpublished counter.
+function syncCoreVersion(pkg: { version?: unknown; bundledVersions?: Record }) {
+  const vitePath = path.join(ROOT, 'vite/packages/vite/package.json');
+  const viteVersion = readJsonFile(vitePath).version;
+  if (typeof viteVersion !== 'string' || !viteVersion.trim()) {
+    throw new Error(`Missing version in ${vitePath}`);
+  }
+  pkg.bundledVersions = { ...pkg.bundledVersions, vite: viteVersion };
+  const current = typeof pkg.version === 'string' ? pkg.version : undefined;
+  let currentVite: string | undefined;
+  try {
+    currentVite = current === undefined ? undefined : decodeCoreVersion(current).vite;
+  } catch {
+    currentVite = undefined;
+  }
+  if (currentVite === viteVersion) {
+    return;
+  }
+  const name = '@voidzero-dev/vite-plus-core';
+  const next = encodeCoreVersion(viteVersion, 0);
+  recordChange(name, changes.get(name)?.old ?? current, next);
+  pkg.version = next;
+}
+
 async function upgradeDependencies(): Promise {
   console.log('Fetching latest versions…');
 
diff --git a/.github/workflows/prepare_release.yml b/.github/workflows/prepare_release.yml
index db7fd48e18..4263305ec1 100644
--- a/.github/workflows/prepare_release.yml
+++ b/.github/workflows/prepare_release.yml
@@ -38,14 +38,15 @@ jobs:
       # release.yml updates HomebrewFormula/vp.rb after the release archives and
       # their checksums are published. Do not point the tap at unpublished assets.
       - name: Bump versions
+        id: versions
         env:
           VERSION: ${{ inputs.version }}
         run: |
           set -euo pipefail
           update_json() {
-            local file=$1
-            sed -i -E "s/^(\s*\"version\":\s*)\"[^\"]+\"/\1\"$VERSION\"/" "$file"
-            grep -qF "\"version\": \"$VERSION\"" "$file" || { echo "::error::Failed to update $file"; exit 1; }
+            local file=$1 version=${2:-$VERSION}
+            sed -i -E "s/^(\s*\"version\":\s*)\"[^\"]+\"/\1\"$version\"/" "$file"
+            grep -qF "\"version\": \"$version\"" "$file" || { echo "::error::Failed to update $file"; exit 1; }
             echo "Updated $file"
           }
           update_toml() {
@@ -54,12 +55,17 @@ jobs:
             grep -qF "version = \"$VERSION\"" "$file" || { echo "::error::Failed to update $file"; exit 1; }
             echo "Updated $file"
           }
+          # The core version encodes the bundled Vite version and a release
+          # counter read from the registry (rfcs/core-version-alignment.md).
+          CORE_VERSION=$(node packages/tools/src/core-version.ts next --vite-plus "$VERSION")
+          echo "core-version=$CORE_VERSION" >> "$GITHUB_OUTPUT"
           update_json packages/cli/package.json
-          update_json packages/core/package.json
+          update_json packages/core/package.json "$CORE_VERSION"
           update_json packages/prompts/package.json
           update_json packages/create-vite-plus/package.json
           update_toml packages/cli/binding/Cargo.toml
           update_toml crates/vp_global_cli/Cargo.toml
+          node packages/tools/src/core-version.ts check --release
 
       - name: Refresh Cargo.lock
         run: cargo check
@@ -81,7 +87,7 @@ jobs:
           base: main
           sign-commits: true
           body: |
-            Release vite-plus v${{ inputs.version }}.
+            Release vite-plus v${{ inputs.version }} with @voidzero-dev/vite-plus-core ${{ steps.versions.outputs.core-version }}.
 
             For a stable release, complete the `HomebrewFormula/vp.rb` update after publication by merging the generated `release/homebrew-v${{ inputs.version }}` PR.
 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0f8d98162a..d671da3207 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -22,6 +22,7 @@ jobs:
     outputs:
       version_changed: ${{ steps.version.outputs.changed }}
       version: ${{ steps.version.outputs.version }}
+      core_version: ${{ steps.core-version.outputs.version }}
     steps:
       - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
 
@@ -33,6 +34,12 @@ jobs:
           file-url: https://unpkg.com/vite-plus@latest/package.json
           file-name: packages/cli/package.json
 
+      # The core version encodes the bundled Vite version, so it differs from
+      # the CLI version (rfcs/core-version-alignment.md).
+      - name: Read core version
+        id: core-version
+        run: echo "version=$(node -p "require('./packages/core/package.json').version")" >> "$GITHUB_OUTPUT"
+
   build-rust:
     name: Build bindings and binaries
     needs: check
@@ -66,6 +73,7 @@ jobs:
       id-token: write # Required for OIDC
     env:
       VERSION: ${{ needs.check.outputs.version }}
+      CORE_VERSION: ${{ needs.check.outputs.core_version }}
       # npm publish-time scanning makes a successful upload temporarily
       # unavailable to installs. After each dependency tier becomes visible on
       # this runner's registry edge, allow time for other CDN edges to settle.
@@ -91,6 +99,9 @@ jobs:
       - name: Install dependencies
         run: pnpm install
 
+      - name: Check core version
+        run: node packages/tools/src/core-version.ts check --release
+
       - name: Download cli dist
         uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
         with:
@@ -182,7 +193,7 @@ jobs:
       - name: Publish core dependency tier
         run: >-
           node ./.github/scripts/publish-npm-package.ts
-          "@voidzero-dev/vite-plus-core@${VERSION}"
+          "@voidzero-dev/vite-plus-core@${CORE_VERSION}"
           --
           pnpm publish --filter=./packages/core --tag latest --access public --no-git-checks
 
@@ -191,7 +202,18 @@ jobs:
       - name: Wait for core to propagate
         run: >-
           node ./.github/scripts/wait-for-npm-packages.ts
-          "@voidzero-dev/vite-plus-core@${VERSION}"
+          "@voidzero-dev/vite-plus-core@${CORE_VERSION}"
+
+      # A bundled Vite downgrade publishes a core version below the current
+      # latest. Show a wrong dist-tag without stopping the release.
+      - name: Check the core latest dist-tag
+        continue-on-error: true
+        run: |
+          LATEST=$(npm view @voidzero-dev/vite-plus-core dist-tags.latest)
+          if [ "$LATEST" != "$CORE_VERSION" ]; then
+            echo "::error::@voidzero-dev/vite-plus-core latest is $LATEST, expected $CORE_VERSION. Run: npm dist-tag add @voidzero-dev/vite-plus-core@$CORE_VERSION latest"
+            exit 1
+          fi
 
       # Make the exact local CLI version installable without moving latest.
       # Bootstrap keeps selecting the previous release until its matching global
@@ -288,7 +310,7 @@ jobs:
 
           ### Published Packages
 
-          - \`@voidzero-dev/vite-plus-core@${VERSION}\`
+          - \`@voidzero-dev/vite-plus-core@${CORE_VERSION}\`
           - \`vite-plus@${VERSION}\`
           - \`create-vite-plus@${VERSION}\`
 
@@ -515,6 +537,7 @@ jobs:
       (needs.deploy-docs.result == 'success' || needs.deploy-docs.result == 'skipped')
     env:
       VERSION: ${{ needs.check.outputs.version }}
+      CORE_VERSION: ${{ needs.check.outputs.core_version }}
       IMAGE: ghcr.io/voidzero-dev/vite-plus
     steps:
       # Non-blocking: a webhook failure must not mark the release run as failed
@@ -529,7 +552,7 @@ jobs:
             A new release is available!
 
             **Published Packages:**
-            • @voidzero-dev/vite-plus-core@${{ env.VERSION }}
+            • @voidzero-dev/vite-plus-core@${{ env.CORE_VERSION }}
             • vite-plus@${{ env.VERSION }}
             • create-vite-plus@${{ env.VERSION }}
 
diff --git a/.github/workflows/reusable-ecosystem-ci.yml b/.github/workflows/reusable-ecosystem-ci.yml
index df17337fcd..23f2bfd7c6 100644
--- a/.github/workflows/reusable-ecosystem-ci.yml
+++ b/.github/workflows/reusable-ecosystem-ci.yml
@@ -85,11 +85,15 @@ jobs:
           # and installs them like a real release, with every package manager
           # (including bun) resolving the standard
           # `vite -> npm:@voidzero-dev/vite-plus-core@` alias. Pin
-          # both packages to 0.0.0 so a correctly installed local build is
+          # the CLI to 0.0.0 and core to its local version (release counter
+          # 99, for example 8.3.599) so a correctly installed local build is
           # always distinguishable from any published version
           # (verify-install.ts asserts it), even on a release commit that
-          # leaves packages/{core,cli} at a published version.
-          (cd packages/core && npm pkg set version=0.0.0)
+          # leaves packages/{core,cli} at a published version. The local core
+          # version still encodes the bundled Vite, so peer checks run as they
+          # do for a release (rfcs/core-version-alignment.md).
+          CORE_VERSION=$(node packages/tools/src/core-version.ts local)
+          (cd packages/core && npm pkg set version="$CORE_VERSION")
           (cd packages/cli && npm pkg set version=0.0.0)
           cd packages/core && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
           cd packages/cli && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
diff --git a/.github/workflows/reusable-release-build.yml b/.github/workflows/reusable-release-build.yml
index 01d59394c5..b1c6948f6d 100644
--- a/.github/workflows/reusable-release-build.yml
+++ b/.github/workflows/reusable-release-build.yml
@@ -86,14 +86,17 @@ jobs:
           #   cliPkg.version (VITE_PLUS_VERSION). Without this a preview
           #   `vp migrate` pins the build-time release placeholder instead of
           #   the test build, and skips the registry-bridge .npmrc.
-          # - packages/core: the release build bakes ${VERSION} into bundled
-          #   Rolldown's binding version guards, matching the
-          #   @voidzero-dev/vite-plus- packages published as
-          #   ${VERSION}. A release checkout already carries ${VERSION}; this
-          #   only changes preview builds.
+          # - packages/core: only for preview builds, which publish every
+          #   package as 0.0.0-commit.. A release checkout already
+          #   carries the core version, which encodes the bundled Vite
+          #   version (rfcs/core-version-alignment.md). Release builds bake
+          #   the CLI version into bundled Rolldown's binding version guards.
           # The list feeds the verify step below, so a stamped file cannot
           # silently drop out of verification.
-          PKG_JSON_STAMP_FILES="packages/cli/package.json packages/core/package.json"
+          PKG_JSON_STAMP_FILES="packages/cli/package.json"
+          if [[ "$VERSION" == 0.0.0-* ]]; then
+            PKG_JSON_STAMP_FILES="$PKG_JSON_STAMP_FILES packages/core/package.json"
+          fi
           for f in $PKG_JSON_STAMP_FILES; do
             pnpm exec json-edit "$f" '_.version = process.env.VERSION'
           done
diff --git a/.github/workflows/test-vp-create.yml b/.github/workflows/test-vp-create.yml
index 31ce989db7..bd47280ebd 100644
--- a/.github/workflows/test-vp-create.yml
+++ b/.github/workflows/test-vp-create.yml
@@ -107,11 +107,15 @@ jobs:
           # and installs them like a real release, with every package manager
           # (including bun) resolving the standard
           # `vite -> npm:@voidzero-dev/vite-plus-core@` alias. Pin
-          # both packages to 0.0.0 so a correctly installed local build is
+          # the CLI to 0.0.0 and core to its local version (release counter
+          # 99, for example 8.3.599) so a correctly installed local build is
           # always distinguishable from any published version, even on a
           # release commit that leaves packages/{core,cli} at a published
-          # version.
-          (cd packages/core && npm pkg set version=0.0.0)
+          # version. The local core version still encodes the bundled Vite,
+          # so peer checks run as they do for a release
+          # (rfcs/core-version-alignment.md).
+          CORE_VERSION=$(node packages/tools/src/core-version.ts local)
+          (cd packages/core && npm pkg set version="$CORE_VERSION")
           (cd packages/cli && npm pkg set version=0.0.0)
           cd packages/core && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
           cd packages/cli && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
@@ -181,6 +185,7 @@ jobs:
       # The local registry serves the packed 0.0.0 build, so create pins the
       # exact version like a real release. The vp binary was built before the
       # pack step pinned the package versions, so align the version explicitly.
+      # The "Set local core version" step sets VP_CORE_VERSION the same way.
       VP_VERSION: '0.0.0'
       # Force full dependency rewriting so the library template's existing
       # vite-plus dep gets overridden with the local build
@@ -204,6 +209,9 @@ jobs:
           name: vite-plus-packages
           path: tmp/tgz
 
+      - name: Set local core version
+        run: echo "VP_CORE_VERSION=$(node packages/tools/src/core-version.ts local)" >> "$GITHUB_ENV"
+
       - name: Install vp CLI
         run: |
           mkdir -p target/release
@@ -238,8 +246,8 @@ jobs:
           echo "$handshake"
           echo "$handshake" | node -e 'const { env } = JSON.parse(require("node:fs").readFileSync(0, "utf8")); for (const [k, v] of Object.entries(env)) console.log(`${k}=${v}`)' >> "$GITHUB_ENV"
 
-      # No VP_OVERRIDE_PACKAGES: with VP_VERSION=0.0.0 the product default
-      # override map already pins `vite` to npm:@voidzero-dev/vite-plus-core@0.0.0
+      # No VP_OVERRIDE_PACKAGES: with VP_CORE_VERSION set, the product default
+      # override map already pins `vite` to npm:@voidzero-dev/vite-plus-core@
       # and `vitest` to the bundled version (see VITE_PLUS_OVERRIDE_PACKAGES in
       # packages/cli/src/utils/constants.ts), and stays correct across vitest
       # bumps without editing this workflow.
diff --git a/MAINTENANCE.md b/MAINTENANCE.md
index 90fef2796d..6edaf31e23 100644
--- a/MAINTENANCE.md
+++ b/MAINTENANCE.md
@@ -24,6 +24,8 @@ pnpm add vite-plus@0.0.0-commit.
 
 The [release workflow](.github/workflows/release.yml) publishes platform packages before `@voidzero-dev/vite-plus-core`, then publishes `vite-plus`. It checks that the required package versions and tarballs are available before continuing to dependent steps.
 
+`@voidzero-dev/vite-plus-core` has its own version, which encodes the bundled Vite version (for example, `8.3.500` for Vite `8.3.5`). The [prepare release workflow](.github/workflows/prepare_release.yml) selects it from the registry and prints both versions in the release PR. The platform packages, `vite-plus`, and `create-vite-plus` use the `vite-plus` version. See [Core Version](packages/core/BUNDLING.md#core-version).
+
 The [propagation helper](.github/scripts/wait-for-npm-packages.ts) defaults to a 10-minute timeout and an additional 60-second wait for CDN propagation after availability checks pass.
 
 ### Recovering from a propagation timeout
diff --git a/README.md b/README.md
index 5f5db2fddd..9e08310875 100644
--- a/README.md
+++ b/README.md
@@ -203,11 +203,11 @@ If you are manually migrating a project to Vite+, install these dev dependencies
 vp install -D vite-plus
 ```
 
-Add package-manager overrides so that other packages use the Vite+ versions. Alias `vite` to `@voidzero-dev/vite-plus-core`, setting `` to the installed `vite-plus` version. Pin `vitest` to ``, using the bundled version shown by `vp toolchain vitest`. The project and `vp test` then use the same Vitest copy. Without the pin, a dependency or workspace package can install a different Vitest version. The two versions can use separate mocks, `expect` functions, and runner states:
+Add package-manager overrides so that other packages use the Vite+ versions. Alias `vite` to `@voidzero-dev/vite-plus-core`, setting `` to the core version that the installed `vite-plus` uses, shown by `vp toolchain vite-plus-core`. The core version encodes the bundled Vite version (for example, `8.3.500` for Vite `8.3.5`), so package managers check `vite` peer ranges against the bundled Vite. Pin `vitest` to ``, using the bundled version shown by `vp toolchain vitest`. The project and `vp test` then use the same Vitest copy. Without the pin, a dependency or workspace package can install a different Vitest version. The two versions can use separate mocks, `expect` functions, and runner states:
 
 ```json
 "overrides": {
-  "vite": "npm:@voidzero-dev/vite-plus-core@",
+  "vite": "npm:@voidzero-dev/vite-plus-core@",
   "vitest": ""
 }
 ```
@@ -216,7 +216,7 @@ If you are using `pnpm`, add this to your `pnpm-workspace.yaml`:
 
 ```yaml
 overrides:
-  vite@*: npm:@voidzero-dev/vite-plus-core@ # Match the installed vite-plus version
+  vite@*: npm:@voidzero-dev/vite-plus-core@ # Match the version from vp toolchain vite-plus-core
   vitest@*:  # Match the version from vp toolchain vitest
 ```
 
@@ -224,7 +224,7 @@ Or, if you are using Yarn:
 
 ```json
 "resolutions": {
-  "vite": "npm:@voidzero-dev/vite-plus-core@",
+  "vite": "npm:@voidzero-dev/vite-plus-core@",
   "vitest": ""
 }
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-api.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-api.mjs
index 573b1212d6..04e308c94f 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-api.mjs
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-api.mjs
@@ -24,6 +24,11 @@ for (const name of [
 assert.equal(ModuleRunner, VpModuleRunner);
 const config = {};
 assert.equal(vite.defineConfig(config), config);
-assert.equal(pkg.dependencies.vite, `npm:@voidzero-dev/vite-plus-core@${pkg.version}`);
+// The core version encodes the bundled Vite version, so it differs from the
+// vite-plus version (rfcs/core-version-alignment.md).
+assert.equal(
+  pkg.dependencies.vite,
+  `npm:@voidzero-dev/vite-plus-core@${require('vite/package.json').version}`,
+);
 assert.equal(pkg.dependencies['@voidzero-dev/vite-plus-core'], undefined);
 console.log('Packed alias, ESM, CommonJS, and module-runner identity passed');
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-npm-layout.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-npm-layout.mjs
index 7d71f37cc7..c94fb893f9 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-npm-layout.mjs
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-npm-layout.mjs
@@ -10,7 +10,11 @@ assert.equal(cliRequire('vite/package.json').name, '@voidzero-dev/vite-plus-core
 if (project.devDependencies.vite) {
   assert.equal(
     project.devDependencies.vite,
-    `npm:@voidzero-dev/vite-plus-core@${cliRequire('vite-plus/package.json').version}`,
+    cliRequire('vite-plus/package.json').dependencies.vite,
+  );
+  assert.equal(
+    project.devDependencies.vite,
+    `npm:@voidzero-dev/vite-plus-core@${cliRequire('vite/package.json').version}`,
   );
   assert.equal(require.resolve('vite/package.json'), cliRequire.resolve('vite/package.json'));
   assert.equal(vitestRequire.resolve('vite/package.json'), cliRequire.resolve('vite/package.json'));
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/pin-core-alias.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/pin-core-alias.mjs
index b1e29e051d..bc0c30158a 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/pin-core-alias.mjs
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/pin-core-alias.mjs
@@ -3,8 +3,10 @@ import { createRequire } from 'node:module';
 
 // Before installation, resolve the checkout exposed by the snapshot runner.
 // CI can stamp its version, so do not hard-code the committed package version.
+// The core version encodes the bundled Vite version, so it differs from the
+// vite-plus version (rfcs/core-version-alignment.md).
 const require = createRequire(import.meta.url);
-const { version } = require('vite-plus/package.json');
+const { version } = require('@voidzero-dev/vite-plus-core/package.json');
 const project = JSON.parse(readFileSync('package.json', 'utf8'));
 project.devDependencies.vite = `npm:@voidzero-dev/vite-plus-core@${version}`;
 writeFileSync('package.json', `${JSON.stringify(project, null, 2)}\n`);
diff --git a/docs/guide/local-cli.md b/docs/guide/local-cli.md
index c2a76e9b93..5a055eecfd 100644
--- a/docs/guide/local-cli.md
+++ b/docs/guide/local-cli.md
@@ -61,26 +61,26 @@ bun add -D vite-plus
 
 :::
 
-Add overrides to your package manager so that other packages resolve the Vite+ versions. Alias `vite` to `@voidzero-dev/vite-plus-core`, setting `` to the installed `vite-plus` version. Pin `vitest` to ``, using the bundled version shown by `vp toolchain vitest` so the whole project shares a single Vitest copy with `vp test`. Without the `vitest` pin, a dependency or workspace package can pull a different Vitest than the bundled runner, splitting Vitest's internals (mocks, `expect`, runner state):
+Add overrides to your package manager so that other packages resolve the Vite+ versions. Alias `vite` to `@voidzero-dev/vite-plus-core`, setting `` to the core version that the installed `vite-plus` uses, shown by `vp toolchain vite-plus-core`. Pin `vitest` to ``, using the bundled version shown by `vp toolchain vitest` so the whole project shares a single Vitest copy with `vp test`. Without the `vitest` pin, a dependency or workspace package can pull a different Vitest than the bundled runner, splitting Vitest's internals (mocks, `expect`, runner state):
 
 ::: code-group
 
 ```yaml [pnpm-workspace.yaml]
 overrides:
-  vite@*: npm:@voidzero-dev/vite-plus-core@ # Match the installed vite-plus version
+  vite@*: npm:@voidzero-dev/vite-plus-core@ # Match the version from vp toolchain vite-plus-core
   vitest@*:  # Match the version from vp toolchain vitest
 ```
 
 ```json [npm / Bun package.json]
 "overrides": {
-  "vite": "npm:@voidzero-dev/vite-plus-core@",
+  "vite": "npm:@voidzero-dev/vite-plus-core@",
   "vitest": ""
 }
 ```
 
 ```json [Yarn package.json]
 "resolutions": {
-  "vite": "npm:@voidzero-dev/vite-plus-core@",
+  "vite": "npm:@voidzero-dev/vite-plus-core@",
   "vitest": ""
 }
 ```
@@ -95,11 +95,11 @@ For example, update an existing `vite` development dependency as follows. Keep t
 ```json [package.json]
 {
   "devDependencies": {
-    "vite": "npm:@voidzero-dev/vite-plus-core@",
+    "vite": "npm:@voidzero-dev/vite-plus-core@",
     "vite-plus": ""
   },
   "overrides": {
-    "vite": "npm:@voidzero-dev/vite-plus-core@",
+    "vite": "npm:@voidzero-dev/vite-plus-core@",
     "vitest": ""
   }
 }
@@ -113,9 +113,10 @@ If you also declare `vitest` directly, set its dependency entry to the exact ver
 Dependencies and plugins can import `vite` or `vitest` directly, even when your own code imports from `vite-plus`. These overrides align their dependencies with the toolchain Vite+ uses:
 
 - The `vite` alias directs those imports to Vite+'s core package. Separate Vite instances can break runtime identity checks: [issue #1391](https://github.com/voidzero-dev/vite-plus/issues/1391) reported TanStack Start returning 404s because an `instanceof` check crossed two copies. [PR #2617](https://github.com/voidzero-dev/vite-plus/pull/2617) addresses the CLI side by sharing Vite through the same alias.
+- The core version encodes the bundled Vite version. Its major and minor are the Vite major and minor, and its patch is the Vite patch times 100 plus a release counter. For example, core `8.3.500` bundles Vite `8.3.5`. Package managers compare this version with the `vite` peer ranges of plugins, so a plugin that supports the bundled Vite passes the check, and a plugin that does not support it still gets a peer warning. [Issue #2806](https://github.com/voidzero-dev/vite-plus/issues/2806) describes the earlier `1.x` versions, which failed every check.
 - The exact `vitest` pin keeps dependencies and `vp test` on the same Vitest version, avoiding separate mocks, `expect` instances, and runner state. [PR #2365](https://github.com/voidzero-dev/vite-plus/pull/2365) documents this requirement for manual installation.
 
-Keep the core alias aligned with your installed `vite-plus` version and update the Vitest pin to match its bundled version when upgrading. [Issue #2356](https://github.com/voidzero-dev/vite-plus/issues/2356) describes how dependency bots can update these packages independently and leave incompatible versions installed together.
+Keep the core alias aligned with the core version of your installed `vite-plus` and update the Vitest pin to match its bundled version when upgrading. [Issue #2356](https://github.com/voidzero-dev/vite-plus/issues/2356) describes how dependency bots can update these packages independently and leave incompatible versions installed together.
 
 :::
 
diff --git a/docs/guide/migrate-rules.md b/docs/guide/migrate-rules.md
index c37dcf3c13..afdc45005e 100644
--- a/docs/guide/migrate-rules.md
+++ b/docs/guide/migrate-rules.md
@@ -379,6 +379,13 @@ Migration converts the Volta `volta.node` pin to `.node-version`, because Vite+
 - pnpm accepts the logical default catalog as either top-level `catalog` or `catalogs.default`, but not both. Migration preserves the existing form and never creates the other form beside it.
 - When an existing named catalog already owns `vite-plus`, `vite`, or `vitest`, migration reuses that managed toolchain catalog for newly added dependencies and overrides. It creates a top-level default catalog only when no managed or default catalog can be reused.
 
+**Peer rules.** The core version encodes the bundled Vite version: core `8.3.500` bundles Vite `8.3.5` (see the [RFC](https://github.com/voidzero-dev/vite-plus/blob/main/rfcs/core-version-alignment.md)). pnpm checks `vite` peer ranges against the `vite` alias, so a plugin that supports the bundled Vite passes, and a plugin that does not support it fails.
+
+- Migration does not add `vite` to `peerDependencyRules.allowAny` or `peerDependencyRules.allowedVersions`.
+- Migration removes an existing `vite` entry from both fields, also an entry that you wrote. The only purpose of that entry was to accept the `1.x` core alias. It removes a field, or `peerDependencyRules`, when no entry remains. Other entries stay. The migrate summary names the removed rule.
+- For a preview build (`0.0.0-commit.`) or a Vite+ prerelease core (such as `8.4.0-vp.0`), migration keeps writing the `vite` rule. Package managers do not accept these versions for ranges such as `^8.0.0`.
+- After migration, a plugin whose `vite` peer range excludes the bundled Vite (for example `^7.0.0`) produces a peer warning. With `strictPeerDependencies`, the install fails. Update the plugin, or add a rule scoped to that plugin, such as `allowedVersions: { 'vite-plugin-foo>vite': '*' }`. Migration removes only the plain `vite` key.
+
 **Other rules.**
 
 - Packages that use `vite-plus` without a direct `vite` declaration keep that layout (see [Vite and Overrides](#vite-and-overrides)).
diff --git a/docs/guide/upgrade-project.md b/docs/guide/upgrade-project.md
index 8b32e058fb..efec5a0d4f 100644
--- a/docs/guide/upgrade-project.md
+++ b/docs/guide/upgrade-project.md
@@ -18,7 +18,7 @@ On a project that is already on Vite+, migrate does a toolchain version upgrade
 
 ## Manually Updating
 
-Update `vite-plus` and the `vite` alias to `@voidzero-dev/vite-plus-core` together, keeping the core version aligned with `vite-plus`. Update these entries wherever they are declared in your workspace, including overrides or catalogs, then install dependencies to refresh the lockfile. Also [update the Vitest pin](#updating-the-vitest-pin) to match the bundled version.
+Update `vite-plus` and the `vite` alias to `@voidzero-dev/vite-plus-core` together. The two packages have different versions: the core version encodes the bundled Vite version (for example, `8.3.500` for Vite `8.3.5`). After you update `vite-plus`, set the alias to the version that `vp toolchain vite-plus-core` shows. Update these entries wherever they are declared in your workspace, including overrides or catalogs, then install dependencies to refresh the lockfile. Also [update the Vitest pin](#updating-the-vitest-pin) to match the bundled version.
 
 Without the global CLI, run the `vp` commands on this page through your package manager, for example `pnpm exec vp toolchain vitest`.
 
diff --git a/ecosystem-ci/patch-project.ts b/ecosystem-ci/patch-project.ts
index 6cd4700203..609142db76 100644
--- a/ecosystem-ci/patch-project.ts
+++ b/ecosystem-ci/patch-project.ts
@@ -6,7 +6,7 @@ import { join } from 'node:path';
 import { VITEST_VERSION } from '../packages/cli/src/utils/constants.ts';
 import vitePlusCorePkg from '../packages/core/package.json' with { type: 'json' };
 import { patchNpmxVitestConfig } from './npmx.ts';
-import { ecosystemCiDir, tgzDir, vitePlusTgzVersion } from './paths.ts';
+import { ecosystemCiDir, tgzDir, vitePlusCoreTgzVersion, vitePlusTgzVersion } from './paths.ts';
 import repos from './repo.json' with { type: 'json' };
 import { prepareWebdriverioProject } from './webdriverio.ts';
 
@@ -31,6 +31,7 @@ const cli = process.env.VP_CLI_BIN ?? 'vp';
 // (local-npm-registry.ts), so vp migrate pins and installs the checkout's
 // own version through the standard registry code paths, with no `file:` specs.
 const vitePlusVersion = vitePlusTgzVersion();
+const vitePlusCoreVersion = vitePlusCoreTgzVersion();
 
 const registryScript = join(
   import.meta.dirname,
@@ -319,12 +320,13 @@ const migrateEnv: NodeJS.ProcessEnv = {
   ...registryInfo.env,
   ...(forceFreshMigration ? { VP_FORCE_MIGRATE: '1' } : {}),
   VP_OVERRIDE_PACKAGES: JSON.stringify({
-    vite: `npm:@voidzero-dev/vite-plus-core@${vitePlusVersion}`,
+    vite: `npm:@voidzero-dev/vite-plus-core@${vitePlusCoreVersion}`,
     ...vitestOverrides,
   }),
   // The vp binary was built before the pack step pinned the package versions,
-  // so align the version migrate pins with the tgz the registry serves.
+  // so align the versions migrate pins with the tgz files the registry serves.
   VP_VERSION: vitePlusVersion,
+  VP_CORE_VERSION: vitePlusCoreVersion,
   ...releaseAgeEnv,
 };
 
diff --git a/ecosystem-ci/paths.ts b/ecosystem-ci/paths.ts
index d6e2c3b118..57c504c2b9 100644
--- a/ecosystem-ci/paths.ts
+++ b/ecosystem-ci/paths.ts
@@ -20,11 +20,26 @@ export const tgzDir = join(projectDir, '..', 'tmp', 'tgz');
  * version on a local run.
  */
 export function vitePlusTgzVersion(): string {
+  return tgzVersion(/^vite-plus-(\d.*)\.tgz$/, 'vite-plus-.tgz');
+}
+
+/**
+ * The version of the packed core tgz. It encodes the bundled Vite version, so
+ * it differs from {@link vitePlusTgzVersion} (rfcs/core-version-alignment.md).
+ */
+export function vitePlusCoreTgzVersion(): string {
+  return tgzVersion(
+    /^voidzero-dev-vite-plus-core-(\d.*)\.tgz$/,
+    'voidzero-dev-vite-plus-core-.tgz',
+  );
+}
+
+function tgzVersion(pattern: RegExp, description: string): string {
   const version = readdirSync(tgzDir)
-    .map((entry) => /^vite-plus-(\d.*)\.tgz$/.exec(entry)?.[1])
+    .map((entry) => pattern.exec(entry)?.[1])
     .find(Boolean);
   if (!version) {
-    throw new Error(`No vite-plus-.tgz found in ${tgzDir}`);
+    throw new Error(`No ${description} found in ${tgzDir}`);
   }
   return version;
 }
diff --git a/packages/cli/BUNDLING.md b/packages/cli/BUNDLING.md
index ae3c70c5d5..8dcfa2bcd6 100644
--- a/packages/cli/BUNDLING.md
+++ b/packages/cli/BUNDLING.md
@@ -22,17 +22,21 @@ its subpaths. This gives the CLI and plugins the same dependency name and
 avoids separate core instances under the alias and canonical package name.
 
 `resolve-core.ts` resolves the alias from the selected CLI package and checks
-its core version. It also checks any Vite dependency declared by the command's
-target project. An incidental hoisted peer does not trigger project validation.
-These checks run before Vite or packaging commands start. Keep the canonical
-name in release metadata and alias targets to identify the published package.
+that the core version matches the exact alias in the CLI manifest. The core
+version encodes the bundled Vite version, so it differs from the CLI version
+(see [Core Version](../core/BUNDLING.md#core-version)). It also checks any Vite
+dependency declared by the command's target project. An incidental hoisted peer
+does not trigger project validation. These checks run before Vite or packaging
+commands start. Keep the canonical name in release metadata and alias targets
+to identify the published package.
 
 For npm projects without overrides, pin a declared `vite` alias to the exact
-installed `vite-plus` version. npm can replace a tagged alias such as
-`npm:@voidzero-dev/vite-plus-core@latest` with upstream Vite to satisfy Vitest's
-peer range. The CLI rejects that mismatch and directs users to `vp migrate`,
-which writes an exact alias and the package-manager overrides. The npm fixtures
-check shared core identity with an exact alias and rejection of the tagged layout.
+core version that the installed `vite-plus` uses. npm can replace a tagged
+alias such as `npm:@voidzero-dev/vite-plus-core@latest` with upstream Vite to
+satisfy Vitest's peer range. The CLI rejects that mismatch and directs users to
+`vp migrate`, which writes an exact alias and the package-manager overrides.
+The npm fixtures check shared core identity with an exact alias and rejection
+of the tagged layout.
 
 ## Build Steps
 
diff --git a/packages/cli/README.md b/packages/cli/README.md
index b70e87a3ea..c75fd08e08 100644
--- a/packages/cli/README.md
+++ b/packages/cli/README.md
@@ -198,11 +198,11 @@ If you are manually migrating a project to Vite+, install these dev dependencies
 vp install -D vite-plus
 ```
 
-Add package-manager overrides so that other packages use the Vite+ versions. Alias `vite` to `@voidzero-dev/vite-plus-core`, setting `` to the installed `vite-plus` version. Pin `vitest` to ``, using the bundled version shown by `vp toolchain vitest`. The project and `vp test` then use the same Vitest copy. Without the pin, a dependency or workspace package can install a different Vitest version. The two versions can use separate mocks, `expect` functions, and runner states:
+Add package-manager overrides so that other packages use the Vite+ versions. Alias `vite` to `@voidzero-dev/vite-plus-core`, setting `` to the core version that the installed `vite-plus` uses, shown by `vp toolchain vite-plus-core`. The core version encodes the bundled Vite version (for example, `8.3.500` for Vite `8.3.5`), so package managers check `vite` peer ranges against the bundled Vite. Pin `vitest` to ``, using the bundled version shown by `vp toolchain vitest`. The project and `vp test` then use the same Vitest copy. Without the pin, a dependency or workspace package can install a different Vitest version. The two versions can use separate mocks, `expect` functions, and runner states:
 
 ```json
 "overrides": {
-  "vite": "npm:@voidzero-dev/vite-plus-core@",
+  "vite": "npm:@voidzero-dev/vite-plus-core@",
   "vitest": ""
 }
 ```
@@ -211,7 +211,7 @@ If you are using `pnpm`, add this to your `pnpm-workspace.yaml`:
 
 ```yaml
 overrides:
-  vite@*: npm:@voidzero-dev/vite-plus-core@ # Match the installed vite-plus version
+  vite@*: npm:@voidzero-dev/vite-plus-core@ # Match the version from vp toolchain vite-plus-core
   vitest@*:  # Match the version from vp toolchain vitest
 ```
 
@@ -219,7 +219,7 @@ Or, if you are using Yarn:
 
 ```json
 "resolutions": {
-  "vite": "npm:@voidzero-dev/vite-plus-core@",
+  "vite": "npm:@voidzero-dev/vite-plus-core@",
   "vitest": ""
 }
 ```
diff --git a/packages/cli/src/__tests__/resolve-core.spec.ts b/packages/cli/src/__tests__/resolve-core.spec.ts
index 8e533913dd..3570e15dfb 100644
--- a/packages/cli/src/__tests__/resolve-core.spec.ts
+++ b/packages/cli/src/__tests__/resolve-core.spec.ts
@@ -5,9 +5,14 @@ import { join } from 'node:path';
 
 import { afterEach, beforeEach, describe, expect, it } from 'vitest';
 
+import corePkg from '../../../core/package.json' with { type: 'json' };
 import cliPkg from '../../package.json' with { type: 'json' };
 import { resolveCore } from '../resolve-core.ts';
 
+// The core version encodes the bundled Vite version, so it differs from the
+// CLI version (rfcs/core-version-alignment.md).
+const CORE_VERSION = corePkg.version;
+
 function writePackageJson(directory: string, manifest: Record): void {
   mkdirSync(directory, { recursive: true });
   writeFileSync(join(directory, 'package.json'), JSON.stringify(manifest));
@@ -16,7 +21,7 @@ function writePackageJson(directory: string, manifest: Record):
 function writeCore(
   directory: string,
   name = '@voidzero-dev/vite-plus-core',
-  version = cliPkg.version,
+  version = CORE_VERSION,
 ): void {
   writePackageJson(directory, {
     name,
@@ -27,10 +32,15 @@ function writeCore(
   writeFileSync(join(directory, 'pack.js'), '');
 }
 
-function writeCli(directory: string, version = cliPkg.version): void {
+function writeCli(
+  directory: string,
+  version = cliPkg.version,
+  viteSpec = `npm:@voidzero-dev/vite-plus-core@${CORE_VERSION}`,
+): void {
   writePackageJson(directory, {
     name: 'vite-plus',
     version,
+    dependencies: { vite: viteSpec },
     exports: { './package.json': './package.json' },
   });
 }
@@ -46,7 +56,7 @@ describe('resolveCore', () => {
     root = realpathSync(mkdtempSync(join(tmpdir(), 'vp-core-resolver-')));
     project = join(root, 'project');
     writePackageJson(project, {
-      devDependencies: { vite: `npm:@voidzero-dev/vite-plus-core@${cliPkg.version}` },
+      devDependencies: { vite: `npm:@voidzero-dev/vite-plus-core@${CORE_VERSION}` },
     });
     cliModule = join(root, 'cli', 'dist', 'bin.js');
     bundled = join(root, 'cli', 'node_modules', 'vite');
@@ -88,29 +98,47 @@ describe('resolveCore', () => {
     expect(resolveCore('', project, cliModule)).toBe(join(bundled, 'index.js'));
   });
 
-  it.each(['0.0.0', '0.0.0-commit.1234567'])(
-    'uses the installed CLI manifest after repacking as %s',
-    (version) => {
-      writeCli(join(root, 'cli'), version);
-      writeCore(bundled, '@voidzero-dev/vite-plus-core', version);
-      writeCore(projectCore, '@voidzero-dev/vite-plus-core', version);
-      expect(resolveCore('', project, cliModule)).toBe(join(bundled, 'index.js'));
-      expect(resolveCore('/pack', project, cliModule)).toBe(join(bundled, 'pack.js'));
-    },
-  );
+  it.each([
+    ['0.0.0', '8.3.599'],
+    ['0.0.0-commit.1234567', '0.0.0-commit.1234567'],
+  ])('uses the installed CLI manifest after repacking as %s', (version, coreVersion) => {
+    writeCli(join(root, 'cli'), version, `npm:@voidzero-dev/vite-plus-core@${coreVersion}`);
+    writeCore(bundled, '@voidzero-dev/vite-plus-core', coreVersion);
+    writeCore(projectCore, '@voidzero-dev/vite-plus-core', coreVersion);
+    expect(resolveCore('', project, cliModule)).toBe(join(bundled, 'index.js'));
+    expect(resolveCore('/pack', project, cliModule)).toBe(join(bundled, 'pack.js'));
+  });
 
   it.each(['project', 'bundled'])(
     'still rejects a mismatched %s core after repacking the CLI',
     (location) => {
-      writeCli(join(root, 'cli'), '0.0.0');
-      writeCore(bundled, '@voidzero-dev/vite-plus-core', '0.0.0');
+      writeCli(join(root, 'cli'), '0.0.0', 'npm:@voidzero-dev/vite-plus-core@8.3.599');
+      writeCore(bundled, '@voidzero-dev/vite-plus-core', '8.3.599');
       writeCore(location === 'project' ? projectCore : bundled);
       expect(() => resolveCore('', project, cliModule)).toThrow(
-        `Expected @voidzero-dev/vite-plus-core@0.0.0, but found @voidzero-dev/vite-plus-core@${cliPkg.version}`,
+        `Expected @voidzero-dev/vite-plus-core@8.3.599, but found @voidzero-dev/vite-plus-core@${CORE_VERSION}`,
       );
     },
   );
 
+  it('rejects a core that matches the CLI version instead of its alias', () => {
+    writeCore(bundled, '@voidzero-dev/vite-plus-core', cliPkg.version);
+    expect(() => resolveCore('', project, cliModule)).toThrow(
+      `Expected @voidzero-dev/vite-plus-core@${CORE_VERSION}, but found @voidzero-dev/vite-plus-core@${cliPkg.version}`,
+    );
+  });
+
+  it('checks only the package name for the repository workspace spec', () => {
+    writeCli(join(root, 'cli'), cliPkg.version, 'workspace:@voidzero-dev/vite-plus-core@*');
+    writeCore(bundled, '@voidzero-dev/vite-plus-core', '0.0.0-any');
+    writeCore(projectCore, '@voidzero-dev/vite-plus-core', '0.0.0-any');
+    expect(resolveCore('', project, cliModule)).toBe(join(bundled, 'index.js'));
+    writeCore(bundled, 'vite', '8.2.2');
+    expect(() => resolveCore('', project, cliModule)).toThrow(
+      'Expected @voidzero-dev/vite-plus-core, but found vite@8.2.2',
+    );
+  });
+
   it('does not fall back to the project when the CLI dependency is missing', () => {
     writeCore(projectCore);
     rmSync(bundled, { recursive: true });
diff --git a/packages/cli/src/migration/__tests__/vite-peer-dependency-rule.spec.ts b/packages/cli/src/migration/__tests__/vite-peer-dependency-rule.spec.ts
new file mode 100644
index 0000000000..cf35000c8c
--- /dev/null
+++ b/packages/cli/src/migration/__tests__/vite-peer-dependency-rule.spec.ts
@@ -0,0 +1,275 @@
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { parse as parseYaml } from 'yaml';
+
+import { PackageManager, type WorkspaceInfo } from '../../types/index.js';
+import { VITE_PLUS_OVERRIDE_PACKAGES } from '../../utils/constants.js';
+import { createMigrationReport } from '../report.js';
+
+// Keep the vite-plus spec stable; each test selects the core alias it needs.
+vi.mock('../../utils/constants.js', async (importOriginal) => {
+  const mod = await importOriginal();
+  return {
+    ...mod,
+    VITE_PLUS_VERSION: 'latest',
+    VITE_PLUS_OVERRIDE_PACKAGES: { ...mod.VITE_PLUS_OVERRIDE_PACKAGES },
+  };
+});
+
+const {
+  detectVitePlusBootstrapPending,
+  ensureVitePlusBootstrap,
+  rewriteStandaloneProject,
+  vitePeerDependencyRuleRequired,
+} = await import('../migrator.js');
+
+const STABLE_CORE = 'npm:@voidzero-dev/vite-plus-core@8.3.500';
+const PREVIEW_CORE = 'npm:@voidzero-dev/vite-plus-core@0.0.0-commit.0c515e3';
+
+type PeerDependencyRules = { allowAny?: string[]; allowedVersions?: Record };
+
+function makeWorkspaceInfo(rootDir: string, version = '10.33.0'): WorkspaceInfo {
+  return {
+    rootDir,
+    isMonorepo: false,
+    monorepoScope: '',
+    workspacePatterns: [],
+    parentDirs: [],
+    packageManager: PackageManager.pnpm,
+    packageManagerVersion: version,
+    downloadPackageManager: {
+      name: PackageManager.pnpm,
+      installDir: '/tmp',
+      binPrefix: '/tmp/bin',
+      packageName: PackageManager.pnpm,
+      version,
+    },
+    packages: [],
+  };
+}
+
+function readJson(filePath: string): Record {
+  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
+}
+
+function readWorkspaceRules(projectPath: string): PeerDependencyRules | undefined {
+  const workspace = parseYaml(
+    fs.readFileSync(path.join(projectPath, 'pnpm-workspace.yaml'), 'utf8'),
+  ) as { peerDependencyRules?: PeerDependencyRules };
+  return workspace.peerDependencyRules;
+}
+
+function readPackageJsonRules(projectPath: string): PeerDependencyRules | undefined {
+  const pkg = readJson(path.join(projectPath, 'package.json')) as {
+    pnpm?: { peerDependencyRules?: PeerDependencyRules };
+  };
+  return pkg.pnpm?.peerDependencyRules;
+}
+
+function writeVitePlusProject(projectPath: string, peerDependencyRules: string[]) {
+  fs.writeFileSync(
+    path.join(projectPath, 'package.json'),
+    JSON.stringify({
+      name: 'test',
+      devDependencies: { 'vite-plus': 'catalog:' },
+      devEngines: {
+        packageManager: { name: 'pnpm', version: '10.33.0', onFail: 'download' },
+      },
+    }),
+  );
+  fs.writeFileSync(
+    path.join(projectPath, 'pnpm-workspace.yaml'),
+    [
+      'catalog:',
+      `  vite: ${VITE_PLUS_OVERRIDE_PACKAGES.vite}`,
+      '  vite-plus: latest',
+      'overrides:',
+      "  vite@*: 'catalog:'",
+      ...peerDependencyRules,
+      '',
+    ].join('\n'),
+  );
+}
+
+describe('vitePeerDependencyRuleRequired', () => {
+  it.each([
+    [STABLE_CORE, false],
+    ['npm:@voidzero-dev/vite-plus-core@9.0.0', false],
+    ['npm:@voidzero-dev/vite-plus-core@8.4.0-vp.0', true],
+    ['npm:@voidzero-dev/vite-plus-core@9.0.0-beta.3.vp.0', true],
+    ['npm:@voidzero-dev/vite-plus-core@0.0.0', true],
+    [PREVIEW_CORE, true],
+    ['npm:@voidzero-dev/vite-plus-core@1.4.0', true],
+    ['npm:@voidzero-dev/vite-plus-core@8.3.500+build.1', true],
+    ['npm:@voidzero-dev/vite-plus-core@latest', true],
+    ['file:../voidzero-dev-vite-plus-core-8.3.599.tgz', true],
+    ['https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-core@1891', true],
+  ])('%s requires the rule: %s', (spec, required) => {
+    expect(vitePeerDependencyRuleRequired(spec)).toBe(required);
+  });
+});
+
+describe('pnpm vite peer rule', () => {
+  let tmpDir: string;
+  const savedViteOverride = VITE_PLUS_OVERRIDE_PACKAGES.vite;
+
+  beforeEach(() => {
+    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-vite-peer-rule-'));
+  });
+
+  afterEach(() => {
+    VITE_PLUS_OVERRIDE_PACKAGES.vite = savedViteOverride;
+    fs.rmSync(tmpDir, { recursive: true, force: true });
+  });
+
+  describe('with a stable core version', () => {
+    beforeEach(() => {
+      VITE_PLUS_OVERRIDE_PACKAGES.vite = STABLE_CORE;
+    });
+
+    it('removes the vite rule from pnpm-workspace.yaml and keeps other entries', () => {
+      writeVitePlusProject(tmpDir, [
+        'peerDependencyRules:',
+        '  allowAny:',
+        '    - react',
+        '    - vite',
+        '  allowedVersions:',
+        "    react: '*'",
+        "    vite: '*'",
+      ]);
+      const report = createMigrationReport();
+
+      expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(true);
+      ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir), report);
+
+      expect(readWorkspaceRules(tmpDir)).toEqual({
+        allowAny: ['react'],
+        allowedVersions: { react: '*' },
+      });
+      expect(report.vitePeerDependencyRuleRemoved).toBe(true);
+      expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(false);
+      expect(ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir)).changed).toBe(false);
+    });
+
+    it('deletes peerDependencyRules from pnpm-workspace.yaml when no rule remains', () => {
+      writeVitePlusProject(tmpDir, [
+        'peerDependencyRules:',
+        '  allowAny: [vite, vitest]',
+        '  allowedVersions:',
+        "    vite: '*'",
+        "    vitest: '*'",
+      ]);
+      const report = createMigrationReport();
+
+      ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir), report);
+
+      expect(readWorkspaceRules(tmpDir)).toBeUndefined();
+      expect(report.vitePeerDependencyRuleRemoved).toBe(true);
+      expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(false);
+    });
+
+    it('writes no peer rule for a project without one', () => {
+      writeVitePlusProject(tmpDir, []);
+      const report = createMigrationReport();
+
+      expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(false);
+      ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir), report);
+
+      expect(readWorkspaceRules(tmpDir)).toBeUndefined();
+      expect(report.vitePeerDependencyRuleRemoved).toBe(false);
+    });
+
+    it('removes the vite rule from package.json before pnpm 10.6.2', () => {
+      fs.writeFileSync(
+        path.join(tmpDir, 'package.json'),
+        JSON.stringify({
+          name: 'test',
+          devDependencies: { 'vite-plus': 'latest' },
+          pnpm: {
+            overrides: { react: '18.3.1' },
+            peerDependencyRules: {
+              allowAny: ['react', 'vite'],
+              allowedVersions: { vite: '*' },
+            },
+          },
+        }),
+      );
+      const report = createMigrationReport();
+
+      ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, '10.6.1'), report);
+
+      expect(readPackageJsonRules(tmpDir)).toEqual({ allowAny: ['react'] });
+      expect(report.vitePeerDependencyRuleRemoved).toBe(true);
+    });
+
+    it('deletes package.json peerDependencyRules when no rule remains', () => {
+      fs.writeFileSync(
+        path.join(tmpDir, 'package.json'),
+        JSON.stringify({
+          name: 'test',
+          devDependencies: { 'vite-plus': 'latest' },
+          pnpm: {
+            peerDependencyRules: { allowAny: ['vite'], allowedVersions: { vite: '*' } },
+          },
+        }),
+      );
+
+      ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, '10.6.1'));
+
+      const pkg = readJson(path.join(tmpDir, 'package.json')) as {
+        pnpm: Record;
+      };
+      expect(pkg.pnpm.peerDependencyRules).toBeUndefined();
+      expect(pkg.pnpm.overrides).toEqual({ 'vite@*': STABLE_CORE });
+    });
+
+    it('removes a vite rule during the full migration', () => {
+      fs.writeFileSync(
+        path.join(tmpDir, 'package.json'),
+        JSON.stringify({
+          name: 'app',
+          devDependencies: { vite: '^7.0.0' },
+          pnpm: {
+            peerDependencyRules: {
+              allowAny: ['react', 'vite'],
+              allowedVersions: { react: '*', vite: '*' },
+            },
+          },
+        }),
+      );
+      const report = createMigrationReport();
+
+      rewriteStandaloneProject(tmpDir, makeWorkspaceInfo(tmpDir, '10.6.1'), true, true, report);
+
+      expect(readPackageJsonRules(tmpDir)).toEqual({
+        allowAny: ['react'],
+        allowedVersions: { react: '*' },
+      });
+      expect(report.vitePeerDependencyRuleRemoved).toBe(true);
+    });
+  });
+
+  describe('with a preview core version', () => {
+    beforeEach(() => {
+      VITE_PLUS_OVERRIDE_PACKAGES.vite = PREVIEW_CORE;
+    });
+
+    it('keeps writing the vite rule', () => {
+      writeVitePlusProject(tmpDir, []);
+      const report = createMigrationReport();
+
+      expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(true);
+      ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir), report);
+
+      expect(readWorkspaceRules(tmpDir)).toEqual({
+        allowAny: ['vite'],
+        allowedVersions: { vite: '*' },
+      });
+      expect(report.vitePeerDependencyRuleRemoved).toBe(false);
+      expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(false);
+    });
+  });
+});
diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts
index de19f1893e..cd3828f51b 100644
--- a/packages/cli/src/migration/bin.ts
+++ b/packages/cli/src/migration/bin.ts
@@ -634,6 +634,11 @@ function showMigrationSummary(options: {
   if (report.packageManagerBootstrapConfigured) {
     log(`${styleText('gray', '•')} Package manager settings configured`);
   }
+  if (report.vitePeerDependencyRuleRemoved) {
+    log(
+      `${styleText('gray', '•')} Removed \`vite\` from pnpm peerDependencyRules: the core version now matches the bundled Vite`,
+    );
+  }
   if (suggestFullMigration) {
     log(FULL_MIGRATION_HINT);
   }
diff --git a/packages/cli/src/migration/migrator/catalog.ts b/packages/cli/src/migration/migrator/catalog.ts
index cd6b13e998..cfc1e9a6f7 100644
--- a/packages/cli/src/migration/migrator/catalog.ts
+++ b/packages/cli/src/migration/migrator/catalog.ts
@@ -23,14 +23,16 @@ import {
   isAlignableVitestEcosystemPackage,
   isLegacyWrapperSpec,
   managedOverridePackages,
+  managedPeerDependencyRuleKeys,
+  mergeManagedPnpmPeerDependencyRules,
   pruneLegacyWrapperAliases,
   removeManagedVitestEntry,
-  removeVitestPeerDependencyRule,
   removeYamlMapVitestEntry,
   rewriteMonorepoProject,
   shouldDropProviderOverrideKey,
   workspaceUsesWebdriverioProvider,
 } from '../migrator.ts';
+import { type MigrationReport } from '../report.ts';
 import {
   type PreparedOxlintConfigs,
   LEGACY_WRAPPER_FALLBACK_VERSIONS,
@@ -39,6 +41,7 @@ import {
   VITEST_IS_MANAGED_OVERRIDE,
   isPlainRecord,
   pnpmOverrideKey,
+  vitePeerDependencyRuleRequired,
   type CatalogDependencyResolver,
   type PackageJsonDependencyField,
   type PnpmPackageJsonSettings,
@@ -373,13 +376,19 @@ export function rewritePnpmWorkspaceYaml(
     if (!usesVitest && VITEST_IS_MANAGED_OVERRIDE) {
       allowAny.items = allowAny.items.filter((n) => n.value !== 'vitest');
     }
+    // A `vite` entry only hides real incompatibilities once the core alias
+    // satisfies `vite` peer ranges (see `vitePeerDependencyRuleRequired`).
+    if (!vitePeerDependencyRuleRequired()) {
+      allowAny.items = allowAny.items.filter((n) => n.value !== 'vite');
+    }
+    const peerRuleKeys = managedPeerDependencyRuleKeys(usesVitest);
     const existing = new Set(allowAny.items.map((n) => n.value));
-    for (const key of Object.keys(managedOverridePackages(usesVitest))) {
+    for (const key of peerRuleKeys) {
       if (!existing.has(key)) {
         allowAny.add(scalarString(key));
       }
     }
-    doc.setIn(['peerDependencyRules', 'allowAny'], allowAny);
+    setPeerDependencyRulesField(doc, 'allowAny', allowAny);
 
     // peerDependencyRules.allowedVersions
     // Same guard as above: a malformed non-map `allowedVersions:` value (pnpm
@@ -395,11 +404,14 @@ export function rewritePnpmWorkspaceYaml(
     if (!usesVitest) {
       removeYamlMapVitestEntry(allowedVersions);
     }
-    for (const key of Object.keys(managedOverridePackages(usesVitest))) {
+    if (!vitePeerDependencyRuleRequired()) {
+      allowedVersions.delete('vite');
+    }
+    for (const key of peerRuleKeys) {
       // - vite: '*'
       allowedVersions.set(scalarString(key), scalarString('*'));
     }
-    doc.setIn(['peerDependencyRules', 'allowedVersions'], allowedVersions);
+    setPeerDependencyRulesField(doc, 'allowedVersions', allowedVersions);
 
     // minimumReleaseAgeExclude: exempt the Vite+-managed packages (vite-plus,
     // @voidzero-dev/*, the ox* family, and the vitest family) from the age gate.
@@ -1223,25 +1235,17 @@ export function rewriteRootWorkspacePackageJson(
         // overrides into the new pnpm config.
         dropRemovePackageOverrideKeys(pkg.pnpm?.overrides);
         // Common case: drop a lingering managed `vitest` override before merging.
+        // `mergeManagedPnpmPeerDependencyRules` drops its peer rule.
         if (!workspaceUsesVitest) {
           removeManagedVitestEntry(pkg.pnpm?.overrides, 'pnpm-ranged');
         }
-        if (!workspaceUsesVitest && pkg.pnpm?.peerDependencyRules) {
-          removeVitestPeerDependencyRule(pkg.pnpm.peerDependencyRules);
-        }
         pkg.pnpm = {
           ...pkg.pnpm,
           overrides: mergeManagedPnpmOverrides(pkg.pnpm?.overrides, managed),
-          peerDependencyRules: {
-            ...pkg.pnpm?.peerDependencyRules,
-            allowAny: [
-              ...new Set([...(pkg.pnpm?.peerDependencyRules?.allowAny ?? []), ...overrideKeys]),
-            ],
-            allowedVersions: {
-              ...pkg.pnpm?.peerDependencyRules?.allowedVersions,
-              ...Object.fromEntries(overrideKeys.map((key) => [key, '*'])),
-            },
-          },
+          peerDependencyRules: mergeManagedPnpmPeerDependencyRules(
+            pkg.pnpm?.peerDependencyRules,
+            workspaceUsesVitest,
+          ),
         };
       } else {
         for (const key of [...overrideKeys, ...PROVIDER_OVERRIDE_DROP_NAMES]) {
@@ -1349,6 +1353,26 @@ export function pnpmWorkspaceMinimumReleaseAgeExemptionsPending(projectPath: str
   return PNPM_MINIMUM_RELEASE_AGE_EXCLUDES.some((exclude) => !existing.has(exclude));
 }
 
+// Write one pnpm-workspace.yaml `peerDependencyRules` field, or delete it when
+// no entry remains. An emptied `peerDependencyRules` map is deleted too.
+function setPeerDependencyRulesField(
+  doc: YamlDocument,
+  field: 'allowAny' | 'allowedVersions',
+  node: YAMLSeq | YAMLMap,
+): void {
+  if (node.items.length > 0) {
+    doc.setIn(['peerDependencyRules', field], node);
+    return;
+  }
+  if (doc.hasIn(['peerDependencyRules', field])) {
+    doc.deleteIn(['peerDependencyRules', field]);
+  }
+  const rules = doc.get('peerDependencyRules');
+  if (rules instanceof YAMLMap && rules.items.length === 0) {
+    doc.delete('peerDependencyRules');
+  }
+}
+
 export function readPnpmWorkspacePeerDependencyRules(
   projectPath: string,
 ): { allowAny?: string[]; allowedVersions?: Record } | undefined {
@@ -1362,6 +1386,52 @@ export function readPnpmWorkspacePeerDependencyRules(
   return doc?.peerDependencyRules;
 }
 
+function rulesHaveVite(
+  rules: { allowAny?: unknown; allowedVersions?: unknown } | null | undefined,
+): boolean {
+  return (
+    (Array.isArray(rules?.allowAny) && rules.allowAny.includes('vite')) ||
+    (isPlainRecord(rules?.allowedVersions) && rules.allowedVersions.vite !== undefined)
+  );
+}
+
+// Whether the root pnpm config (the package.json `pnpm` field or
+// pnpm-workspace.yaml) holds a `vite` peer rule.
+export function hasVitePeerDependencyRule(projectPath: string): boolean {
+  if (rulesHaveVite(readPnpmWorkspacePeerDependencyRules(projectPath))) {
+    return true;
+  }
+  const packageJsonPath = path.join(projectPath, 'package.json');
+  if (!fs.existsSync(packageJsonPath)) {
+    return false;
+  }
+  const pkg = readJsonFile(packageJsonPath) as { pnpm?: PnpmPackageJsonSettings };
+  return rulesHaveVite(pkg.pnpm?.peerDependencyRules);
+}
+
+// Capture the `vite` peer rule before a migration step. Call the returned
+// function after the step: it records the rule's removal in the report, so the
+// migrate summary names it (see `vitePeerDependencyRuleRequired`).
+export function trackVitePeerDependencyRuleRemoval(
+  projectPath: string,
+  packageManager: PackageManager,
+  report: MigrationReport | undefined,
+): () => void {
+  if (
+    !report ||
+    packageManager !== PackageManager.pnpm ||
+    vitePeerDependencyRuleRequired() ||
+    !hasVitePeerDependencyRule(projectPath)
+  ) {
+    return () => {};
+  }
+  return () => {
+    if (!hasVitePeerDependencyRule(projectPath)) {
+      report.vitePeerDependencyRuleRemoved = true;
+    }
+  };
+}
+
 export function ensurePnpmWorkspacePackages(
   projectPath: string,
   workspacePatterns: string[],
diff --git a/packages/cli/src/migration/migrator/orchestrators.ts b/packages/cli/src/migration/migrator/orchestrators.ts
index dc4a2c5550..7147aed0ad 100644
--- a/packages/cli/src/migration/migrator/orchestrators.ts
+++ b/packages/cli/src/migration/migrator/orchestrators.ts
@@ -25,6 +25,7 @@ import {
   injectLintTypeCheckDefaults,
   managedOverridePackages,
   mergeManagedPnpmOverrides,
+  mergeManagedPnpmPeerDependencyRules,
   mergeStagedConfigToViteConfig,
   mergeTsdownConfigFile,
   mergeViteConfigFiles,
@@ -40,7 +41,6 @@ import {
   removeLintStagedFromPackageJson,
   removeMigratedOxlintConfigs,
   removeManagedVitestEntry,
-  removeVitestPeerDependencyRule,
   rewriteAllImports,
   rewriteBunCatalog,
   rewriteLintStagedConfigFile,
@@ -52,6 +52,7 @@ import {
   setDirectViteEdge,
   sourceTreeReferencesRetainedVitestModule,
   takePnpmWorkspaceSettings,
+  trackVitePeerDependencyRuleRemoval,
   usesVitestBrowserMode,
   usesWebdriverioProvider,
   workspaceUsesVitestDirectly,
@@ -81,6 +82,11 @@ export function rewriteStandaloneProject(
   }
 
   const packageManager = workspaceInfo.packageManager;
+  const reportVitePeerDependencyRuleRemoval = trackVitePeerDependencyRuleRemoval(
+    projectPath,
+    packageManager,
+    report,
+  );
   const catalogDependencyResolver = createCatalogDependencyResolver(projectPath, packageManager);
   const vitestEcosystemPackages = collectVitestEcosystemInstallDependencyNames(projectPath);
   // Captured before `rewritePackageJson` strips `oxlint`; the import rewriter
@@ -193,13 +199,10 @@ export function rewriteStandaloneProject(
         // whose target is a removed package, before re-merging the user's
         // overrides into the new pnpm config.
         dropRemovePackageOverrideKeys(pkg.pnpm?.overrides);
-        // Common case: drop a lingering managed `vitest` override + its peer
-        // rules before re-merging.
+        // Common case: drop a lingering managed `vitest` override before
+        // re-merging. `mergeManagedPnpmPeerDependencyRules` drops its peer rule.
         if (!usesVitest) {
           removeManagedVitestEntry(pkg.pnpm?.overrides, 'pnpm-ranged');
-          if (pkg.pnpm?.peerDependencyRules) {
-            removeVitestPeerDependencyRule(pkg.pnpm.peerDependencyRules);
-          }
         }
         // Project already has pnpm config in package.json -- keep using it.
         // `mergeManagedPnpmOverrides` writes the range-qualified keys and drops
@@ -208,16 +211,10 @@ export function rewriteStandaloneProject(
         pkg.pnpm = {
           ...pkg.pnpm,
           overrides: mergeManagedPnpmOverrides(pkg.pnpm?.overrides, managed),
-          peerDependencyRules: {
-            ...pkg.pnpm?.peerDependencyRules,
-            allowAny: [
-              ...new Set([...(pkg.pnpm?.peerDependencyRules?.allowAny ?? []), ...overrideKeys]),
-            ],
-            allowedVersions: {
-              ...pkg.pnpm?.peerDependencyRules?.allowedVersions,
-              ...Object.fromEntries(overrideKeys.map((key) => [key, '*'])),
-            },
-          },
+          peerDependencyRules: mergeManagedPnpmPeerDependencyRules(
+            pkg.pnpm?.peerDependencyRules,
+            usesVitest,
+          ),
         };
       } else {
         movedPnpmSettings = takePnpmWorkspaceSettings(pkg);
@@ -345,6 +342,7 @@ export function rewriteStandaloneProject(
   dropDeadOxlintPluginsDependency(projectPath, workspaceInfo.packages, originalOxlintDependencies);
   wrapLazyPluginsInViteConfig(projectPath, silent, report);
   migrateTaskCacheConfigInViteConfig(projectPath, silent, report);
+  reportVitePeerDependencyRuleRemoval();
 }
 
 /**
@@ -357,6 +355,11 @@ export function rewriteMonorepo(
   silent = false,
   report?: MigrationReport,
 ): void {
+  const reportVitePeerDependencyRuleRemoval = trackVitePeerDependencyRuleRemoval(
+    workspaceInfo.rootDir,
+    workspaceInfo.packageManager,
+    report,
+  );
   const catalogDependencyResolver = createCatalogDependencyResolver(
     workspaceInfo.rootDir,
     workspaceInfo.packageManager,
@@ -515,6 +518,7 @@ export function rewriteMonorepo(
     wrapLazyPluginsInViteConfig(projectPath, silent, report);
     migrateTaskCacheConfigInViteConfig(projectPath, silent, report);
   }
+  reportVitePeerDependencyRuleRemoval();
 }
 
 /**
diff --git a/packages/cli/src/migration/migrator/shared.ts b/packages/cli/src/migration/migrator/shared.ts
index 03473942b6..c0d8a1fa00 100644
--- a/packages/cli/src/migration/migrator/shared.ts
+++ b/packages/cli/src/migration/migrator/shared.ts
@@ -3,7 +3,7 @@ import path from 'node:path';
 
 import * as prompts from '@voidzero-dev/vite-plus-prompts';
 import type { OxlintConfig } from 'oxlint';
-import { coerce, satisfies } from 'verkit';
+import { coerce, satisfies, tryParse } from 'verkit';
 
 import { VITEST_VERSION, VITE_PLUS_OVERRIDE_PACKAGES } from '../../utils/constants.ts';
 import { readJsonFile } from '../../utils/json.ts';
@@ -211,6 +211,35 @@ export const VITEST_BROWSER_DEP_NAMES = [
 // and must be left untouched.
 export const VITEST_IS_MANAGED_OVERRIDE = 'vitest' in VITE_PLUS_OVERRIDE_PACKAGES;
 
+const CORE_ALIAS_PREFIX = 'npm:@voidzero-dev/vite-plus-core@';
+
+/**
+ * Whether pnpm still needs the managed `vite` peer rule (`allowAny: [vite]`,
+ * `allowedVersions: { vite: '*' }`) for the given core alias.
+ *
+ * A stable core version encodes the bundled Vite version (`8.3.500` bundles
+ * Vite `8.3.5`, see rfcs/core-version-alignment.md). Package managers check
+ * `vite` peer ranges against it correctly, so the rule would only hide real
+ * incompatibilities. Every other spec still needs the rule: a preview
+ * (`0.0.0-commit.`), a Vite+ prerelease (`8.4.0-vp.0` fails `^8.0.0`), a
+ * `1.x` core from before the alignment, a dist-tag, or a `file:` or URL spec
+ * whose version is unknown.
+ */
+export function vitePeerDependencyRuleRequired(
+  viteSpec: string | undefined = VITE_PLUS_OVERRIDE_PACKAGES.vite,
+): boolean {
+  if (!viteSpec?.startsWith(CORE_ALIAS_PREFIX)) {
+    return true;
+  }
+  const version = tryParse(viteSpec.slice(CORE_ALIAS_PREFIX.length));
+  return (
+    !version ||
+    version.major < 8 ||
+    (version.prerelease?.length ?? 0) > 0 ||
+    (version.build?.length ?? 0) > 0
+  );
+}
+
 // Fallback specs used when normalizing a stale wrapper alias. Real user
 // ranges (e.g. `vitest: ^3.0.0`) are preserved — only the wrapper alias is
 // rewritten. For `vitest`, we substitute the vitest version vite-plus
@@ -334,7 +363,7 @@ function checkPackageVersion(projectPath: string, name: string, minVersion: stri
   return true;
 }
 
-type PnpmPeerDependencyRules = {
+export type PnpmPeerDependencyRules = {
   allowAny?: string[];
   allowedVersions?: Record;
   [key: string]: unknown;
diff --git a/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts b/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts
index 5a4105b168..df9ecae5e5 100644
--- a/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts
+++ b/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts
@@ -27,6 +27,8 @@ import {
   isLegacyWrapperSpec,
   isProtocolPinnedSpec,
   managedOverridePackages,
+  managedPeerDependencyRuleKeys,
+  mergeManagedPnpmPeerDependencyRules,
   migratePnpmSettingsToWorkspaceYaml,
   normalizeVitestPeerCatalogSpec,
   pnpmPackageJsonSettingsPending,
@@ -45,6 +47,7 @@ import {
   rewriteYarnrcYml,
   setDirectViteEdge,
   takePnpmWorkspaceSettings,
+  trackVitePeerDependencyRuleRemoval,
   vitestEcosystemCatalogReferencesPending,
   workspaceUsesVitestDirectly,
   workspaceUsesWebdriverio,
@@ -62,6 +65,7 @@ import {
   managedOverrideKey,
   pnpmMajor,
   pnpmOverrideKey,
+  vitePeerDependencyRuleRequired,
   type CatalogDependencyResolver,
   type ManagedOverrideKeyStyle,
   type PnpmPackageJsonSettings,
@@ -241,8 +245,16 @@ function pnpmPeerDependencyRulesSatisfyVitePlus(
   ) {
     return false;
   }
-  const overrideKeys = Object.keys(managedOverridePackages(usesVitest));
-  return overrideKeys.every((key) => allowAny.has(key) && allowedVersions[key] === '*');
+  // A stale `vite` peer rule is not satisfied once the core alias satisfies
+  // `vite` peer ranges. It must be removed (see `vitePeerDependencyRuleRequired`).
+  if (
+    !vitePeerDependencyRuleRequired() &&
+    (allowAny.has('vite') || allowedVersions.vite !== undefined)
+  ) {
+    return false;
+  }
+  const ruleKeys = managedPeerDependencyRuleKeys(usesVitest);
+  return ruleKeys.every((key) => allowAny.has(key) && allowedVersions[key] === '*');
 }
 
 function npmVitePlusManagedDependenciesPending(
@@ -921,34 +933,19 @@ function ensureOverrideEntries(
 }
 
 function ensurePnpmPeerDependencyRules(pkg: BootstrapPackageJson, usesVitest: boolean): boolean {
-  const overrideKeys = Object.keys(managedOverridePackages(usesVitest));
   pkg.pnpm ??= {};
-  // Common case: drop a lingering managed `vitest` peer rule from the source
-  // shape before re-deriving the managed rules.
-  const seed = { ...pkg.pnpm.peerDependencyRules } as {
-    allowAny?: string[];
-    allowedVersions?: Record;
-  };
-  if (!usesVitest && VITEST_IS_MANAGED_OVERRIDE) {
-    if (Array.isArray(seed.allowAny)) {
-      seed.allowAny = seed.allowAny.filter((key) => key !== 'vitest');
-    }
-    if (seed.allowedVersions) {
-      seed.allowedVersions = { ...seed.allowedVersions };
-      delete seed.allowedVersions.vitest;
-    }
-  }
-  const peerDependencyRules = {
-    ...seed,
-    allowAny: [...new Set([...(seed.allowAny ?? []), ...overrideKeys])],
-    allowedVersions: {
-      ...seed.allowedVersions,
-      ...Object.fromEntries(overrideKeys.map((key) => [key, '*'])),
-    },
-  };
+  const peerDependencyRules = mergeManagedPnpmPeerDependencyRules(
+    pkg.pnpm.peerDependencyRules,
+    usesVitest,
+  );
   const changed =
-    JSON.stringify(pkg.pnpm.peerDependencyRules ?? {}) !== JSON.stringify(peerDependencyRules);
-  pkg.pnpm.peerDependencyRules = peerDependencyRules;
+    JSON.stringify(pkg.pnpm.peerDependencyRules ?? {}) !==
+    JSON.stringify(peerDependencyRules ?? {});
+  if (peerDependencyRules) {
+    pkg.pnpm.peerDependencyRules = peerDependencyRules;
+  } else {
+    delete pkg.pnpm.peerDependencyRules;
+  }
   return changed;
 }
 
@@ -967,6 +964,11 @@ export function ensureVitePlusBootstrap(
   if (!fs.existsSync(packageJsonPath)) {
     return result;
   }
+  const reportVitePeerDependencyRuleRemoval = trackVitePeerDependencyRuleRemoval(
+    projectPath,
+    workspaceInfo.packageManager,
+    report,
+  );
 
   // Shared override/catalog sinks are workspace-wide, so keep vitest managed
   // when any package needs it. Each package's direct vitest dependency is
@@ -1276,6 +1278,7 @@ export function ensureVitePlusBootstrap(
   if (result.changed && report) {
     report.packageManagerBootstrapConfigured = true;
   }
+  reportVitePeerDependencyRuleRemoval();
   return result;
 }
 
diff --git a/packages/cli/src/migration/migrator/vitest-ecosystem.ts b/packages/cli/src/migration/migrator/vitest-ecosystem.ts
index ffff16ac75..cfa1797b25 100644
--- a/packages/cli/src/migration/migrator/vitest-ecosystem.ts
+++ b/packages/cli/src/migration/migrator/vitest-ecosystem.ts
@@ -28,9 +28,11 @@ import {
   VITEST_BROWSER_DEP_NAMES,
   VITEST_IS_MANAGED_OVERRIDE,
   pnpmOverrideKey,
+  vitePeerDependencyRuleRequired,
   type CatalogDependencyResolver,
   type ManagedOverrideKeyStyle,
   type PackageJsonDependencyField,
+  type PnpmPeerDependencyRules,
 } from './shared.ts';
 
 // Official packages that do not declare a required `vitest` peer. Keep them
@@ -557,6 +559,69 @@ export function removeVitestPeerDependencyRule(peerDependencyRules: {
   }
 }
 
+// Remove every `vite` entry from pnpm peerDependencyRules, in place. Once the
+// core version encodes the bundled Vite (see `vitePeerDependencyRuleRequired`),
+// the rule only hides real incompatibilities. Migration cannot tell whether an
+// earlier migration or the user wrote the entry; its only purpose was to accept
+// the `1.x` alias, so both go.
+export function removeVitePeerDependencyRule(peerDependencyRules: {
+  allowAny?: string[];
+  allowedVersions?: Record;
+}): void {
+  if (Array.isArray(peerDependencyRules.allowAny)) {
+    peerDependencyRules.allowAny = peerDependencyRules.allowAny.filter((key) => key !== 'vite');
+  }
+  if (peerDependencyRules.allowedVersions) {
+    delete peerDependencyRules.allowedVersions.vite;
+  }
+}
+
+// The pnpm peerDependencyRules keys vite-plus manages: the managed override
+// keys, minus `vite` once its core alias satisfies `vite` peer ranges.
+export function managedPeerDependencyRuleKeys(usesVitest: boolean): string[] {
+  const keys = Object.keys(managedOverridePackages(usesVitest));
+  return vitePeerDependencyRuleRequired() ? keys : keys.filter((key) => key !== 'vite');
+}
+
+// Re-derive the managed pnpm peerDependencyRules on top of the package.json
+// `pnpm.peerDependencyRules` shape. Returns the new rules, or `undefined` when
+// no rule remains so the caller can drop the field. Does not mutate `rules`.
+export function mergeManagedPnpmPeerDependencyRules(
+  rules: PnpmPeerDependencyRules | undefined,
+  usesVitest: boolean,
+): PnpmPeerDependencyRules | undefined {
+  const seed: PnpmPeerDependencyRules = { ...rules };
+  if (Array.isArray(seed.allowAny)) {
+    seed.allowAny = [...seed.allowAny];
+  }
+  if (seed.allowedVersions) {
+    seed.allowedVersions = { ...seed.allowedVersions };
+  }
+  // Common case: drop a lingering managed `vitest` peer rule.
+  if (!usesVitest) {
+    removeVitestPeerDependencyRule(seed);
+  }
+  if (!vitePeerDependencyRuleRequired()) {
+    removeVitePeerDependencyRule(seed);
+  }
+  const keys = managedPeerDependencyRuleKeys(usesVitest);
+  const next: PnpmPeerDependencyRules = {
+    ...seed,
+    allowAny: [...new Set([...(seed.allowAny ?? []), ...keys])],
+    allowedVersions: {
+      ...seed.allowedVersions,
+      ...Object.fromEntries(keys.map((key) => [key, '*'])),
+    },
+  };
+  if (next.allowAny?.length === 0) {
+    delete next.allowAny;
+  }
+  if (next.allowedVersions && Object.keys(next.allowedVersions).length === 0) {
+    delete next.allowedVersions;
+  }
+  return Object.keys(next).length > 0 ? next : undefined;
+}
+
 // Legacy wrapper package names that may appear as the target of override
 // aliases left over from earlier vite-plus migrations. `@voidzero-dev/vite-plus-test`
 // was deleted; any catalog/override entry still pointing at it is stale.
diff --git a/packages/cli/src/migration/report.ts b/packages/cli/src/migration/report.ts
index ab9d75722f..220d5f83a3 100644
--- a/packages/cli/src/migration/report.ts
+++ b/packages/cli/src/migration/report.ts
@@ -33,6 +33,7 @@ export interface MigrationReport {
   gitHooksConfigured: boolean;
   frameworkShimAdded: boolean;
   packageManagerBootstrapConfigured: boolean;
+  vitePeerDependencyRuleRemoved: boolean;
   dependencyUpgrades: DependencyVersionChange[];
   warnings: string[];
   manualSteps: string[];
@@ -62,6 +63,7 @@ export function createMigrationReport(): MigrationReport {
     gitHooksConfigured: false,
     frameworkShimAdded: false,
     packageManagerBootstrapConfigured: false,
+    vitePeerDependencyRuleRemoved: false,
     dependencyUpgrades: [],
     warnings: [],
     manualSteps: [],
diff --git a/packages/cli/src/resolve-core.ts b/packages/cli/src/resolve-core.ts
index c1b9886445..3941c32a41 100644
--- a/packages/cli/src/resolve-core.ts
+++ b/packages/cli/src/resolve-core.ts
@@ -5,24 +5,44 @@ import { join } from 'node:path';
 import { readNearestPackageJson } from './utils/package.ts';
 
 const CORE_PACKAGE_NAME = '@voidzero-dev/vite-plus-core';
+const CORE_ALIAS_PREFIX = `npm:${CORE_PACKAGE_NAME}@`;
 
-function checkCoreVersion(packageJsonPath: string, expectedVersion: string): void {
+function checkCoreVersion(packageJsonPath: string, expectedVersion: string | undefined): void {
   const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
     name?: string;
     version?: string;
   };
-  if (pkg.name !== CORE_PACKAGE_NAME || pkg.version !== expectedVersion) {
+  if (
+    pkg.name !== CORE_PACKAGE_NAME ||
+    (expectedVersion !== undefined && pkg.version !== expectedVersion)
+  ) {
+    const expected =
+      expectedVersion === undefined ? CORE_PACKAGE_NAME : `${CORE_PACKAGE_NAME}@${expectedVersion}`;
     throw new Error(
-      `Expected ${CORE_PACKAGE_NAME}@${expectedVersion}, but found ${pkg.name}@${pkg.version} at ${packageJsonPath}. ` +
+      `Expected ${expected}, but found ${pkg.name}@${pkg.version} at ${packageJsonPath}. ` +
         'Run `vp migrate` to align the Vite alias, then run `vp install`.',
     );
   }
 }
 
+/**
+ * Read the core version that the selected CLI pins through its `vite` alias.
+ * The core version encodes the bundled Vite version, so it differs from the
+ * CLI version (rfcs/core-version-alignment.md). Packing turns the repository's
+ * `workspace:` spec into the exact alias. Any other spec pins no version.
+ */
+function readExpectedCoreVersion(cliPackageJsonPath: string): string | undefined {
+  const { dependencies } = JSON.parse(readFileSync(cliPackageJsonPath, 'utf8')) as {
+    dependencies?: Record;
+  };
+  const spec = dependencies?.vite;
+  return spec?.startsWith(CORE_ALIAS_PREFIX) ? spec.slice(CORE_ALIAS_PREFIX.length) : undefined;
+}
+
 function checkProjectCoreVersion(
   cwd: string,
   corePackageJsonPath: string,
-  expectedVersion: string,
+  expectedVersion: string | undefined,
 ): void {
   // npm can hoist an upstream Vite peer even when the project only declares
   // vite-plus. Validate Vite only when the nearest package declares it.
@@ -60,9 +80,7 @@ export function resolveCore(
   const cliRequire = createRequire(modulePath);
   // Read the selected CLI's installed manifest. A static JSON import is inlined
   // during the build, before CI and preview packers can stamp a new version.
-  const { version: expectedVersion } = JSON.parse(
-    readFileSync(cliRequire.resolve('vite-plus/package.json'), 'utf8'),
-  ) as { version: string };
+  const expectedVersion = readExpectedCoreVersion(cliRequire.resolve('vite-plus/package.json'));
   let corePackageJsonPath: string;
   try {
     corePackageJsonPath = cliRequire.resolve('vite/package.json');
diff --git a/packages/cli/src/sync-versions/__tests__/plan.spec.ts b/packages/cli/src/sync-versions/__tests__/plan.spec.ts
index dcb9c29956..8b4b4751b1 100644
--- a/packages/cli/src/sync-versions/__tests__/plan.spec.ts
+++ b/packages/cli/src/sync-versions/__tests__/plan.spec.ts
@@ -10,6 +10,7 @@ import {
 
 const toolchain: SyncVersionsToolchain = {
   vitePlus: '0.4.0',
+  vitePlusCore: '8.3.500',
   vitest: '5.0.0',
 };
 
@@ -50,7 +51,7 @@ describe('planSyncVersions', () => {
         '@vitest/coverage-v8': '5.0.0',
         '@vitest/eslint-plugin': '1.6.0',
         '@vitest/coverage-c8': '0.33.0',
-        '@voidzero-dev/vite-plus-core': '0.4.0',
+        '@voidzero-dev/vite-plus-core': '8.3.500',
         'vite-plus': '0.4.0',
         vitest: '5.0.0',
       },
@@ -77,7 +78,7 @@ describe('planSyncVersions', () => {
       devDependencies: {
         '@vitest/ui': 'npm:@vitest/ui@5.0.0',
         'vite-plus': 'npm:@scope/vite-plus-fork@0.3.0',
-        vite: 'npm:@voidzero-dev/vite-plus-core@0.4.0',
+        vite: 'npm:@voidzero-dev/vite-plus-core@8.3.500',
         vitest: 'npm:vitest@5.0.0',
       },
     });
@@ -113,7 +114,7 @@ describe('planSyncVersions', () => {
       catalog: { '@vitest/coverage-v8': '5.0.0' },
       catalogs: {
         toolchain: {
-          vite: 'npm:@voidzero-dev/vite-plus-core@0.4.0',
+          vite: 'npm:@voidzero-dev/vite-plus-core@8.3.500',
           'vite-plus': '0.4.0',
         },
       },
@@ -138,7 +139,7 @@ describe('planSyncVersions', () => {
         .replace("vitest: '^4.1.11'", "vitest: '5.0.0'")
         .replace(
           'vite: "npm:@voidzero-dev/vite-plus-core@0.3.0"',
-          'vite: "npm:@voidzero-dev/vite-plus-core@0.4.0"',
+          'vite: "npm:@voidzero-dev/vite-plus-core@8.3.500"',
         )
         .replace('vite-plus: 0.3.0', 'vite-plus: 0.4.0'),
     );
diff --git a/packages/cli/src/sync-versions/__tests__/protocol.spec.ts b/packages/cli/src/sync-versions/__tests__/protocol.spec.ts
index a7120c3c11..b95c7e61ef 100644
--- a/packages/cli/src/sync-versions/__tests__/protocol.spec.ts
+++ b/packages/cli/src/sync-versions/__tests__/protocol.spec.ts
@@ -6,6 +6,7 @@ const manifest = {
   schemaVersion: 1,
   nodes: [
     { id: 'vite-plus', version: '0.4.0' },
+    { id: 'vite-plus-core', version: '8.3.500' },
     { id: 'vitest', version: '5.0.0' },
   ],
 };
@@ -49,9 +50,10 @@ describe('runSyncVersionsProtocol', () => {
 });
 
 describe('toolchainFromManifest', () => {
-  it('reads the exact Vite+ and Vitest versions', () => {
+  it('reads the exact Vite+, core, and Vitest versions', () => {
     expect(toolchainFromManifest(manifest)).toEqual({
       vitePlus: '0.4.0',
+      vitePlusCore: '8.3.500',
       vitest: '5.0.0',
     });
   });
@@ -64,6 +66,14 @@ describe('toolchainFromManifest', () => {
       schemaVersion: 1,
       nodes: [
         { id: 'vite-plus', version: '0.4.0' },
+        { id: 'vitest', version: '5.0.0' },
+      ],
+    },
+    {
+      schemaVersion: 1,
+      nodes: [
+        { id: 'vite-plus', version: '0.4.0' },
+        { id: 'vite-plus-core', version: '8.3.500' },
         { id: 'vitest', version: '' },
       ],
     },
diff --git a/packages/cli/src/sync-versions/plan.ts b/packages/cli/src/sync-versions/plan.ts
index 72258b163b..b87128598b 100644
--- a/packages/cli/src/sync-versions/plan.ts
+++ b/packages/cli/src/sync-versions/plan.ts
@@ -88,6 +88,8 @@ export type SyncVersionsManifestSnapshot = SyncVersionsRequestV1['manifests'][nu
 
 export interface SyncVersionsToolchain {
   vitePlus: string;
+  /** Encodes the bundled Vite version, so it differs from `vitePlus`. */
+  vitePlusCore: string;
   vitest: string;
 }
 
@@ -141,9 +143,12 @@ function parsePackageJson(contents: string): Record {
 }
 
 function targetVersion(name: string, toolchain: SyncVersionsToolchain): string | undefined {
-  if (name === 'vite-plus' || name === '@voidzero-dev/vite-plus-core') {
+  if (name === 'vite-plus') {
     return toolchain.vitePlus;
   }
+  if (name === '@voidzero-dev/vite-plus-core') {
+    return toolchain.vitePlusCore;
+  }
   if (name === 'vitest' || isAlignableVitestEcosystemPackage(name)) {
     return toolchain.vitest;
   }
diff --git a/packages/cli/src/sync-versions/protocol.ts b/packages/cli/src/sync-versions/protocol.ts
index f6cf1e78f8..d2b1f52357 100644
--- a/packages/cli/src/sync-versions/protocol.ts
+++ b/packages/cli/src/sync-versions/protocol.ts
@@ -35,6 +35,7 @@ export function toolchainFromManifest(input: unknown): SyncVersionsToolchain {
   const manifest = toolchainManifestSchema.parse(input);
   return {
     vitePlus: requiredToolVersion(manifest.nodes, 'vite-plus'),
+    vitePlusCore: requiredToolVersion(manifest.nodes, 'vite-plus-core'),
     vitest: requiredToolVersion(manifest.nodes, 'vitest'),
   };
 }
diff --git a/packages/cli/src/utils/__tests__/constants.spec.ts b/packages/cli/src/utils/__tests__/constants.spec.ts
index 5a2b514ef1..5570a483a0 100644
--- a/packages/cli/src/utils/__tests__/constants.spec.ts
+++ b/packages/cli/src/utils/__tests__/constants.spec.ts
@@ -1,5 +1,6 @@
 import { afterEach, describe, expect, it, vi } from 'vitest';
 
+import corePkg from '../../../../core/package.json' with { type: 'json' };
 import cliPkg from '../../../package.json' with { type: 'json' };
 
 describe('Vite+ dependency versions', () => {
@@ -8,19 +9,61 @@ describe('Vite+ dependency versions', () => {
     vi.resetModules();
   });
 
-  it('uses the concrete CLI version for vite-plus and vite-plus-core by default', async () => {
+  it('uses the concrete CLI and core versions by default', async () => {
     vi.stubEnv('VP_VERSION', '');
+    vi.stubEnv('VP_CORE_VERSION', '');
     vi.stubEnv('VP_OVERRIDE_PACKAGES', '');
     vi.resetModules();
 
-    const { VITE_PLUS_OVERRIDE_PACKAGES, VITE_PLUS_VERSION } = await import('../constants.js');
+    const { VITE_PLUS_CORE_VERSION, VITE_PLUS_OVERRIDE_PACKAGES, VITE_PLUS_VERSION } =
+      await import('../constants.js');
 
     expect(VITE_PLUS_VERSION).toBe(cliPkg.version);
+    expect(VITE_PLUS_CORE_VERSION).toBe(corePkg.version);
     expect(VITE_PLUS_OVERRIDE_PACKAGES.vite).toBe(
-      `npm:@voidzero-dev/vite-plus-core@${cliPkg.version}`,
+      `npm:@voidzero-dev/vite-plus-core@${corePkg.version}`,
     );
   });
 
+  it('keeps the core version for a release VP_VERSION', async () => {
+    vi.stubEnv('VP_VERSION', '1.0.1');
+    vi.stubEnv('VP_CORE_VERSION', '');
+    vi.stubEnv('VP_OVERRIDE_PACKAGES', '');
+    vi.resetModules();
+
+    const { VITE_PLUS_CORE_VERSION } = await import('../constants.js');
+
+    expect(VITE_PLUS_CORE_VERSION).toBe(corePkg.version);
+  });
+
+  it.each(['0.0.0', '0.0.0-commit.0c515e3'])(
+    'uses a preview VP_VERSION %s for the core too',
+    async (version) => {
+      vi.stubEnv('VP_VERSION', version);
+      vi.stubEnv('VP_CORE_VERSION', '');
+      vi.stubEnv('VP_OVERRIDE_PACKAGES', '');
+      vi.resetModules();
+
+      const { VITE_PLUS_OVERRIDE_PACKAGES } = await import('../constants.js');
+
+      expect(VITE_PLUS_OVERRIDE_PACKAGES.vite).toBe(`npm:@voidzero-dev/vite-plus-core@${version}`);
+    },
+  );
+
+  it('prefers VP_CORE_VERSION over a preview VP_VERSION', async () => {
+    vi.stubEnv('VP_VERSION', '0.0.0');
+    vi.stubEnv('VP_CORE_VERSION', '8.3.599');
+    vi.stubEnv('VP_OVERRIDE_PACKAGES', '');
+    vi.resetModules();
+
+    const { VITE_PLUS_CORE_VERSION, VITE_PLUS_OVERRIDE_PACKAGES, VITE_PLUS_VERSION } =
+      await import('../constants.js');
+
+    expect(VITE_PLUS_VERSION).toBe('0.0.0');
+    expect(VITE_PLUS_CORE_VERSION).toBe('8.3.599');
+    expect(VITE_PLUS_OVERRIDE_PACKAGES.vite).toBe('npm:@voidzero-dev/vite-plus-core@8.3.599');
+  });
+
   it('preserves explicit prerelease overrides', async () => {
     const vitePlusUrl = 'https://pkg.pr.new/voidzero-dev/vite-plus@1891';
     const viteCoreUrl =
diff --git a/packages/cli/src/utils/constants.ts b/packages/cli/src/utils/constants.ts
index 6338fc0ea6..cb71dcfbe2 100644
--- a/packages/cli/src/utils/constants.ts
+++ b/packages/cli/src/utils/constants.ts
@@ -1,10 +1,18 @@
 import { createRequire } from 'node:module';
 
+import corePkg from '../../../core/package.json' with { type: 'json' };
 import cliPkg from '../../package.json' with { type: 'json' };
 
 export const VITE_PLUS_NAME = 'vite-plus';
 export const VITE_PLUS_VERSION = process.env.VP_VERSION || cliPkg.version;
 
+// The core version encodes the bundled Vite version (rfcs/core-version-alignment.md),
+// so it differs from the vite-plus version. Preview builds publish every package
+// at one `0.0.0-
` version, so a preview VP_VERSION also selects the core.
+export const VITE_PLUS_CORE_VERSION =
+  process.env.VP_CORE_VERSION ||
+  (process.env.VP_VERSION?.startsWith('0.0.0') ? process.env.VP_VERSION : corePkg.version);
+
 // Mirrors Vite's DEFAULT_CONFIG_FILES order so readers and writers target the same file.
 export const VITE_CONFIG_FILES = [
   'vite.config.js',
@@ -27,7 +35,7 @@ export const TSDOWN_MIGRATION_SKILL_URL =
 export const VITE_PLUS_OVERRIDE_PACKAGES: Record = process.env.VP_OVERRIDE_PACKAGES
   ? JSON.parse(process.env.VP_OVERRIDE_PACKAGES)
   : {
-      vite: `npm:@voidzero-dev/vite-plus-core@${VITE_PLUS_VERSION}`,
+      vite: `npm:@voidzero-dev/vite-plus-core@${VITE_PLUS_CORE_VERSION}`,
       // The runner inlines its assertion and runner state. Its remaining
       // official dependencies use exact versions; community providers and
       // Istanbul internals have their own version ranges.
diff --git a/packages/core/BUNDLING.md b/packages/core/BUNDLING.md
index e946fb9f3a..814b075f8c 100644
--- a/packages/core/BUNDLING.md
+++ b/packages/core/BUNDLING.md
@@ -156,7 +156,7 @@ The `rolldown_webcontainer_fallback` CLI snapshot executes the packed binding lo
 
 ### Release Build: Native Binding Rewriting
 
-During release builds (`RELEASE_BUILD=1`), `bundleRolldown()` rewrites Rolldown's native binding requires through `build-support/rewrite-rolldown-binding.ts`. For every platform in the CLI's `napi.targets` (mapped to napi platform suffixes with `parseTriple`), the loader's `@rolldown/binding-` requires become `@voidzero-dev/vite-plus-`, and that branch's version guard switches from the Rolldown version to core's version, which is what the Vite+ platform packages are published as.
+During release builds (`RELEASE_BUILD=1`), `bundleRolldown()` rewrites Rolldown's native binding requires through `build-support/rewrite-rolldown-binding.ts`. For every platform in the CLI's `napi.targets` (mapped to napi platform suffixes with `parseTriple`), the loader's `@rolldown/binding-` requires become `@voidzero-dev/vite-plus-`, and that branch's version guard switches from the Rolldown version to the CLI version, which is what the Vite+ platform packages are published as.
 
 **Platform-specific binding rewrites**, one per `napi.targets` entry in `packages/cli/package.json` (see the target table in [CLI Package Bundling](../cli/BUNDLING.md#napi-targets)), for example:
 
@@ -171,7 +171,7 @@ Specifiers for platforms Vite+ does not ship (android, freebsd, the `wasm32-wasi
 
 1. **Self-contained distribution** - Users don't need to install separate `@rolldown/binding-*` packages
 2. **Declared dependency graph** - Core resolves the binding through its own `optionalDependencies` (injected during publish by `packages/cli/publish-native-addons.ts`), so pnpm's global virtual store and Yarn PnP work without hidden hoisting, and core no longer requires back into `vite-plus`
-3. **Version alignment** - Each rewritten branch's guard checks the platform package version against core's version; both are published lockstep
+3. **Version alignment** - Each rewritten branch's guard checks the platform package version against the CLI version, which the platform packages use. The core version differs (see [Core Version](#core-version))
 
 **Resolution chain**:
 
@@ -316,7 +316,8 @@ dist/
 1. Update the Vite pin in [`packages/tools/.upstream-versions.json`](../tools/.upstream-versions.json) and review the resolver patch against the new source
 2. Follow the [upstream sync and build instructions](../../CONTRIBUTING.md#pull-upstream-dependencies) to prepare the source, regenerate Vite types, and rebuild the packages
 3. Verify `bundledVersions.vite` in `package.json` is updated
-4. Test with `pnpm test`
+4. Set `version` in `package.json` to the encoded version for the new Vite version, for example `8.4.200` for Vite `8.4.2` (see [Core Version](#core-version)). The upgrade-deps workflow does this step automatically
+5. Test with `pnpm test`
 
 ### Updating Rolldown
 
@@ -405,3 +406,20 @@ The `bundledVersions` field in `package.json` records the exact versions of bund
 ```
 
 This is automatically updated by `mergePackageJson()` during each build.
+
+### Core Version
+
+The core version encodes `bundledVersions.vite`, so it differs from the `vite-plus` version. Package managers compare the version of the `vite` alias target with the `vite` peer ranges of plugins, so the encoded version makes those checks match the bundled Vite. See [the RFC](../../rfcs/core-version-alignment.md) for the design.
+
+| Release                                 | Core version                                       | Example                          |
+| --------------------------------------- | -------------------------------------------------- | -------------------------------- |
+| Stable `vite-plus`                      | `..` | Vite `8.3.5`, `n = 1`: `8.3.501` |
+| `vite-plus` prerelease, stable Vite     | `-vp.`                | `8.4.0-vp.0`                     |
+| `vite-plus` prerelease, Vite prerelease | `.vp.`                            | `9.0.0-beta.3.vp.0`              |
+| Local and CI builds                     | `n = 99`                                           | `8.3.599`                        |
+
+`n` counts the stable releases on one Vite version, from 0 to 98. `k` counts the prereleases on one base. [`packages/tools/src/core-version.ts`](../tools/src/core-version.ts) implements the scheme:
+
+- `prepare_release.yml` runs `core-version.ts next` to select the next unpublished version from the registry.
+- `.github/scripts/upgrade-deps.ts` resets `version` to `n = 0` when the bundled Vite changes.
+- `__tests__/core-version.spec.ts` checks that the committed `version` encodes `bundledVersions.vite`.
diff --git a/packages/core/__tests__/core-version.spec.ts b/packages/core/__tests__/core-version.spec.ts
new file mode 100644
index 0000000000..42714fea4a
--- /dev/null
+++ b/packages/core/__tests__/core-version.spec.ts
@@ -0,0 +1,12 @@
+import { describe, expect, it } from 'vite-plus/test';
+
+import cliPkg from '../../cli/package.json' with { type: 'json' };
+import { assertCoreVersionMatchesVite } from '../../tools/src/core-version.ts';
+import corePkg from '../package.json' with { type: 'json' };
+
+describe('core version', () => {
+  // The preview flow stamps `0.0.0-commit.` on every package.
+  it.skipIf(corePkg.version.startsWith('0.0.0-commit.'))('encodes the bundled Vite version', () => {
+    expect(() => assertCoreVersionMatchesVite(corePkg, cliPkg.version)).not.toThrow();
+  });
+});
diff --git a/packages/core/build-support/rewrite-rolldown-binding.ts b/packages/core/build-support/rewrite-rolldown-binding.ts
index f5d1bb35ef..998760cff6 100644
--- a/packages/core/build-support/rewrite-rolldown-binding.ts
+++ b/packages/core/build-support/rewrite-rolldown-binding.ts
@@ -21,7 +21,7 @@ export interface RewriteRolldownBindingOptions {
    * are left on `@rolldown/binding-*` and keep their upstream version guards.
    */
   platformSuffixes: ReadonlySet;
-  /** Version the platform packages are published as (core's own version). */
+  /** Version the platform packages are published as (the CLI version). */
   version: string;
 }
 
diff --git a/packages/core/build.ts b/packages/core/build.ts
index c3bf11b33a..44cc6bb168 100644
--- a/packages/core/build.ts
+++ b/packages/core/build.ts
@@ -414,7 +414,9 @@ async function bundleRolldown() {
         const result = rewriteRolldownBindingRequires(source, {
           packageName: cliPkgJson.napi.packageName,
           platformSuffixes: vitePlusPlatformSuffixes,
-          version: pkgJson.version,
+          // The platform packages publish at the CLI version. The core version
+          // encodes the bundled Vite version instead (rfcs/core-version-alignment.md).
+          version: cliPkgJson.version,
         });
         source = result.source;
         for (const suffix of result.rewrittenSuffixes) {
diff --git a/packages/core/package.json b/packages/core/package.json
index 2bb0583d8d..33833f0297 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@voidzero-dev/vite-plus-core",
-  "version": "1.5.1",
+  "version": "8.3.700",
   "description": "The Unified Toolchain for the Web",
   "homepage": "https://viteplus.dev/guide",
   "bugs": {
diff --git a/packages/tools/src/__tests__/core-version.spec.ts b/packages/tools/src/__tests__/core-version.spec.ts
new file mode 100644
index 0000000000..961aa5c73f
--- /dev/null
+++ b/packages/tools/src/__tests__/core-version.spec.ts
@@ -0,0 +1,149 @@
+import { describe, expect, test } from 'vitest';
+
+import {
+  assertCoreVersionMatchesVite,
+  decodeCoreVersion,
+  encodeCoreVersion,
+  nextCoreVersion,
+} from '../core-version.ts';
+
+const PUBLISHED_BEFORE_ALIGNMENT = ['0.1.21-alpha.7', '0.0.2-test.1', '1.0.0-rc.0', '1.4.0'];
+
+describe('encodeCoreVersion', () => {
+  test('encodes the Vite patch and the release counter in the patch', () => {
+    expect(encodeCoreVersion('8.3.5', 0)).toBe('8.3.500');
+    expect(encodeCoreVersion('8.3.5', 1)).toBe('8.3.501');
+    expect(encodeCoreVersion('8.4.0', 0)).toBe('8.4.0');
+    expect(encodeCoreVersion('8.4.1', 0)).toBe('8.4.100');
+    expect(encodeCoreVersion('8.3.5', 99)).toBe('8.3.599');
+  });
+
+  test('rejects a Vite prerelease and an out-of-range counter', () => {
+    expect(() => encodeCoreVersion('9.0.0-beta.3', 0)).toThrow('Vite prerelease');
+    expect(() => encodeCoreVersion('8.3.5', 100)).toThrow('from 0 to 99');
+    expect(() => encodeCoreVersion('8.3.5', -1)).toThrow('from 0 to 99');
+  });
+});
+
+describe('decodeCoreVersion', () => {
+  test('decodes stable and prerelease core versions', () => {
+    expect(decodeCoreVersion('8.3.500')).toEqual({ vite: '8.3.5', n: 0 });
+    expect(decodeCoreVersion('8.3.501')).toEqual({ vite: '8.3.5', n: 1 });
+    expect(decodeCoreVersion('8.4.1')).toEqual({ vite: '8.4.0', n: 1 });
+    expect(decodeCoreVersion('8.4.0-vp.2')).toEqual({ vite: '8.4.0', n: 0, k: 2 });
+    expect(decodeCoreVersion('9.0.0-beta.3.vp.1')).toEqual({ vite: '9.0.0-beta.3', k: 1 });
+  });
+
+  test('rejects prereleases without the vp counter', () => {
+    expect(() => decodeCoreVersion('0.0.0-commit.0c515e3')).toThrow('not an encoded');
+    expect(() => decodeCoreVersion('8.4.0-rc.0')).toThrow('not an encoded');
+    expect(() => decodeCoreVersion('8.4.0-vp.x')).toThrow('not an encoded');
+    expect(() => decodeCoreVersion('latest')).toThrow('Invalid version');
+  });
+});
+
+describe('nextCoreVersion', () => {
+  test('starts the counter at 0 for a new Vite version', () => {
+    expect(
+      nextCoreVersion({ vite: '8.3.5', vitePlus: '1.5.0', published: PUBLISHED_BEFORE_ALIGNMENT }),
+    ).toBe('8.3.500');
+  });
+
+  test('increments the counter for each stable release on one Vite version', () => {
+    const published = [...PUBLISHED_BEFORE_ALIGNMENT, '8.3.500', '8.3.501', '8.3.600'];
+    expect(nextCoreVersion({ vite: '8.3.5', vitePlus: '1.5.2', published })).toBe('8.3.502');
+    expect(nextCoreVersion({ vite: '8.3.6', vitePlus: '1.6.1', published })).toBe('8.3.601');
+    expect(nextCoreVersion({ vite: '8.4.0', vitePlus: '1.7.0', published })).toBe('8.4.0');
+  });
+
+  test('ignores prereleases and local versions for the stable counter', () => {
+    const published = ['8.4.0-vp.0', '8.4.0-vp.1', '8.4.99'];
+    expect(nextCoreVersion({ vite: '8.4.0', vitePlus: '1.7.0', published })).toBe('8.4.0');
+  });
+
+  test('gives each Vite+ prerelease on a stable Vite a unique vp counter', () => {
+    expect(nextCoreVersion({ vite: '8.4.0', vitePlus: '1.7.0-rc.0', published: [] })).toBe(
+      '8.4.0-vp.0',
+    );
+    expect(
+      nextCoreVersion({ vite: '8.4.0', vitePlus: '1.8.0-rc.0', published: ['8.4.0-vp.0'] }),
+    ).toBe('8.4.0-vp.1');
+    expect(
+      nextCoreVersion({
+        vite: '8.4.0',
+        vitePlus: '1.8.0-rc.0',
+        published: ['8.4.0', '8.4.0-vp.0', '8.4.0-vp.1'],
+      }),
+    ).toBe('8.4.1-vp.0');
+  });
+
+  test('appends the vp counter to a bundled Vite prerelease', () => {
+    expect(nextCoreVersion({ vite: '9.0.0-beta.3', vitePlus: '2.0.0-beta.0', published: [] })).toBe(
+      '9.0.0-beta.3.vp.0',
+    );
+    expect(
+      nextCoreVersion({
+        vite: '9.0.0-beta.3',
+        vitePlus: '2.0.0-beta.1',
+        published: ['9.0.0-beta.2.vp.4', '9.0.0-beta.3.vp.0'],
+      }),
+    ).toBe('9.0.0-beta.3.vp.1');
+  });
+
+  test('rejects a stable vite-plus release on a Vite prerelease', () => {
+    expect(() =>
+      nextCoreVersion({ vite: '9.0.0-beta.3', vitePlus: '2.0.0', published: [] }),
+    ).toThrow('cannot bundle the Vite prerelease');
+  });
+
+  test('fails when the release counter is exhausted', () => {
+    const published = Array.from({ length: 99 }, (_, n) => encodeCoreVersion('8.3.5', n));
+    expect(() => nextCoreVersion({ vite: '8.3.5', vitePlus: '1.9.0', published })).toThrow(
+      'the maximum',
+    );
+  });
+});
+
+function manifest(version: string, vite = '8.3.5') {
+  return { version, bundledVersions: { vite } };
+}
+
+describe('assertCoreVersionMatchesVite', () => {
+  test('accepts a core version that encodes the bundled Vite', () => {
+    expect(() => assertCoreVersionMatchesVite(manifest('8.3.500'), '1.5.0')).not.toThrow();
+    expect(() => assertCoreVersionMatchesVite(manifest('8.3.599'), '0.0.0')).not.toThrow();
+    expect(() =>
+      assertCoreVersionMatchesVite(manifest('8.4.0-vp.1', '8.4.0'), '1.7.0-rc.1'),
+    ).not.toThrow();
+    expect(() =>
+      assertCoreVersionMatchesVite(manifest('9.0.0-beta.3.vp.0', '9.0.0-beta.3'), '2.0.0-beta.0'),
+    ).not.toThrow();
+  });
+
+  test('rejects a core version that encodes another Vite version', () => {
+    expect(() => assertCoreVersionMatchesVite(manifest('8.3.400'), '1.5.0')).toThrow(
+      'Set its version to 8.3.500',
+    );
+    expect(() => assertCoreVersionMatchesVite(manifest('1.4.0'), '1.4.0')).toThrow(
+      'encodes Vite 1.4.0',
+    );
+  });
+
+  test('rejects mixed prerelease and stable versions', () => {
+    expect(() => assertCoreVersionMatchesVite(manifest('8.3.500'), '1.5.0-rc.0')).toThrow(
+      'both be prereleases',
+    );
+    expect(() => assertCoreVersionMatchesVite(manifest('8.3.500-vp.0'), '1.5.0')).toThrow(
+      'both be prereleases',
+    );
+  });
+
+  test('rejects the local build counter for a release', () => {
+    expect(() =>
+      assertCoreVersionMatchesVite(manifest('8.3.599'), '1.5.0', { release: true }),
+    ).toThrow('reserved for local builds');
+    expect(() =>
+      assertCoreVersionMatchesVite(manifest('8.3.598'), '1.5.0', { release: true }),
+    ).not.toThrow();
+  });
+});
diff --git a/packages/tools/src/core-version.ts b/packages/tools/src/core-version.ts
new file mode 100644
index 0000000000..6b59de36ef
--- /dev/null
+++ b/packages/tools/src/core-version.ts
@@ -0,0 +1,278 @@
+/**
+ * Version scheme of `@voidzero-dev/vite-plus-core` (rfcs/core-version-alignment.md).
+ *
+ * The core version encodes the bundled Vite version, so package managers check
+ * `vite` peer ranges against the alias `vite: npm:@voidzero-dev/vite-plus-core@...`
+ * correctly:
+ *
+ *   stable Vite+ release:          ..
+ *   Vite+ prerelease, stable Vite: -vp.
+ *   Vite+ prerelease, Vite pre:    .vp.
+ *
+ * `n` counts the stable Vite+ releases on one Vite version (0 to 98). `n = 99`
+ * is reserved for local and CI builds, so they never collide with a published
+ * version. `k` counts the Vite+ prereleases on one base.
+ *
+ * Usage:
+ *   node packages/tools/src/core-version.ts next --vite-plus 
+ *   node packages/tools/src/core-version.ts local
+ *   node packages/tools/src/core-version.ts check [--release]
+ */
+
+import { execFileSync } from 'node:child_process';
+import { readFileSync } from 'node:fs';
+import { parseArgs } from 'node:util';
+
+export const CORE_PACKAGE_NAME = '@voidzero-dev/vite-plus-core';
+export const MAX_RELEASE_COUNTER = 98;
+export const LOCAL_BUILD_COUNTER = 99;
+
+const PRERELEASE_TAG = 'vp';
+const VERSION_RE =
+  /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?$/;
+
+interface ParsedVersion {
+  major: number;
+  minor: number;
+  patch: number;
+  prerelease: string[];
+}
+
+export interface DecodedCoreVersion {
+  /** The bundled Vite version. */
+  vite: string;
+  /** The stable release counter. Undefined on a Vite prerelease. */
+  n?: number;
+  /** The Vite+ prerelease counter. Undefined on a stable core version. */
+  k?: number;
+}
+
+function parseVersion(version: string): ParsedVersion {
+  const match = VERSION_RE.exec(version);
+  if (!match) {
+    throw new Error(`Invalid version: ${version}`);
+  }
+  return {
+    major: Number(match[1]),
+    minor: Number(match[2]),
+    patch: Number(match[3]),
+    prerelease: match[4] ? match[4].split('.') : [],
+  };
+}
+
+export function isPrereleaseVersion(version: string): boolean {
+  return parseVersion(version).prerelease.length > 0;
+}
+
+/** Returns the stable core version for release counter `n` on a stable Vite. */
+export function encodeCoreVersion(vite: string, n: number): string {
+  const { major, minor, patch, prerelease } = parseVersion(vite);
+  if (prerelease.length > 0) {
+    throw new Error(`A stable core version cannot encode the Vite prerelease ${vite}`);
+  }
+  if (!Number.isInteger(n) || n < 0 || n > LOCAL_BUILD_COUNTER) {
+    throw new Error(`The release counter must be an integer from 0 to ${LOCAL_BUILD_COUNTER}`);
+  }
+  return `${major}.${minor}.${patch * 100 + n}`;
+}
+
+/** Decodes a core version into the bundled Vite version and its counters. */
+export function decodeCoreVersion(core: string): DecodedCoreVersion {
+  const { major, minor, patch, prerelease } = parseVersion(core);
+  const stableVite = `${major}.${minor}.${Math.floor(patch / 100)}`;
+  if (prerelease.length === 0) {
+    return { vite: stableVite, n: patch % 100 };
+  }
+  const k = prerelease.at(-1)!;
+  if (prerelease.at(-2) !== PRERELEASE_TAG || !/^(?:0|[1-9]\d*)$/.test(k)) {
+    throw new Error(`${core} is not an encoded core version`);
+  }
+  if (prerelease.length === 2) {
+    return { vite: stableVite, n: patch % 100, k: Number(k) };
+  }
+  return {
+    vite: `${major}.${minor}.${patch}-${prerelease.slice(0, -2).join('.')}`,
+    k: Number(k),
+  };
+}
+
+function tryDecodeCoreVersion(core: string): DecodedCoreVersion | undefined {
+  try {
+    return decodeCoreVersion(core);
+  } catch {
+    return undefined;
+  }
+}
+
+function nextCounter(values: Iterable): number {
+  let next = 0;
+  for (const value of values) {
+    next = Math.max(next, value + 1);
+  }
+  return next;
+}
+
+/**
+ * Returns the core version for a release.
+ *
+ * @param vite - The bundled Vite version.
+ * @param vitePlus - The `vite-plus` version of the release.
+ * @param published - Every published core version. Versions that do not decode
+ *   (the `0.x` and `1.x` lines) are ignored.
+ */
+export function nextCoreVersion({
+  vite,
+  vitePlus,
+  published,
+}: {
+  vite: string;
+  vitePlus: string;
+  published: readonly string[];
+}): string {
+  const isVitePlusPrerelease = isPrereleaseVersion(vitePlus);
+  const decoded = published.flatMap((version) => {
+    const result = tryDecodeCoreVersion(version);
+    return result ? [result] : [];
+  });
+
+  if (isPrereleaseVersion(vite)) {
+    if (!isVitePlusPrerelease) {
+      throw new Error(`The stable vite-plus ${vitePlus} cannot bundle the Vite prerelease ${vite}`);
+    }
+    const k = nextCounter(
+      decoded.flatMap((entry) => (entry.vite === vite && entry.k !== undefined ? [entry.k] : [])),
+    );
+    return `${vite}.${PRERELEASE_TAG}.${k}`;
+  }
+
+  const n = nextCounter(
+    decoded.flatMap((entry) =>
+      entry.vite === vite &&
+      entry.k === undefined &&
+      entry.n !== undefined &&
+      entry.n <= MAX_RELEASE_COUNTER
+        ? [entry.n]
+        : [],
+    ),
+  );
+  if (n > MAX_RELEASE_COUNTER) {
+    throw new Error(
+      `Vite ${vite} already has ${MAX_RELEASE_COUNTER + 1} core releases, the maximum`,
+    );
+  }
+  const base = encodeCoreVersion(vite, n);
+  if (!isVitePlusPrerelease) {
+    return base;
+  }
+  const k = nextCounter(
+    published.flatMap((version) => {
+      const match = version.startsWith(`${base}-${PRERELEASE_TAG}.`)
+        ? tryDecodeCoreVersion(version)
+        : undefined;
+      return match?.k === undefined ? [] : [match.k];
+    }),
+  );
+  return `${base}-${PRERELEASE_TAG}.${k}`;
+}
+
+/**
+ * Asserts that a core manifest encodes its bundled Vite version, and that the
+ * core version is a prerelease exactly when the `vite-plus` version is one.
+ * With `release`, also rejects the local build counter.
+ */
+export function assertCoreVersionMatchesVite(
+  pkg: { version: string; bundledVersions?: { vite?: string } },
+  vitePlus: string,
+  { release = false }: { release?: boolean } = {},
+): void {
+  const vite = pkg.bundledVersions?.vite;
+  if (!vite) {
+    throw new Error(`${CORE_PACKAGE_NAME} has no bundledVersions.vite`);
+  }
+  const decoded = decodeCoreVersion(pkg.version);
+  if (decoded.vite !== vite) {
+    throw new Error(
+      `${CORE_PACKAGE_NAME}@${pkg.version} encodes Vite ${decoded.vite}, but it bundles Vite ${vite}. ` +
+        `Set its version to ${isPrereleaseVersion(vite) ? `${vite}.${PRERELEASE_TAG}.0` : encodeCoreVersion(vite, 0)}.`,
+    );
+  }
+  const isCorePrerelease = decoded.k !== undefined;
+  if (isCorePrerelease !== isPrereleaseVersion(vitePlus)) {
+    throw new Error(
+      `${CORE_PACKAGE_NAME}@${pkg.version} and vite-plus@${vitePlus} must both be prereleases or both be stable`,
+    );
+  }
+  if (release && decoded.n === LOCAL_BUILD_COUNTER) {
+    throw new Error(
+      `${CORE_PACKAGE_NAME}@${pkg.version} uses the counter ${LOCAL_BUILD_COUNTER}, which is reserved for local builds`,
+    );
+  }
+}
+
+function readManifest(relativePath: string): {
+  version: string;
+  bundledVersions?: { vite?: string };
+} {
+  return JSON.parse(readFileSync(new URL(relativePath, import.meta.url), 'utf8'));
+}
+
+function readPublishedCoreVersions(): string[] {
+  const output = execFileSync('npm', ['view', CORE_PACKAGE_NAME, 'versions', '--json'], {
+    encoding: 'utf8',
+  });
+  const versions: unknown = JSON.parse(output);
+  if (typeof versions === 'string') {
+    return [versions];
+  }
+  if (!Array.isArray(versions) || versions.some((version) => typeof version !== 'string')) {
+    throw new Error(`Unexpected output from npm view ${CORE_PACKAGE_NAME} versions`);
+  }
+  return versions;
+}
+
+function main(argv: string[]): void {
+  const { positionals, values } = parseArgs({
+    args: argv,
+    allowPositionals: true,
+    options: {
+      'vite-plus': { type: 'string' },
+      release: { type: 'boolean', default: false },
+    },
+  });
+  const core = readManifest('../../core/package.json');
+  const vite = core.bundledVersions?.vite;
+  if (!vite) {
+    throw new Error(`packages/core/package.json has no bundledVersions.vite`);
+  }
+
+  switch (positionals[0]) {
+    case 'next': {
+      const vitePlus = values['vite-plus'];
+      if (!vitePlus) {
+        throw new Error('next requires --vite-plus ');
+      }
+      console.log(nextCoreVersion({ vite, vitePlus, published: readPublishedCoreVersions() }));
+      break;
+    }
+    case 'local':
+      console.log(encodeCoreVersion(vite, LOCAL_BUILD_COUNTER));
+      break;
+    case 'check': {
+      const cli = readManifest('../../cli/package.json');
+      assertCoreVersionMatchesVite(core, cli.version, { release: values.release });
+      console.log(`${CORE_PACKAGE_NAME}@${core.version} encodes Vite ${vite}`);
+      break;
+    }
+    default:
+      throw new Error(`Unknown command: ${positionals[0] ?? ''}. Use next, local, or check.`);
+  }
+}
+
+if (import.meta.main) {
+  try {
+    main(process.argv.slice(2));
+  } catch (error) {
+    console.error(`[core-version] ${(error as Error).message}`);
+    process.exit(1);
+  }
+}

From 03b08de46d5cf80589429cef590f3908e949ca05 Mon Sep 17 00:00:00 2001
From: "MK (fengmk2)" 
Date: Fri, 30 Oct 2026 15:21:49 +0900
Subject: [PATCH 03/10] fix(cli): keep the stale core check for the workspace
 spec

The repository CLI declares `vite: workspace:@voidzero-dev/vite-plus-core@*`,
which has no exact version. The core check then compared only the package
name, so a project core with a stale version passed and `vp dev` started.
Fall back to the version of the core that the CLI resolves.

Co-Authored-By: Claude Opus 5.5 
---
 ...dule_identity_npm_tagged_alias_rejected.md | 15 ---------
 .../cli/src/__tests__/resolve-core.spec.ts    | 33 ++++++++++++++-----
 packages/cli/src/resolve-core.ts              | 27 +++++++++++----
 3 files changed, 45 insertions(+), 30 deletions(-)
 delete mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots/core_module_identity_npm_tagged_alias_rejected.md

diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots/core_module_identity_npm_tagged_alias_rejected.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots/core_module_identity_npm_tagged_alias_rejected.md
deleted file mode 100644
index 8f3b5c664c..0000000000
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots/core_module_identity_npm_tagged_alias_rejected.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# core_module_identity_npm_tagged_alias_rejected
-
-## `vpt write-file package.json '{"name":"core-module-identity","private":true,"type":"module","packageManager":"npm@11.11.0","devDependencies":{"vite-plus":"latest","vite":"npm:@voidzero-dev/vite-plus-core@latest"}}
-'`
-
-
-## `vp install --ignore-scripts`
-
-
-## `node check-tagged-alias.mjs`
-
-```
-vp dev rejects the replaced npm alias and reports the repair command
-vp pack rejects the replaced npm alias and reports the repair command
-```
diff --git a/packages/cli/src/__tests__/resolve-core.spec.ts b/packages/cli/src/__tests__/resolve-core.spec.ts
index 3570e15dfb..86cb6dfc2a 100644
--- a/packages/cli/src/__tests__/resolve-core.spec.ts
+++ b/packages/cli/src/__tests__/resolve-core.spec.ts
@@ -128,15 +128,30 @@ describe('resolveCore', () => {
     );
   });
 
-  it('checks only the package name for the repository workspace spec', () => {
-    writeCli(join(root, 'cli'), cliPkg.version, 'workspace:@voidzero-dev/vite-plus-core@*');
-    writeCore(bundled, '@voidzero-dev/vite-plus-core', '0.0.0-any');
-    writeCore(projectCore, '@voidzero-dev/vite-plus-core', '0.0.0-any');
-    expect(resolveCore('', project, cliModule)).toBe(join(bundled, 'index.js'));
-    writeCore(bundled, 'vite', '8.2.2');
-    expect(() => resolveCore('', project, cliModule)).toThrow(
-      'Expected @voidzero-dev/vite-plus-core, but found vite@8.2.2',
-    );
+  describe('with the repository workspace spec', () => {
+    beforeEach(() => {
+      writeCli(join(root, 'cli'), cliPkg.version, 'workspace:@voidzero-dev/vite-plus-core@*');
+      writeCore(bundled, '@voidzero-dev/vite-plus-core', '0.0.0-any');
+    });
+
+    it('compares the project core with the core that the CLI resolves', () => {
+      writeCore(projectCore, '@voidzero-dev/vite-plus-core', '0.0.0-any');
+      expect(resolveCore('', project, cliModule)).toBe(join(bundled, 'index.js'));
+    });
+
+    it('rejects a stale project core', () => {
+      writeCore(projectCore, '@voidzero-dev/vite-plus-core', '0.0.0-stale');
+      expect(() => resolveCore('', project, cliModule)).toThrow(
+        'Expected @voidzero-dev/vite-plus-core@0.0.0-any, but found @voidzero-dev/vite-plus-core@0.0.0-stale',
+      );
+    });
+
+    it('rejects upstream Vite in the CLI dependency', () => {
+      writeCore(bundled, 'vite', '8.2.2');
+      expect(() => resolveCore('', project, cliModule)).toThrow(
+        'Expected @voidzero-dev/vite-plus-core, but found vite@8.2.2',
+      );
+    });
   });
 
   it('does not fall back to the project when the CLI dependency is missing', () => {
diff --git a/packages/cli/src/resolve-core.ts b/packages/cli/src/resolve-core.ts
index 3941c32a41..61b511851f 100644
--- a/packages/cli/src/resolve-core.ts
+++ b/packages/cli/src/resolve-core.ts
@@ -29,14 +29,26 @@ function checkCoreVersion(packageJsonPath: string, expectedVersion: string | und
  * Read the core version that the selected CLI pins through its `vite` alias.
  * The core version encodes the bundled Vite version, so it differs from the
  * CLI version (rfcs/core-version-alignment.md). Packing turns the repository's
- * `workspace:` spec into the exact alias. Any other spec pins no version.
+ * `workspace:` spec into the exact alias. A spec without an exact version
+ * falls back to the version of the core that the CLI resolves, so a stale
+ * project core still fails in the repository layout.
  */
-function readExpectedCoreVersion(cliPackageJsonPath: string): string | undefined {
+function readExpectedCoreVersion(
+  cliPackageJsonPath: string,
+  corePackageJsonPath: string,
+): string | undefined {
   const { dependencies } = JSON.parse(readFileSync(cliPackageJsonPath, 'utf8')) as {
     dependencies?: Record;
   };
   const spec = dependencies?.vite;
-  return spec?.startsWith(CORE_ALIAS_PREFIX) ? spec.slice(CORE_ALIAS_PREFIX.length) : undefined;
+  if (spec?.startsWith(CORE_ALIAS_PREFIX)) {
+    return spec.slice(CORE_ALIAS_PREFIX.length);
+  }
+  const core = JSON.parse(readFileSync(corePackageJsonPath, 'utf8')) as {
+    name?: string;
+    version?: string;
+  };
+  return core.name === CORE_PACKAGE_NAME ? core.version : undefined;
 }
 
 function checkProjectCoreVersion(
@@ -78,15 +90,18 @@ export function resolveCore(
   modulePath = import.meta.url,
 ): string {
   const cliRequire = createRequire(modulePath);
-  // Read the selected CLI's installed manifest. A static JSON import is inlined
-  // during the build, before CI and preview packers can stamp a new version.
-  const expectedVersion = readExpectedCoreVersion(cliRequire.resolve('vite-plus/package.json'));
   let corePackageJsonPath: string;
   try {
     corePackageJsonPath = cliRequire.resolve('vite/package.json');
   } catch (cause) {
     throw new Error('Could not resolve the bundled Vite dependency. Run `vp install`.', { cause });
   }
+  // Read the selected CLI's installed manifest. A static JSON import is inlined
+  // during the build, before CI and preview packers can stamp a new version.
+  const expectedVersion = readExpectedCoreVersion(
+    cliRequire.resolve('vite-plus/package.json'),
+    corePackageJsonPath,
+  );
   checkCoreVersion(corePackageJsonPath, expectedVersion);
   checkProjectCoreVersion(cwd, corePackageJsonPath, expectedVersion);
 

From e436d7f08ea786f0535dec21c9a37888cb614a5c Mon Sep 17 00:00:00 2001
From: "MK (fengmk2)" 
Date: Fri, 30 Oct 2026 15:21:58 +0900
Subject: [PATCH 04/10] test(snapshots): re-record for the encoded core version

- Remove the pnpm `vite` peer rule from migration snapshots, and record the
  summary line for projects whose rule migration removes.
- npm now keeps a tagged core alias and resolves Vitest's `vite` peer to the
  CLI's core, because the core version satisfies that range. Replace the
  tagged-alias rejection case with a case that checks the shared core, and
  update the CLI-only layout check.
- Read the core version from the core manifest or the CLI alias in the core
  identity fixtures, not from the vite-plus version.

Co-Authored-By: Claude Opus 5.5 
---
 ...mand_update_catalog_protocol_legacy_key.md | 10 -------
 .../command_update_catalog_protocol_pnpm.md   |  5 ----
 .../command_update_catalog_protocol_pnpm12.md |  5 ----
 .../core_module_identity/check-npm-layout.mjs | 13 ++++-----
 .../check-tagged-alias.mjs                    | 25 ++++++-----------
 .../core_module_identity/snapshots.toml       |  4 ++-
 ...odule_identity_npm_cli_only_no_override.md |  2 +-
 .../core_module_identity_npm_tagged_alias.md  | 28 +++++++++++++++++++
 .../create_approve_builds_migrate_pnpm11.md   | 15 ----------
 .../snapshots/create_approve_builds_pnpm11.md | 15 ----------
 .../snapshots/create_org_bundled_monorepo.md  |  5 ----
 .../snapshots/migration_add_git_hooks.md      |  5 ----
 .../migration_auto_create_vite_config.md      |  5 ----
 .../snapshots/migration_baseurl_tsconfig.md   |  5 ----
 ...igration_chained_lint_staged_pre_commit.md |  5 ----
 .../migration_composed_husky_custom_dir.md    |  5 ----
 .../migration_composed_husky_prepare.md       |  5 ----
 .../migration_env_prefix_lint_staged.md       |  5 ----
 .../snapshots/migration_eslint.md             |  5 ----
 .../snapshots/migration_eslint_lint_staged.md |  5 ----
 .../migration_eslint_lintstagedrc.md          |  5 ----
 .../snapshots/migration_eslint_npx_wrapper.md |  5 ----
 .../snapshots/migration_existing_husky.md     |  5 ----
 .../migration_existing_husky_lint_staged.md   |  5 ----
 .../migration_existing_husky_v8_hooks.md      |  5 ----
 ...migration_existing_husky_v8_multi_hooks.md |  5 ----
 .../migration_existing_lint_staged_config.md  |  5 ----
 ...igration_existing_pnpm_exec_lint_staged.md |  5 ----
 .../migration_existing_prepare_script.md      |  5 ----
 .../snapshots/migration_from_tsdown.md        |  5 ----
 .../migration_from_tsdown_json_config.md      |  5 ----
 .../snapshots/migration_from_vitest_config.md |  2 --
 .../snapshots/migration_from_vitest_files.md  |  2 --
 ...ration_hooks_skip_on_existing_hookspath.md |  5 ----
 .../migration_husky_catalog_version.md        |  5 ----
 .../migration_husky_latest_dist_tag.md        |  5 ----
 ...tion_husky_latest_dist_tag_v9_installed.md |  5 ----
 .../snapshots/migration_husky_or_prepare.md   |  5 ----
 .../migration_husky_semicolon_prepare.md      |  5 ----
 ...igration_husky_v8_preserves_lint_staged.md |  5 ----
 .../snapshots/migration_lazy_plugins_await.md |  5 ----
 .../migration_lint_staged_in_scripts.md       |  5 ----
 .../migration_lint_staged_merge_fail.md       |  5 ----
 .../migration_lint_staged_ts_config.md        |  5 ----
 .../snapshots/migration_lintstagedrc_json.md  |  5 ----
 .../migration_lintstagedrc_merge_fail.md      |  5 ----
 .../migration_lintstagedrc_not_support.md     |  5 ----
 .../migration_lintstagedrc_staged_exists.md   |  5 ----
 .../migration_merge_vite_config_js.md         |  5 ----
 .../migration_merge_vite_config_ts.md         |  2 --
 .../snapshots/migration_monorepo_pnpm.md      |  2 --
 ...repo_pnpm_overrides_dependency_selector.md |  5 ----
 .../snapshots/migration_no_git_repo.md        |  5 ----
 .../snapshots/migration_no_hooks.md           |  5 ----
 .../migration_no_hooks_with_husky.md          |  5 ----
 .../snapshots/migration_npmx_dev.md           |  3 +-
 .../snapshots/migration_other_hook_tool.md    |  5 ----
 .../migration_oxlintrc_json_with_comments.md  |  5 ----
 .../snapshots/migration_oxlintrc_jsonc.md     |  5 ----
 ...migration_partially_installed_vite_plus.md |  5 ----
 ...migration_partially_migrated_pre_commit.md |  5 ----
 .../snapshots/migration_playwright_test.md    |  3 +-
 ...gration_preserve_below_policy_node_pins.md |  5 ----
 .../snapshots/migration_prettier.md           |  5 ----
 .../migration_prettier_eslint_combo.md        |  5 ----
 .../migration_prettier_ignore_unknown.md      |  5 ----
 .../migration_prettier_lint_staged.md         |  5 ----
 .../snapshots/migration_prettier_pkg_json.md  |  5 ----
 .../migration_rewrite_declare_module.md       |  2 --
 .../migration_skip_vite_dependency.md         |  5 ----
 .../migration_skip_vite_peer_dependency.md    |  5 ----
 .../migration_stale_vitest_alias_startup.md   |  5 ----
 .../snapshots/migration_standalone_pnpm.md    |  5 ----
 .../snapshots/migration_subpath.md            |  5 ----
 .../migration_tsconfig_esmoduleinterop.md     |  5 ----
 ...igration_upgrade_browser_peer_only_pnpm.md |  3 +-
 ...ration_upgrade_browser_source_only_pnpm.md |  3 +-
 .../migrate_vitest4_community_provider.md     |  1 +
 ...ration_upgrade_browser_webdriverio_pnpm.md |  3 +-
 .../preserve_community_provider_version.md    |  1 +
 .../upgrade_community_provider_catalog.md     |  2 --
 .../migration_upgrade_hooks_flag_pnpm.md      |  1 +
 ..._upgrade_monorepo_vitest_localized_pnpm.md |  3 +-
 ...ration_upgrade_nuxt_test_utils_monorepo.md |  2 --
 ...ration_upgrade_peer_vitest_catalog_pnpm.md |  6 +---
 ...ration_upgrade_pnpm_bundled_catalog_dep.md |  5 ----
 ...migration_upgrade_pnpm_catalogs_default.md | 10 -------
 .../migration_upgrade_setup_full_pnpm.md      |  1 +
 ...tion_upgrade_setup_skipped_default_pnpm.md |  6 +---
 .../migration_upgrade_stale_local_pnpm.md     |  6 +---
 ...pgrade_vitest_reference_whitespace_pnpm.md |  6 +---
 ...igration_vite_plus_in_dependencies_pnpm.md |  5 ----
 .../snapshots/migration_vite_version.md       |  5 ----
 .../snapshots/migration_vitest_import_only.md |  5 ----
 .../snapshots/migration_vitest_peer_dep.md    |  2 --
 .../snapshots/migration_workspace_members.md  |  5 ----
 .../snapshots/new_vite_monorepo.md            |  5 ----
 packages/cli/BUNDLING.md                      | 14 ++++++----
 98 files changed, 69 insertions(+), 459 deletions(-)
 create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots/core_module_identity_npm_tagged_alias.md

diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_update_catalog_protocol_legacy_key/snapshots/command_update_catalog_protocol_legacy_key.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_update_catalog_protocol_legacy_key/snapshots/command_update_catalog_protocol_legacy_key.md
index ad34531d72..27da3c19a3 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_update_catalog_protocol_legacy_key/snapshots/command_update_catalog_protocol_legacy_key.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_update_catalog_protocol_legacy_key/snapshots/command_update_catalog_protocol_legacy_key.md
@@ -32,11 +32,6 @@ catalog:
   vite-plus: 
 overrides:
   vite: "catalog:"
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: "*"
 ```
 
 ## `vp up`
@@ -85,11 +80,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: "catalog:"
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: "*"
 ```
 
 ## `vp up`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_update_catalog_protocol_pnpm/snapshots/command_update_catalog_protocol_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_update_catalog_protocol_pnpm/snapshots/command_update_catalog_protocol_pnpm.md
index 7de98a9cee..72b25fe774 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_update_catalog_protocol_pnpm/snapshots/command_update_catalog_protocol_pnpm.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_update_catalog_protocol_pnpm/snapshots/command_update_catalog_protocol_pnpm.md
@@ -60,9 +60,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: "catalog:"
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: "*"
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_update_catalog_protocol_pnpm12/snapshots/command_update_catalog_protocol_pnpm12.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_update_catalog_protocol_pnpm12/snapshots/command_update_catalog_protocol_pnpm12.md
index 96f01d9165..9b738a2952 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_update_catalog_protocol_pnpm12/snapshots/command_update_catalog_protocol_pnpm12.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_update_catalog_protocol_pnpm12/snapshots/command_update_catalog_protocol_pnpm12.md
@@ -27,11 +27,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: "catalog:"
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: "*"
 ```
 
 ## `vp up`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-npm-layout.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-npm-layout.mjs
index c94fb893f9..f984cfd3d4 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-npm-layout.mjs
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-npm-layout.mjs
@@ -23,11 +23,10 @@ if (project.devDependencies.vite) {
   );
 } else {
   assert.deepEqual(Object.keys(project.devDependencies), ['vite-plus']);
-  assert.equal(require('vite/package.json').name, 'vite');
-  assert.equal(vitestRequire('vite/package.json').name, 'vite');
-  assert.notEqual(
-    cliRequire.resolve('vite/package.json'),
-    vitestRequire.resolve('vite/package.json'),
-  );
-  console.log('npm installed separate CLI core and upstream Vitest peer without overrides');
+  // The core version encodes the bundled Vite version, so it satisfies Vitest's
+  // `vite` peer range. npm resolves that peer to the CLI's core instead of
+  // installing upstream Vite (rfcs/core-version-alignment.md).
+  assert.equal(require('vite/package.json').name, '@voidzero-dev/vite-plus-core');
+  assert.equal(vitestRequire.resolve('vite/package.json'), cliRequire.resolve('vite/package.json'));
+  console.log('npm shares the CLI core with the Vitest peer without a project alias or overrides');
 }
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-tagged-alias.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-tagged-alias.mjs
index 40e971754c..5b22621d02 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-tagged-alias.mjs
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/check-tagged-alias.mjs
@@ -1,24 +1,17 @@
 import assert from 'node:assert/strict';
-import { spawnSync } from 'node:child_process';
 import { createRequire } from 'node:module';
 
 const require = createRequire(import.meta.url);
 const cliRequire = createRequire(require.resolve('vite-plus/package.json'));
+const vitestRequire = createRequire(cliRequire.resolve('vitest/package.json'));
 const project = require('./package.json');
 assert.equal(project.overrides, undefined);
 assert.equal(project.devDependencies.vite, 'npm:@voidzero-dev/vite-plus-core@latest');
-// npm 11.11 accepts a peer's upstream Vite as satisfying the tagged alias.
-assert.equal(require('vite/package.json').name, 'vite');
-assert.equal(cliRequire('vite/package.json').name, '@voidzero-dev/vite-plus-core');
-assert.notEqual(require.resolve('vite/package.json'), cliRequire.resolve('vite/package.json'));
-
-for (const args of [['dev'], ['pack', 'entry.js']]) {
-  const result = spawnSync('vp', args, { encoding: 'utf8', timeout: 30_000 });
-  assert.ifError(result.error);
-  const output = result.stdout + result.stderr;
-  assert.equal(result.status, 1, output);
-  assert.match(output, /Expected @voidzero-dev\/vite-plus-core@/);
-  assert.match(output, /but found vite@/);
-  assert.match(output, /Run `vp migrate` to align the Vite alias, then run `vp install`\./);
-  console.log(`vp ${args[0]} rejects the replaced npm alias and reports the repair command`);
-}
+// The core version encodes the bundled Vite version, so it satisfies Vitest's
+// `vite` peer range and npm keeps the tagged alias on the core
+// (rfcs/core-version-alignment.md). A `1.x` core made npm replace the alias
+// with upstream Vite.
+assert.equal(require('vite/package.json').name, '@voidzero-dev/vite-plus-core');
+assert.equal(require.resolve('vite/package.json'), cliRequire.resolve('vite/package.json'));
+assert.equal(vitestRequire.resolve('vite/package.json'), cliRequire.resolve('vite/package.json'));
+console.log('npm keeps the tagged core alias and shares it between the project, CLI, and Vitest');
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots.toml
index d0be4f3f72..9de89704ba 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots.toml
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots.toml
@@ -156,11 +156,13 @@ steps = [
 ]
 
 [[case]]
-name = "core_module_identity_npm_tagged_alias_rejected"
+name = "core_module_identity_npm_tagged_alias"
 vp = "global"
 local-registry = true
+comment = "The core version satisfies Vitest's vite peer range, so npm keeps a tagged core alias instead of replacing it with upstream Vite."
 steps = [
   { argv = ["vpt", "write-file", "package.json", "{\"name\":\"core-module-identity\",\"private\":true,\"type\":\"module\",\"packageManager\":\"npm@11.11.0\",\"devDependencies\":{\"vite-plus\":\"latest\",\"vite\":\"npm:@voidzero-dev/vite-plus-core@latest\"}}\n"], snapshot = false },
   { argv = ["vp", "install", "--ignore-scripts"], timeout = 120000, snapshot = false },
   ["node", "check-tagged-alias.mjs"],
+  ["vp", "pack", "entry.js"],
 ]
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots/core_module_identity_npm_cli_only_no_override.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots/core_module_identity_npm_cli_only_no_override.md
index 0571631515..3c9c60ff20 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots/core_module_identity_npm_cli_only_no_override.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots/core_module_identity_npm_cli_only_no_override.md
@@ -13,7 +13,7 @@
 ## `node check-npm-layout.mjs`
 
 ```
-npm installed separate CLI core and upstream Vitest peer without overrides
+npm shares the CLI core with the Vitest peer without a project alias or overrides
 ```
 
 ## `node check-bundled.mjs`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots/core_module_identity_npm_tagged_alias.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots/core_module_identity_npm_tagged_alias.md
new file mode 100644
index 0000000000..d47cbe8631
--- /dev/null
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_module_identity/snapshots/core_module_identity_npm_tagged_alias.md
@@ -0,0 +1,28 @@
+# core_module_identity_npm_tagged_alias
+
+The core version satisfies Vitest's vite peer range, so npm keeps a tagged core alias instead of replacing it with upstream Vite.
+
+## `vpt write-file package.json '{"name":"core-module-identity","private":true,"type":"module","packageManager":"npm@11.11.0","devDependencies":{"vite-plus":"latest","vite":"npm:@voidzero-dev/vite-plus-core@latest"}}
+'`
+
+
+## `vp install --ignore-scripts`
+
+
+## `node check-tagged-alias.mjs`
+
+```
+npm keeps the tagged core alias and shares it between the project, CLI, and Vitest
+```
+
+## `vp pack entry.js`
+
+```
+VITE+ - The Unified Toolchain for the Web
+
+ℹ entry: entry.js
+ℹ Build start
+ℹ dist/entry.mjs   kB │ gzip:  kB
+ℹ 1 files, total:  kB
+✔ Build complete in 
+```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_approve_builds_migrate_pnpm11/snapshots/create_approve_builds_migrate_pnpm11.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_approve_builds_migrate_pnpm11/snapshots/create_approve_builds_migrate_pnpm11.md
index 7121581f7a..4741c59d57 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_approve_builds_migrate_pnpm11/snapshots/create_approve_builds_migrate_pnpm11.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_approve_builds_migrate_pnpm11/snapshots/create_approve_builds_migrate_pnpm11.md
@@ -24,11 +24,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: "catalog:"
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: "*"
 ```
 
 ## `vp create @your-org:with-build-dep --no-interactive --directory default-app`
@@ -57,11 +52,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: "catalog:"
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: "*"
 ```
 
 ## `cd default-app && vp pm approve-builds core-js`
@@ -84,9 +74,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: "catalog:"
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: "*"
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_approve_builds_pnpm11/snapshots/create_approve_builds_pnpm11.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_approve_builds_pnpm11/snapshots/create_approve_builds_pnpm11.md
index 6c8ca152cb..ab6923f9a7 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_approve_builds_pnpm11/snapshots/create_approve_builds_pnpm11.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_approve_builds_pnpm11/snapshots/create_approve_builds_pnpm11.md
@@ -23,11 +23,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: "catalog:"
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: "*"
 ```
 
 ## `vp create @your-org:with-build-dep --no-interactive --directory default-app`
@@ -55,11 +50,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: "catalog:"
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: "*"
 ```
 
 ## `cd default-app && vp pm approve-builds core-js`
@@ -82,9 +72,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: "catalog:"
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: "*"
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/snapshots/create_org_bundled_monorepo.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/snapshots/create_org_bundled_monorepo.md
index 5c05d098d5..ff75554728 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/snapshots/create_org_bundled_monorepo.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/create_org_bundled_monorepo/snapshots/create_org_bundled_monorepo.md
@@ -46,11 +46,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: "catalog:"
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: "*"
 ```
 
 ## `vpt stat-file my-mono/.git --assert dir`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_add_git_hooks/snapshots/migration_add_git_hooks.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_add_git_hooks/snapshots/migration_add_git_hooks.md
index 54af3c6544..70d08e0758 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_add_git_hooks/snapshots/migration_add_git_hooks.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_add_git_hooks/snapshots/migration_add_git_hooks.md
@@ -43,11 +43,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file .vite-hooks/pre-commit`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_auto_create_vite_config/snapshots/migration_auto_create_vite_config.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_auto_create_vite_config/snapshots/migration_auto_create_vite_config.md
index d206896401..cba60fec73 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_auto_create_vite_config/snapshots/migration_auto_create_vite_config.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_auto_create_vite_config/snapshots/migration_auto_create_vite_config.md
@@ -90,9 +90,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_baseurl_tsconfig/snapshots/migration_baseurl_tsconfig.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_baseurl_tsconfig/snapshots/migration_baseurl_tsconfig.md
index 786b036a0d..30052873dc 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_baseurl_tsconfig/snapshots/migration_baseurl_tsconfig.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_baseurl_tsconfig/snapshots/migration_baseurl_tsconfig.md
@@ -97,9 +97,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_chained_lint_staged_pre_commit/snapshots/migration_chained_lint_staged_pre_commit.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_chained_lint_staged_pre_commit/snapshots/migration_chained_lint_staged_pre_commit.md
index 41101ccc53..01c8723581 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_chained_lint_staged_pre_commit/snapshots/migration_chained_lint_staged_pre_commit.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_chained_lint_staged_pre_commit/snapshots/migration_chained_lint_staged_pre_commit.md
@@ -48,11 +48,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file vite.config.ts`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_composed_husky_custom_dir/snapshots/migration_composed_husky_custom_dir.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_composed_husky_custom_dir/snapshots/migration_composed_husky_custom_dir.md
index 8f9a9de90b..1480398795 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_composed_husky_custom_dir/snapshots/migration_composed_husky_custom_dir.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_composed_husky_custom_dir/snapshots/migration_composed_husky_custom_dir.md
@@ -57,11 +57,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file .config/husky/pre-commit`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_composed_husky_prepare/snapshots/migration_composed_husky_prepare.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_composed_husky_prepare/snapshots/migration_composed_husky_prepare.md
index f3126ccd6e..aa852bfc34 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_composed_husky_prepare/snapshots/migration_composed_husky_prepare.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_composed_husky_prepare/snapshots/migration_composed_husky_prepare.md
@@ -44,9 +44,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_env_prefix_lint_staged/snapshots/migration_env_prefix_lint_staged.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_env_prefix_lint_staged/snapshots/migration_env_prefix_lint_staged.md
index 8448cf35d6..a352df32bd 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_env_prefix_lint_staged/snapshots/migration_env_prefix_lint_staged.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_env_prefix_lint_staged/snapshots/migration_env_prefix_lint_staged.md
@@ -48,11 +48,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file vite.config.ts`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint/snapshots/migration_eslint.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint/snapshots/migration_eslint.md
index 6fd722d9e2..4124dc07f3 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint/snapshots/migration_eslint.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint/snapshots/migration_eslint.md
@@ -44,11 +44,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt stat-file eslint.config.mjs --assert-not file`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_lint_staged/snapshots/migration_eslint_lint_staged.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_lint_staged/snapshots/migration_eslint_lint_staged.md
index 6e787aae18..ded31805d7 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_lint_staged/snapshots/migration_eslint_lint_staged.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_lint_staged/snapshots/migration_eslint_lint_staged.md
@@ -41,11 +41,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file vite.config.ts`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_lintstagedrc/snapshots/migration_eslint_lintstagedrc.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_lintstagedrc/snapshots/migration_eslint_lintstagedrc.md
index 6bb47ada0e..38b2c8732d 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_lintstagedrc/snapshots/migration_eslint_lintstagedrc.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_lintstagedrc/snapshots/migration_eslint_lintstagedrc.md
@@ -41,11 +41,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt stat-file .lintstagedrc.json --assert-not file`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_npx_wrapper/snapshots/migration_eslint_npx_wrapper.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_npx_wrapper/snapshots/migration_eslint_npx_wrapper.md
index cb125d95b5..c5600fa1cc 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_npx_wrapper/snapshots/migration_eslint_npx_wrapper.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_eslint_npx_wrapper/snapshots/migration_eslint_npx_wrapper.md
@@ -46,11 +46,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt stat-file eslint.config.mjs --assert-not file`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky/snapshots/migration_existing_husky.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky/snapshots/migration_existing_husky.md
index 38647dd192..f07e9d6bf3 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky/snapshots/migration_existing_husky.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky/snapshots/migration_existing_husky.md
@@ -44,11 +44,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file .husky/pre-commit`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky_lint_staged/snapshots/migration_existing_husky_lint_staged.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky_lint_staged/snapshots/migration_existing_husky_lint_staged.md
index fc0951692c..e560ba94cd 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky_lint_staged/snapshots/migration_existing_husky_lint_staged.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky_lint_staged/snapshots/migration_existing_husky_lint_staged.md
@@ -48,11 +48,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file vite.config.ts`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky_v8_hooks/snapshots/migration_existing_husky_v8_hooks.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky_v8_hooks/snapshots/migration_existing_husky_v8_hooks.md
index 45966828d4..464cce89a5 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky_v8_hooks/snapshots/migration_existing_husky_v8_hooks.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky_v8_hooks/snapshots/migration_existing_husky_v8_hooks.md
@@ -45,11 +45,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file .husky/pre-commit`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky_v8_multi_hooks/snapshots/migration_existing_husky_v8_multi_hooks.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky_v8_multi_hooks/snapshots/migration_existing_husky_v8_multi_hooks.md
index a6511ad5c9..29ffba5760 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky_v8_multi_hooks/snapshots/migration_existing_husky_v8_multi_hooks.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_husky_v8_multi_hooks/snapshots/migration_existing_husky_v8_multi_hooks.md
@@ -45,11 +45,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file .husky/pre-commit`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_lint_staged_config/snapshots/migration_existing_lint_staged_config.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_lint_staged_config/snapshots/migration_existing_lint_staged_config.md
index 2729823751..4dc9ce6272 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_lint_staged_config/snapshots/migration_existing_lint_staged_config.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_lint_staged_config/snapshots/migration_existing_lint_staged_config.md
@@ -43,11 +43,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt stat-file .lintstagedrc.json --assert-not file`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_pnpm_exec_lint_staged/snapshots/migration_existing_pnpm_exec_lint_staged.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_pnpm_exec_lint_staged/snapshots/migration_existing_pnpm_exec_lint_staged.md
index a40a4a8bb7..601711ff4d 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_pnpm_exec_lint_staged/snapshots/migration_existing_pnpm_exec_lint_staged.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_pnpm_exec_lint_staged/snapshots/migration_existing_pnpm_exec_lint_staged.md
@@ -48,11 +48,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file vite.config.ts`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_prepare_script/snapshots/migration_existing_prepare_script.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_prepare_script/snapshots/migration_existing_prepare_script.md
index 4de69944ad..eb973a231e 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_prepare_script/snapshots/migration_existing_prepare_script.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_existing_prepare_script/snapshots/migration_existing_prepare_script.md
@@ -44,11 +44,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file .vite-hooks/pre-commit`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/snapshots/migration_from_tsdown.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/snapshots/migration_from_tsdown.md
index e715a94cef..7c3acb7133 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/snapshots/migration_from_tsdown.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown/snapshots/migration_from_tsdown.md
@@ -85,11 +85,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vp migrate --no-interactive`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown_json_config/snapshots/migration_from_tsdown_json_config.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown_json_config/snapshots/migration_from_tsdown_json_config.md
index 7e226727f1..919b5e2bff 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown_json_config/snapshots/migration_from_tsdown_json_config.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_tsdown_json_config/snapshots/migration_from_tsdown_json_config.md
@@ -86,11 +86,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vp migrate --no-interactive`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_vitest_config/snapshots/migration_from_vitest_config.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_vitest_config/snapshots/migration_from_vitest_config.md
index bcd8c684ae..54cb5e435b 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_vitest_config/snapshots/migration_from_vitest_config.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_vitest_config/snapshots/migration_from_vitest_config.md
@@ -108,9 +108,7 @@ overrides:
   '@vitest/browser@*': 
 peerDependencyRules:
   allowAny:
-    - vite
     - vitest
   allowedVersions:
-    vite: '*'
     vitest: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_vitest_files/snapshots/migration_from_vitest_files.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_vitest_files/snapshots/migration_from_vitest_files.md
index 41978da470..f3a6324d81 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_vitest_files/snapshots/migration_from_vitest_files.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_from_vitest_files/snapshots/migration_from_vitest_files.md
@@ -52,10 +52,8 @@ overrides:
   vitest@*: 'catalog:'
 peerDependencyRules:
   allowAny:
-    - vite
     - vitest
   allowedVersions:
-    vite: '*'
     vitest: '*'
 ```
 
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_hooks_skip_on_existing_hookspath/snapshots/migration_hooks_skip_on_existing_hookspath.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_hooks_skip_on_existing_hookspath/snapshots/migration_hooks_skip_on_existing_hookspath.md
index ef9b150f19..b18f3b41cd 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_hooks_skip_on_existing_hookspath/snapshots/migration_hooks_skip_on_existing_hookspath.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_hooks_skip_on_existing_hookspath/snapshots/migration_hooks_skip_on_existing_hookspath.md
@@ -43,11 +43,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `git config --local core.hooksPath`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_catalog_version/snapshots/migration_husky_catalog_version.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_catalog_version/snapshots/migration_husky_catalog_version.md
index 2d866471f8..13be0b56a2 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_catalog_version/snapshots/migration_husky_catalog_version.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_catalog_version/snapshots/migration_husky_catalog_version.md
@@ -54,11 +54,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file vite.config.ts`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_latest_dist_tag/snapshots/migration_husky_latest_dist_tag.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_latest_dist_tag/snapshots/migration_husky_latest_dist_tag.md
index 70af6b9a4b..e86d5eb535 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_latest_dist_tag/snapshots/migration_husky_latest_dist_tag.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_latest_dist_tag/snapshots/migration_husky_latest_dist_tag.md
@@ -44,9 +44,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_latest_dist_tag_v9_installed/snapshots/migration_husky_latest_dist_tag_v9_installed.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_latest_dist_tag_v9_installed/snapshots/migration_husky_latest_dist_tag_v9_installed.md
index 2e5b05206c..1e8ec36dbe 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_latest_dist_tag_v9_installed/snapshots/migration_husky_latest_dist_tag_v9_installed.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_latest_dist_tag_v9_installed/snapshots/migration_husky_latest_dist_tag_v9_installed.md
@@ -48,9 +48,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_or_prepare/snapshots/migration_husky_or_prepare.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_or_prepare/snapshots/migration_husky_or_prepare.md
index 9b895203cb..a8b696566c 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_or_prepare/snapshots/migration_husky_or_prepare.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_or_prepare/snapshots/migration_husky_or_prepare.md
@@ -48,9 +48,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_semicolon_prepare/snapshots/migration_husky_semicolon_prepare.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_semicolon_prepare/snapshots/migration_husky_semicolon_prepare.md
index 1cca97e5b1..eac86d09ef 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_semicolon_prepare/snapshots/migration_husky_semicolon_prepare.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_semicolon_prepare/snapshots/migration_husky_semicolon_prepare.md
@@ -48,9 +48,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_v8_preserves_lint_staged/snapshots/migration_husky_v8_preserves_lint_staged.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_v8_preserves_lint_staged/snapshots/migration_husky_v8_preserves_lint_staged.md
index b775701d03..c0460fd0b8 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_v8_preserves_lint_staged/snapshots/migration_husky_v8_preserves_lint_staged.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_husky_v8_preserves_lint_staged/snapshots/migration_husky_v8_preserves_lint_staged.md
@@ -48,9 +48,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lazy_plugins_await/snapshots/migration_lazy_plugins_await.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lazy_plugins_await/snapshots/migration_lazy_plugins_await.md
index 0f2a1951b1..236b092660 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lazy_plugins_await/snapshots/migration_lazy_plugins_await.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lazy_plugins_await/snapshots/migration_lazy_plugins_await.md
@@ -62,9 +62,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lint_staged_in_scripts/snapshots/migration_lint_staged_in_scripts.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lint_staged_in_scripts/snapshots/migration_lint_staged_in_scripts.md
index 467fd26f76..4775b5160b 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lint_staged_in_scripts/snapshots/migration_lint_staged_in_scripts.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lint_staged_in_scripts/snapshots/migration_lint_staged_in_scripts.md
@@ -49,11 +49,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file vite.config.ts`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lint_staged_merge_fail/snapshots/migration_lint_staged_merge_fail.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lint_staged_merge_fail/snapshots/migration_lint_staged_merge_fail.md
index 23fb3a8a00..b83bdbda3e 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lint_staged_merge_fail/snapshots/migration_lint_staged_merge_fail.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lint_staged_merge_fail/snapshots/migration_lint_staged_merge_fail.md
@@ -50,11 +50,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file vite.config.ts`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lint_staged_ts_config/snapshots/migration_lint_staged_ts_config.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lint_staged_ts_config/snapshots/migration_lint_staged_ts_config.md
index e13263d45d..4b03c89791 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lint_staged_ts_config/snapshots/migration_lint_staged_ts_config.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lint_staged_ts_config/snapshots/migration_lint_staged_ts_config.md
@@ -46,11 +46,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file lint-staged.config.ts`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_json/snapshots/migration_lintstagedrc_json.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_json/snapshots/migration_lintstagedrc_json.md
index 4134ba0d43..b40631cf68 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_json/snapshots/migration_lintstagedrc_json.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_json/snapshots/migration_lintstagedrc_json.md
@@ -98,11 +98,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file vite.config.ts`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_merge_fail/snapshots/migration_lintstagedrc_merge_fail.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_merge_fail/snapshots/migration_lintstagedrc_merge_fail.md
index fe4904d1e5..686ce20a82 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_merge_fail/snapshots/migration_lintstagedrc_merge_fail.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_merge_fail/snapshots/migration_lintstagedrc_merge_fail.md
@@ -47,11 +47,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file .lintstagedrc.json`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_not_support/snapshots/migration_lintstagedrc_not_support.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_not_support/snapshots/migration_lintstagedrc_not_support.md
index beff4d8795..ea662e0ec4 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_not_support/snapshots/migration_lintstagedrc_not_support.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_not_support/snapshots/migration_lintstagedrc_not_support.md
@@ -74,9 +74,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_staged_exists/snapshots/migration_lintstagedrc_staged_exists.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_staged_exists/snapshots/migration_lintstagedrc_staged_exists.md
index e3662eebe4..b768812354 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_staged_exists/snapshots/migration_lintstagedrc_staged_exists.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_lintstagedrc_staged_exists/snapshots/migration_lintstagedrc_staged_exists.md
@@ -44,11 +44,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt stat-file .lintstagedrc.json --assert file`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_merge_vite_config_js/snapshots/migration_merge_vite_config_js.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_merge_vite_config_js/snapshots/migration_merge_vite_config_js.md
index 165f3445bf..029771df29 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_merge_vite_config_js/snapshots/migration_merge_vite_config_js.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_merge_vite_config_js/snapshots/migration_merge_vite_config_js.md
@@ -84,9 +84,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_merge_vite_config_ts/snapshots/migration_merge_vite_config_ts.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_merge_vite_config_ts/snapshots/migration_merge_vite_config_ts.md
index 64ac0df171..097d14b181 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_merge_vite_config_ts/snapshots/migration_merge_vite_config_ts.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_merge_vite_config_ts/snapshots/migration_merge_vite_config_ts.md
@@ -164,9 +164,7 @@ overrides:
   vitest@*: 'catalog:'
 peerDependencyRules:
   allowAny:
-    - vite
     - vitest
   allowedVersions:
-    vite: '*'
     vitest: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_pnpm/snapshots/migration_monorepo_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_pnpm/snapshots/migration_monorepo_pnpm.md
index 9b74a136c7..6294fb9da8 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_pnpm/snapshots/migration_monorepo_pnpm.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_pnpm/snapshots/migration_monorepo_pnpm.md
@@ -135,10 +135,8 @@ overrides:
   vitest@*: 'catalog:'
 peerDependencyRules:
   allowAny:
-    - vite
     - vitest
   allowedVersions:
-    vite: '*'
     vitest: '*'
 minimumReleaseAgeExclude:
   - vite-plus
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_pnpm_overrides_dependency_selector/snapshots/migration_monorepo_pnpm_overrides_dependency_selector.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_pnpm_overrides_dependency_selector/snapshots/migration_monorepo_pnpm_overrides_dependency_selector.md
index ab8dc31e0a..56cea89ad3 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_pnpm_overrides_dependency_selector/snapshots/migration_monorepo_pnpm_overrides_dependency_selector.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_monorepo_pnpm_overrides_dependency_selector/snapshots/migration_monorepo_pnpm_overrides_dependency_selector.md
@@ -68,11 +68,6 @@ overrides:
   supertest>superagent: 9.0.2
   react-click-away-listener>react: 0.0.0-experimental-7dc903cd-20251203
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file packages/app/package.json`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_no_git_repo/snapshots/migration_no_git_repo.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_no_git_repo/snapshots/migration_no_git_repo.md
index cb103d8e0f..53df88e954 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_no_git_repo/snapshots/migration_no_git_repo.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_no_git_repo/snapshots/migration_no_git_repo.md
@@ -39,11 +39,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt stat-file .vite-hooks --assert dir`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_no_hooks/snapshots/migration_no_hooks.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_no_hooks/snapshots/migration_no_hooks.md
index 830b4912ea..d7d4cb3ee7 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_no_hooks/snapshots/migration_no_hooks.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_no_hooks/snapshots/migration_no_hooks.md
@@ -39,11 +39,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt stat-file .vite-hooks --assert-not dir`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_no_hooks_with_husky/snapshots/migration_no_hooks_with_husky.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_no_hooks_with_husky/snapshots/migration_no_hooks_with_husky.md
index 4d0cd34d8b..1405b4955a 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_no_hooks_with_husky/snapshots/migration_no_hooks_with_husky.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_no_hooks_with_husky/snapshots/migration_no_hooks_with_husky.md
@@ -48,11 +48,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt stat-file .husky --assert-not dir`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_npmx_dev/snapshots/migration_npmx_dev.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_npmx_dev/snapshots/migration_npmx_dev.md
index 47b7ab55d9..5e1ad05ab6 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_npmx_dev/snapshots/migration_npmx_dev.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_npmx_dev/snapshots/migration_npmx_dev.md
@@ -16,6 +16,7 @@ VITE+ - The Unified Toolchain for the Web
     @vitest/browser-playwright  4.1.10 → 
     @vitest/coverage-v8         4.1.10 → 
 • Package manager settings configured
+• Removed `vite` from pnpm peerDependencyRules: the core version now matches the bundled Vite
 ```
 
 ## `vpt print-file package.json`
@@ -62,9 +63,7 @@ overrides:
   vitest@*: 'catalog:'
 peerDependencyRules:
   allowAny:
-    - vite
     - vitest
   allowedVersions:
-    vite: '*'
     vitest: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_other_hook_tool/snapshots/migration_other_hook_tool.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_other_hook_tool/snapshots/migration_other_hook_tool.md
index cade69a2f6..22c25bdfb1 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_other_hook_tool/snapshots/migration_other_hook_tool.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_other_hook_tool/snapshots/migration_other_hook_tool.md
@@ -48,9 +48,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlintrc_json_with_comments/snapshots/migration_oxlintrc_json_with_comments.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlintrc_json_with_comments/snapshots/migration_oxlintrc_json_with_comments.md
index 0bce7e8eea..bff0813631 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlintrc_json_with_comments/snapshots/migration_oxlintrc_json_with_comments.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlintrc_json_with_comments/snapshots/migration_oxlintrc_json_with_comments.md
@@ -81,9 +81,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlintrc_jsonc/snapshots/migration_oxlintrc_jsonc.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlintrc_jsonc/snapshots/migration_oxlintrc_jsonc.md
index 494f0ccc1d..424171ff34 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlintrc_jsonc/snapshots/migration_oxlintrc_jsonc.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_oxlintrc_jsonc/snapshots/migration_oxlintrc_jsonc.md
@@ -90,9 +90,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_partially_installed_vite_plus/snapshots/migration_partially_installed_vite_plus.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_partially_installed_vite_plus/snapshots/migration_partially_installed_vite_plus.md
index 97dd757d01..ea063b0541 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_partially_installed_vite_plus/snapshots/migration_partially_installed_vite_plus.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_partially_installed_vite_plus/snapshots/migration_partially_installed_vite_plus.md
@@ -60,11 +60,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file vite.config.ts`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_partially_migrated_pre_commit/snapshots/migration_partially_migrated_pre_commit.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_partially_migrated_pre_commit/snapshots/migration_partially_migrated_pre_commit.md
index 5bb81236bb..3b9ae80ad5 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_partially_migrated_pre_commit/snapshots/migration_partially_migrated_pre_commit.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_partially_migrated_pre_commit/snapshots/migration_partially_migrated_pre_commit.md
@@ -45,11 +45,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file .husky/pre-commit`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/snapshots/migration_playwright_test.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/snapshots/migration_playwright_test.md
index 983c329bc3..7140b67dc1 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/snapshots/migration_playwright_test.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_playwright_test/snapshots/migration_playwright_test.md
@@ -16,6 +16,7 @@ VITE+ - The Unified Toolchain for the Web
     @vitest/browser-playwright  4.1.10 → 
     @vitest/coverage-v8         4.1.10 → 
 • Package manager settings configured
+• Removed `vite` from pnpm peerDependencyRules: the core version now matches the bundled Vite
 ```
 
 ## `vpt print-file package.json`
@@ -62,9 +63,7 @@ overrides:
   vitest@*: 'catalog:'
 peerDependencyRules:
   allowAny:
-    - vite
     - vitest
   allowedVersions:
-    vite: '*'
     vitest: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_preserve_below_policy_node_pins/snapshots/migration_preserve_below_policy_node_pins.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_preserve_below_policy_node_pins/snapshots/migration_preserve_below_policy_node_pins.md
index 2ad8880f7e..db43ba69cc 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_preserve_below_policy_node_pins/snapshots/migration_preserve_below_policy_node_pins.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_preserve_below_policy_node_pins/snapshots/migration_preserve_below_policy_node_pins.md
@@ -67,11 +67,6 @@ catalogs:
     vite-plus: 
 overrides:
   vite@*: catalog:vite-stack
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vp migrate --no-interactive`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier/snapshots/migration_prettier.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier/snapshots/migration_prettier.md
index bd736ea504..310f8c21df 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier/snapshots/migration_prettier.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier/snapshots/migration_prettier.md
@@ -45,11 +45,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt stat-file .prettierrc.json --assert-not file`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_eslint_combo/snapshots/migration_prettier_eslint_combo.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_eslint_combo/snapshots/migration_prettier_eslint_combo.md
index 53f8b36cda..fbeab78a28 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_eslint_combo/snapshots/migration_prettier_eslint_combo.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_eslint_combo/snapshots/migration_prettier_eslint_combo.md
@@ -47,11 +47,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt stat-file eslint.config.mjs --assert-not file`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_ignore_unknown/snapshots/migration_prettier_ignore_unknown.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_ignore_unknown/snapshots/migration_prettier_ignore_unknown.md
index 69ad496ca8..f4c973cccf 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_ignore_unknown/snapshots/migration_prettier_ignore_unknown.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_ignore_unknown/snapshots/migration_prettier_ignore_unknown.md
@@ -45,11 +45,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt stat-file .prettierrc.json --assert-not file`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_lint_staged/snapshots/migration_prettier_lint_staged.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_lint_staged/snapshots/migration_prettier_lint_staged.md
index 09e6ef9c66..3d436e89e1 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_lint_staged/snapshots/migration_prettier_lint_staged.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_lint_staged/snapshots/migration_prettier_lint_staged.md
@@ -42,11 +42,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file vite.config.ts`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_pkg_json/snapshots/migration_prettier_pkg_json.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_pkg_json/snapshots/migration_prettier_pkg_json.md
index b0dd73997e..cf6f814751 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_pkg_json/snapshots/migration_prettier_pkg_json.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_prettier_pkg_json/snapshots/migration_prettier_pkg_json.md
@@ -43,11 +43,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vpt print-file vite.config.ts`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_rewrite_declare_module/snapshots/migration_rewrite_declare_module.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_rewrite_declare_module/snapshots/migration_rewrite_declare_module.md
index 16d65ed4e1..de54e90796 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_rewrite_declare_module/snapshots/migration_rewrite_declare_module.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_rewrite_declare_module/snapshots/migration_rewrite_declare_module.md
@@ -84,9 +84,7 @@ overrides:
   vitest@*: 'catalog:'
 peerDependencyRules:
   allowAny:
-    - vite
     - vitest
   allowedVersions:
-    vite: '*'
     vitest: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_skip_vite_dependency/snapshots/migration_skip_vite_dependency.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_skip_vite_dependency/snapshots/migration_skip_vite_dependency.md
index 6681cd8c83..c963c637ec 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_skip_vite_dependency/snapshots/migration_skip_vite_dependency.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_skip_vite_dependency/snapshots/migration_skip_vite_dependency.md
@@ -74,9 +74,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_skip_vite_peer_dependency/snapshots/migration_skip_vite_peer_dependency.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_skip_vite_peer_dependency/snapshots/migration_skip_vite_peer_dependency.md
index 3e2ad38e8c..c6df351109 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_skip_vite_peer_dependency/snapshots/migration_skip_vite_peer_dependency.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_skip_vite_peer_dependency/snapshots/migration_skip_vite_peer_dependency.md
@@ -74,9 +74,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/snapshots/migration_stale_vitest_alias_startup.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/snapshots/migration_stale_vitest_alias_startup.md
index f07bedca24..21eb2332fe 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/snapshots/migration_stale_vitest_alias_startup.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/snapshots/migration_stale_vitest_alias_startup.md
@@ -65,9 +65,4 @@ catalog:
 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_standalone_pnpm/snapshots/migration_standalone_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_standalone_pnpm/snapshots/migration_standalone_pnpm.md
index 190803a2df..7f20f7781c 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_standalone_pnpm/snapshots/migration_standalone_pnpm.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_standalone_pnpm/snapshots/migration_standalone_pnpm.md
@@ -40,9 +40,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: "catalog:"
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: "*"
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_subpath/snapshots/migration_subpath.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_subpath/snapshots/migration_subpath.md
index bea45e89e0..f8872f5d07 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_subpath/snapshots/migration_subpath.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_subpath/snapshots/migration_subpath.md
@@ -69,9 +69,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsconfig_esmoduleinterop/snapshots/migration_tsconfig_esmoduleinterop.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsconfig_esmoduleinterop/snapshots/migration_tsconfig_esmoduleinterop.md
index 02ac9a1233..317e458361 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsconfig_esmoduleinterop/snapshots/migration_tsconfig_esmoduleinterop.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_tsconfig_esmoduleinterop/snapshots/migration_tsconfig_esmoduleinterop.md
@@ -71,9 +71,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_peer_only_pnpm/snapshots/migration_upgrade_browser_peer_only_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_peer_only_pnpm/snapshots/migration_upgrade_browser_peer_only_pnpm.md
index e2e816ce40..1ee7aa7d57 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_peer_only_pnpm/snapshots/migration_upgrade_browser_peer_only_pnpm.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_peer_only_pnpm/snapshots/migration_upgrade_browser_peer_only_pnpm.md
@@ -13,6 +13,7 @@ VITE+ - The Unified Toolchain for the Web
     vite-plus  latest → 
     vite              → 
 • Package manager settings configured
+• Removed `vite` from pnpm peerDependencyRules: the core version now matches the bundled Vite
 ```
 
 ## `vpt print-file package.json`
@@ -56,10 +57,8 @@ overrides:
   vitest@*: 'catalog:'
 peerDependencyRules:
   allowAny:
-    - vite
     - vitest
   allowedVersions:
-    vite: '*'
     vitest: '*'
 ```
 
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_source_only_pnpm/snapshots/migration_upgrade_browser_source_only_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_source_only_pnpm/snapshots/migration_upgrade_browser_source_only_pnpm.md
index bf85dc3a16..be418e27f8 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_source_only_pnpm/snapshots/migration_upgrade_browser_source_only_pnpm.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_source_only_pnpm/snapshots/migration_upgrade_browser_source_only_pnpm.md
@@ -19,6 +19,7 @@ VITE+ - The Unified Toolchain for the Web
     vite                    → 
     @vitest/browser  4.1.8  → 
 • Package manager settings configured
+• Removed `vite` from pnpm peerDependencyRules: the core version now matches the bundled Vite
 ```
 
 ## `vpt print-file package.json`
@@ -59,9 +60,7 @@ overrides:
   vitest@*: 'catalog:'
 peerDependencyRules:
   allowAny:
-    - vite
     - vitest
   allowedVersions:
-    vite: '*'
     vitest: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/migrate_vitest4_community_provider.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/migrate_vitest4_community_provider.md
index e118fff0f5..e0afce59c0 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/migrate_vitest4_community_provider.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/migrate_vitest4_community_provider.md
@@ -17,6 +17,7 @@ VITE+ - The Unified Toolchain for the Web
 ◇ Migrated . to Vite+ 
 • Node   pnpm 
 • 1 file had imports rewritten
+• Removed `vite` from pnpm peerDependencyRules: the core version now matches the bundled Vite
 ```
 
 ## `vpt print-file package.json`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/migration_upgrade_browser_webdriverio_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/migration_upgrade_browser_webdriverio_pnpm.md
index f6184c755f..2264e3d0cf 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/migration_upgrade_browser_webdriverio_pnpm.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/migration_upgrade_browser_webdriverio_pnpm.md
@@ -19,6 +19,7 @@ VITE+ - The Unified Toolchain for the Web
     vite              → 
 • 1 file had imports rewritten
 • Package manager settings configured
+• Removed `vite` from pnpm peerDependencyRules: the core version now matches the bundled Vite
 ```
 
 ## `vpt print-file package.json`
@@ -77,10 +78,8 @@ overrides:
   '@vitest/browser@*': 
 peerDependencyRules:
   allowAny:
-    - vite
     - vitest
   allowedVersions:
-    vite: '*'
     vitest: '*'
 allowBuilds:
   edgedriver: true
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/preserve_community_provider_version.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/preserve_community_provider_version.md
index 621f76c9c2..0eff0f5412 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/preserve_community_provider_version.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/preserve_community_provider_version.md
@@ -18,6 +18,7 @@ VITE+ - The Unified Toolchain for the Web
     vite              → 
 • 1 file had imports rewritten
 • Package manager settings configured
+• Removed `vite` from pnpm peerDependencyRules: the core version now matches the bundled Vite
 ```
 
 ## `vpt print-file package.json`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/upgrade_community_provider_catalog.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/upgrade_community_provider_catalog.md
index 69a21053b2..f93ad80924 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/upgrade_community_provider_catalog.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_browser_webdriverio_pnpm/snapshots/upgrade_community_provider_catalog.md
@@ -75,10 +75,8 @@ allowBuilds:
   geckodriver: true
 peerDependencyRules:
   allowAny:
-    - vite
     - vitest
   allowedVersions:
-    vite: '*'
     vitest: '*'
 ```
 
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_hooks_flag_pnpm/snapshots/migration_upgrade_hooks_flag_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_hooks_flag_pnpm/snapshots/migration_upgrade_hooks_flag_pnpm.md
index 6974febf41..8e1a3d1342 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_hooks_flag_pnpm/snapshots/migration_upgrade_hooks_flag_pnpm.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_hooks_flag_pnpm/snapshots/migration_upgrade_hooks_flag_pnpm.md
@@ -18,6 +18,7 @@ VITE+ - The Unified Toolchain for the Web
 • 2 config updates applied
 • Git hooks configured
 • Package manager settings configured
+• Removed `vite` from pnpm peerDependencyRules: the core version now matches the bundled Vite
 ```
 
 ## `git config --local core.hooksPath`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_monorepo_vitest_localized_pnpm/snapshots/migration_upgrade_monorepo_vitest_localized_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_monorepo_vitest_localized_pnpm/snapshots/migration_upgrade_monorepo_vitest_localized_pnpm.md
index a095fef1db..28c87514c2 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_monorepo_vitest_localized_pnpm/snapshots/migration_upgrade_monorepo_vitest_localized_pnpm.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_monorepo_vitest_localized_pnpm/snapshots/migration_upgrade_monorepo_vitest_localized_pnpm.md
@@ -13,6 +13,7 @@ VITE+ - The Unified Toolchain for the Web
     vite-plus  latest → 
     vite              → 
 • Package manager settings configured
+• Removed `vite` from pnpm peerDependencyRules: the core version now matches the bundled Vite
 ```
 
 ## `vpt print-file package.json`
@@ -68,9 +69,7 @@ overrides:
   vitest@*: 'catalog:'
 peerDependencyRules:
   allowAny:
-    - vite
     - vitest
   allowedVersions:
-    vite: '*'
     vitest: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_nuxt_test_utils_monorepo/snapshots/migration_upgrade_nuxt_test_utils_monorepo.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_nuxt_test_utils_monorepo/snapshots/migration_upgrade_nuxt_test_utils_monorepo.md
index 465554a573..fe95d68337 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_nuxt_test_utils_monorepo/snapshots/migration_upgrade_nuxt_test_utils_monorepo.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_nuxt_test_utils_monorepo/snapshots/migration_upgrade_nuxt_test_utils_monorepo.md
@@ -62,10 +62,8 @@ overrides:
   vitest@*: 'catalog:'
 peerDependencyRules:
   allowAny:
-    - vite
     - vitest
   allowedVersions:
-    vite: '*'
     vitest: '*'
 ```
 
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_peer_vitest_catalog_pnpm/snapshots/migration_upgrade_peer_vitest_catalog_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_peer_vitest_catalog_pnpm/snapshots/migration_upgrade_peer_vitest_catalog_pnpm.md
index 15698124cc..a3a367b082 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_peer_vitest_catalog_pnpm/snapshots/migration_upgrade_peer_vitest_catalog_pnpm.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_peer_vitest_catalog_pnpm/snapshots/migration_upgrade_peer_vitest_catalog_pnpm.md
@@ -13,6 +13,7 @@ VITE+ - The Unified Toolchain for the Web
     vite-plus  latest → 
     vite              → 
 • Package manager settings configured
+• Removed `vite` from pnpm peerDependencyRules: the core version now matches the bundled Vite
 ```
 
 ## `vpt print-file package.json`
@@ -50,11 +51,6 @@ catalogs:
   test: {}
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vp migrate --no-interactive`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_pnpm_bundled_catalog_dep/snapshots/migration_upgrade_pnpm_bundled_catalog_dep.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_pnpm_bundled_catalog_dep/snapshots/migration_upgrade_pnpm_bundled_catalog_dep.md
index 9e63d36f5e..ee30c7c0aa 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_pnpm_bundled_catalog_dep/snapshots/migration_upgrade_pnpm_bundled_catalog_dep.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_pnpm_bundled_catalog_dep/snapshots/migration_upgrade_pnpm_bundled_catalog_dep.md
@@ -49,9 +49,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_pnpm_catalogs_default/snapshots/migration_upgrade_pnpm_catalogs_default.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_pnpm_catalogs_default/snapshots/migration_upgrade_pnpm_catalogs_default.md
index 1cb71f44bd..32b831b67c 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_pnpm_catalogs_default/snapshots/migration_upgrade_pnpm_catalogs_default.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_pnpm_catalogs_default/snapshots/migration_upgrade_pnpm_catalogs_default.md
@@ -52,11 +52,6 @@ catalogs:
     rari: ^0.14.12
 overrides:
   vite@*: catalog:build
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vp migrate --no-interactive`
@@ -106,9 +101,4 @@ catalogs:
     rari: ^0.14.12
 overrides:
   vite@*: catalog:build
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_setup_full_pnpm/snapshots/migration_upgrade_setup_full_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_setup_full_pnpm/snapshots/migration_upgrade_setup_full_pnpm.md
index a8618adbf1..145033fe63 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_setup_full_pnpm/snapshots/migration_upgrade_setup_full_pnpm.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_setup_full_pnpm/snapshots/migration_upgrade_setup_full_pnpm.md
@@ -22,6 +22,7 @@ VITE+ - The Unified Toolchain for the Web
 • 2 config updates applied
 • Node version manager file migrated to .node-version
 • Package manager settings configured
+• Removed `vite` from pnpm peerDependencyRules: the core version now matches the bundled Vite
 → Manual follow-up:
   - Remove the "volta" field from package.json
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_setup_skipped_default_pnpm/snapshots/migration_upgrade_setup_skipped_default_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_setup_skipped_default_pnpm/snapshots/migration_upgrade_setup_skipped_default_pnpm.md
index 227c579723..69513ff898 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_setup_skipped_default_pnpm/snapshots/migration_upgrade_setup_skipped_default_pnpm.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_setup_skipped_default_pnpm/snapshots/migration_upgrade_setup_skipped_default_pnpm.md
@@ -13,6 +13,7 @@ VITE+ - The Unified Toolchain for the Web
     vite-plus  0.1.21 → 
     vite              → 
 • Package manager settings configured
+• Removed `vite` from pnpm peerDependencyRules: the core version now matches the bundled Vite
 • Skipped editor, hooks, and lint setup. Run `vp migrate --full` to apply them.
 ```
 
@@ -47,11 +48,6 @@ pnpm settings consolidated by the version upgrade
 ```
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 catalog:
   vite: npm:@voidzero-dev/vite-plus-core@
   vite-plus: 
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_stale_local_pnpm/snapshots/migration_upgrade_stale_local_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_stale_local_pnpm/snapshots/migration_upgrade_stale_local_pnpm.md
index e02b10c9fb..9e2e00f978 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_stale_local_pnpm/snapshots/migration_upgrade_stale_local_pnpm.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_stale_local_pnpm/snapshots/migration_upgrade_stale_local_pnpm.md
@@ -16,6 +16,7 @@ VITE+ - The Unified Toolchain for the Web
     vite-plus  0.1.24 → 
     vite              → 
 • Package manager settings configured
+• Removed `vite` from pnpm peerDependencyRules: the core version now matches the bundled Vite
 ```
 
 ## `vpt print-file package.json`
@@ -46,11 +47,6 @@ pnpm settings should be consolidated here
 ```
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 catalog:
   vite: npm:@voidzero-dev/vite-plus-core@
   vite-plus: 
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_vitest_reference_whitespace_pnpm/snapshots/migration_upgrade_vitest_reference_whitespace_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_vitest_reference_whitespace_pnpm/snapshots/migration_upgrade_vitest_reference_whitespace_pnpm.md
index 3ace8f239c..f5ba43cf8c 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_vitest_reference_whitespace_pnpm/snapshots/migration_upgrade_vitest_reference_whitespace_pnpm.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_upgrade_vitest_reference_whitespace_pnpm/snapshots/migration_upgrade_vitest_reference_whitespace_pnpm.md
@@ -10,6 +10,7 @@ VITE+ - The Unified Toolchain for the Web
 ◇ Migrated . to Vite+ 
 • Node   pnpm 
 • 2 config updates applied, 1 file had imports rewritten
+• Removed `vite` from pnpm peerDependencyRules: the core version now matches the bundled Vite
 ```
 
 ## `vpt print-file package.json`
@@ -54,11 +55,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
 
 ## `vp migrate --no-interactive`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vite_plus_in_dependencies_pnpm/snapshots/migration_vite_plus_in_dependencies_pnpm.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vite_plus_in_dependencies_pnpm/snapshots/migration_vite_plus_in_dependencies_pnpm.md
index d038952dc4..16c8fcc19a 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vite_plus_in_dependencies_pnpm/snapshots/migration_vite_plus_in_dependencies_pnpm.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vite_plus_in_dependencies_pnpm/snapshots/migration_vite_plus_in_dependencies_pnpm.md
@@ -45,9 +45,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vite_version/snapshots/migration_vite_version.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vite_version/snapshots/migration_vite_version.md
index 2e5cc07b7f..6f965f0493 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vite_version/snapshots/migration_vite_version.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vite_version/snapshots/migration_vite_version.md
@@ -41,9 +41,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vitest_import_only/snapshots/migration_vitest_import_only.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vitest_import_only/snapshots/migration_vitest_import_only.md
index d20d676a0a..85efefa5b0 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vitest_import_only/snapshots/migration_vitest_import_only.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vitest_import_only/snapshots/migration_vitest_import_only.md
@@ -50,9 +50,4 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: 'catalog:'
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vitest_peer_dep/snapshots/migration_vitest_peer_dep.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vitest_peer_dep/snapshots/migration_vitest_peer_dep.md
index f67ce74fd3..56e0db8748 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vitest_peer_dep/snapshots/migration_vitest_peer_dep.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_vitest_peer_dep/snapshots/migration_vitest_peer_dep.md
@@ -51,9 +51,7 @@ overrides:
   vitest@*: 'catalog:'
 peerDependencyRules:
   allowAny:
-    - vite
     - vitest
   allowedVersions:
-    vite: '*'
     vitest: '*'
 ```
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_members/snapshots/migration_workspace_members.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_members/snapshots/migration_workspace_members.md
index 8add5c6b23..63182370c6 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_members/snapshots/migration_workspace_members.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_workspace_members/snapshots/migration_workspace_members.md
@@ -27,11 +27,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: "catalog:"
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: "*"
 ```
 
 ## `vp run -r build`
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots/new_vite_monorepo.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots/new_vite_monorepo.md
index bb258b1685..8057d0a2e4 100644
--- a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots/new_vite_monorepo.md
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/new_vite_monorepo/snapshots/new_vite_monorepo.md
@@ -93,11 +93,6 @@ catalog:
   vite-plus: 
 overrides:
   vite@*: "catalog:"
-peerDependencyRules:
-  allowAny:
-    - vite
-  allowedVersions:
-    vite: "*"
 ```
 
 ## `vpt stat-file vite-plus-monorepo/.gitignore --assert file`
diff --git a/packages/cli/BUNDLING.md b/packages/cli/BUNDLING.md
index 8dcfa2bcd6..49bb00e8f6 100644
--- a/packages/cli/BUNDLING.md
+++ b/packages/cli/BUNDLING.md
@@ -31,12 +31,14 @@ commands start. Keep the canonical name in release metadata and alias targets
 to identify the published package.
 
 For npm projects without overrides, pin a declared `vite` alias to the exact
-core version that the installed `vite-plus` uses. npm can replace a tagged
-alias such as `npm:@voidzero-dev/vite-plus-core@latest` with upstream Vite to
-satisfy Vitest's peer range. The CLI rejects that mismatch and directs users to
-`vp migrate`, which writes an exact alias and the package-manager overrides.
-The npm fixtures check shared core identity with an exact alias and rejection
-of the tagged layout.
+core version that the installed `vite-plus` uses. With a `1.x` core, npm
+replaced a tagged alias such as `npm:@voidzero-dev/vite-plus-core@latest` with
+upstream Vite to satisfy Vitest's peer range. The encoded core version
+satisfies that range, so npm now keeps the alias on the core. The CLI still
+rejects upstream Vite and a core version that differs from its alias, and
+directs users to `vp migrate`, which writes an exact alias and the
+package-manager overrides. The npm fixtures check shared core identity with an
+exact alias, a tagged alias, and no project alias.
 
 ## Build Steps
 

From 3558bd55a3fdc39ffeb88b1e18c3dfba0b377bcc Mon Sep 17 00:00:00 2001
From: "MK (fengmk2)" 
Date: Fri, 6 Nov 2026 16:26:10 +0800
Subject: [PATCH 05/10] fix(release): wait for core at its own version in the
 Homebrew update

The Homebrew updater waited for @voidzero-dev/vite-plus-core at the CLI
version. The core version now encodes the bundled Vite version, so that
wait could never succeed. Read the core version from the `vite` alias of
the published vite-plus manifest instead. The manual recovery command keeps
its single version argument.

Co-Authored-By: Claude Opus 5.5 
---
 .../scripts/__tests__/homebrew-formula.mjs    | 23 +++++++++++++
 .github/scripts/update-homebrew-formula.mjs   | 32 ++++++++++++++++---
 rfcs/core-version-alignment.md                |  1 +
 3 files changed, 51 insertions(+), 5 deletions(-)

diff --git a/.github/scripts/__tests__/homebrew-formula.mjs b/.github/scripts/__tests__/homebrew-formula.mjs
index 3b280a0c1a..b78520b545 100644
--- a/.github/scripts/__tests__/homebrew-formula.mjs
+++ b/.github/scripts/__tests__/homebrew-formula.mjs
@@ -6,6 +6,7 @@ import { test } from 'node:test';
 import { fileURLToPath } from 'node:url';
 
 import {
+  publishedCoreVersion,
   publishedFormulaVersion,
   releaseAssets,
   targets,
@@ -143,3 +144,25 @@ await test('updates preserve recipe changes and reset only a previous version re
     /Unsafe formula URL/,
   );
 });
+
+await test('reads the core version from the published CLI alias', async () => {
+  const manifest = (dependencies) => async (url) => {
+    assert.equal(url, `https://registry.npmjs.org/vite-plus/${version}`);
+    return Response.json({ dependencies });
+  };
+  assert.equal(
+    await publishedCoreVersion(
+      version,
+      manifest({ vite: 'npm:@voidzero-dev/vite-plus-core@8.3.501' }),
+    ),
+    '8.3.501',
+  );
+  await assert.rejects(
+    publishedCoreVersion(version, manifest({ vite: '^8.0.0' })),
+    /does not alias vite to @voidzero-dev\/vite-plus-core/,
+  );
+  await assert.rejects(
+    publishedCoreVersion(version, async () => new Response('', { status: 404 })),
+    /HTTP 404/,
+  );
+});
diff --git a/.github/scripts/update-homebrew-formula.mjs b/.github/scripts/update-homebrew-formula.mjs
index ba7195d22f..afce258973 100644
--- a/.github/scripts/update-homebrew-formula.mjs
+++ b/.github/scripts/update-homebrew-formula.mjs
@@ -137,6 +137,24 @@ export async function verifyRelease(version, release, fetchImpl = fetch) {
   return assets;
 }
 
+const CORE_ALIAS_PREFIX = 'npm:@voidzero-dev/vite-plus-core@';
+
+// The core version encodes the bundled Vite version, so it differs from the
+// vite-plus version (rfcs/core-version-alignment.md). Read it from the `vite`
+// alias of the published CLI. The release job publishes the CLI before this
+// script runs.
+export async function publishedCoreVersion(version, fetchImpl = fetch) {
+  const url = `https://registry.npmjs.org/vite-plus/${version}`;
+  const response = await fetchImpl(url, { signal: AbortSignal.timeout(120_000) });
+  assert.ok(response.ok, `Download failed: ${url} (HTTP ${response.status})`);
+  const spec = (await response.json()).dependencies?.vite;
+  assert.ok(
+    typeof spec === 'string' && spec.startsWith(CORE_ALIAS_PREFIX),
+    `vite-plus@${version} does not alias vite to @voidzero-dev/vite-plus-core: ${spec}`,
+  );
+  return spec.slice(CORE_ALIAS_PREFIX.length);
+}
+
 async function main() {
   const [version] = process.argv.slice(2);
   stableVersion(version ?? '');
@@ -146,13 +164,17 @@ async function main() {
     }),
   );
   const assets = await verifyRelease(version, release);
+  const coreVersion = await publishedCoreVersion(version);
   const packages = [
-    'vite-plus',
-    '@voidzero-dev/vite-plus-core',
-    '@voidzero-dev/vite-plus-prompts',
-    ...targets.map(([, , , platform]) => `@voidzero-dev/vite-plus-${platform}`),
+    { name: 'vite-plus', version },
+    { name: '@voidzero-dev/vite-plus-core', version: coreVersion },
+    { name: '@voidzero-dev/vite-plus-prompts', version },
+    ...targets.map(([, , , platform]) => ({
+      name: `@voidzero-dev/vite-plus-${platform}`,
+      version,
+    })),
   ];
-  await waitForNpmPackagesFromEnv(packages.map((name) => ({ name, version })));
+  await waitForNpmPackagesFromEnv(packages);
   const file = new URL('../../HomebrewFormula/vp.rb', import.meta.url);
   const source = await readFile(file, 'utf8');
   const result = updateFormula(source, version, assets);
diff --git a/rfcs/core-version-alignment.md b/rfcs/core-version-alignment.md
index 92310f4abc..f50d9883aa 100644
--- a/rfcs/core-version-alignment.md
+++ b/rfcs/core-version-alignment.md
@@ -347,6 +347,7 @@ cannot mix it up with a published version.
 | `packages/tools/src/local-npm-registry.ts`, `pack-local-vite-plus.ts`                                                            | No change: both serve the packed tarball's own version                                                                                                                                                                                                                                                                                                                        |
 | `.github/scripts/upgrade-deps.ts`                                                                                                | Reset the core version after a bundled Vite change                                                                                                                                                                                                                                                                                                                            |
 | `.github/renovate.json`                                                                                                          | No change                                                                                                                                                                                                                                                                                                                                                                     |
+| `.github/scripts/update-homebrew-formula.mjs`                                                                                    | Read the core version from the `vite` alias of the published `vite-plus` manifest, and wait for core at that version                                                                                                                                                                                                                                                          |
 | `.claude/skills/release-manager/SKILL.md`                                                                                        | Read both versions from the release branch; publish checks use the core version for core                                                                                                                                                                                                                                                                                      |
 | `packages/core/BUNDLING.md`                                                                                                      | Document the version scheme next to `bundledVersions`                                                                                                                                                                                                                                                                                                                         |
 | `MAINTENANCE.md`                                                                                                                 | Mention the two versions in the release description                                                                                                                                                                                                                                                                                                                           |

From 6c9f9344092fcc5f440e5ecffba91e7e4e9fba96 Mon Sep 17 00:00:00 2001
From: "MK (fengmk2)" 
Date: Mon, 9 Nov 2026 18:28:56 +0800
Subject: [PATCH 06/10] refactor: simplify the core version and peer-rule code

- Decide the stale pnpm peer-rule keys (`vitest` and `vite`) in one
  function, and check and remove them with shared helpers in the YAML
  writer, the package.json merge, and the satisfied check.
- Define the core alias prefix once for the CLI and once for the scripts.
- Read the bundled core manifest once in resolve-core, and compare the
  project's core with the core that the CLI resolves.
- Read the core version only from VP_CORE_VERSION or the core manifest.
- Reuse the core-version helpers in upgrade-deps, the download helper in
  the Homebrew updater, and readPackageJsonIfExists in the migrator.
- Remove the unused VP_CORE_VERSION from the ecosystem CI migrate env.

Co-Authored-By: Claude Opus 5.5 
---
 .github/scripts/update-homebrew-formula.mjs   | 35 ++++---
 .github/scripts/upgrade-deps.ts               | 17 ++--
 ecosystem-ci/patch-project.ts                 |  3 +-
 ecosystem-ci/paths.ts                         | 12 +--
 .../cli/src/migration/migrator/catalog.ts     | 46 +++------
 packages/cli/src/migration/migrator/shared.ts | 12 ++-
 .../migration/migrator/vite-plus-bootstrap.ts | 23 ++---
 .../migration/migrator/vitest-ecosystem.ts    | 95 ++++++++-----------
 packages/cli/src/resolve-core.ts              | 49 +++++-----
 .../cli/src/utils/__tests__/constants.spec.ts | 21 +---
 packages/cli/src/utils/constants.ts           | 13 +--
 packages/tools/src/core-version.ts            | 24 ++---
 12 files changed, 150 insertions(+), 200 deletions(-)

diff --git a/.github/scripts/update-homebrew-formula.mjs b/.github/scripts/update-homebrew-formula.mjs
index afce258973..edd97dcd00 100644
--- a/.github/scripts/update-homebrew-formula.mjs
+++ b/.github/scripts/update-homebrew-formula.mjs
@@ -5,7 +5,8 @@ import { readFile, writeFile } from 'node:fs/promises';
 import { resolve } from 'node:path';
 import { pathToFileURL } from 'node:url';
 
-import { waitForNpmPackagesFromEnv } from './wait-for-npm-packages.ts';
+import { CORE_ALIAS_PREFIX } from '../../packages/tools/src/core-version.ts';
+import { DEFAULT_NPM_REGISTRY, waitForNpmPackagesFromEnv } from './wait-for-npm-packages.ts';
 
 export const targets = [
   ['macos', 'arm', 'aarch64-apple-darwin', 'darwin-arm64'],
@@ -105,15 +106,17 @@ export function updateFormula(source, version, assets) {
   return updated;
 }
 
+async function download(url, fetchImpl) {
+  const response = await fetchImpl(url, { signal: AbortSignal.timeout(120_000) });
+  assert.ok(response.ok, `Download failed: ${url} (HTTP ${response.status})`);
+  return response;
+}
+
 export async function verifyRelease(version, release, fetchImpl = fetch) {
   stableVersion(version);
   assert.equal(release.tag_name, `v${version}`);
   assert.ok(!release.draft && !release.prerelease, 'Release must be published and stable');
-  const get = async (url) => {
-    const response = await fetchImpl(url, { signal: AbortSignal.timeout(120_000) });
-    assert.ok(response.ok, `Download failed: ${url} (HTTP ${response.status})`);
-    return response;
-  };
+  const get = (url) => download(url, fetchImpl);
   // The formula is added with the per-user Homebrew runtime. Check the release
   // tag, never main, before enabling installation.
   await get(`https://raw.githubusercontent.com/${repository}/v${version}/HomebrewFormula/vp.rb`);
@@ -137,17 +140,17 @@ export async function verifyRelease(version, release, fetchImpl = fetch) {
   return assets;
 }
 
-const CORE_ALIAS_PREFIX = 'npm:@voidzero-dev/vite-plus-core@';
-
 // The core version encodes the bundled Vite version, so it differs from the
 // vite-plus version (rfcs/core-version-alignment.md). Read it from the `vite`
 // alias of the published CLI. The release job publishes the CLI before this
 // script runs.
-export async function publishedCoreVersion(version, fetchImpl = fetch) {
-  const url = `https://registry.npmjs.org/vite-plus/${version}`;
-  const response = await fetchImpl(url, { signal: AbortSignal.timeout(120_000) });
-  assert.ok(response.ok, `Download failed: ${url} (HTTP ${response.status})`);
-  const spec = (await response.json()).dependencies?.vite;
+export async function publishedCoreVersion(
+  version,
+  fetchImpl = fetch,
+  registry = process.env.PUBLISH_REGISTRY ?? DEFAULT_NPM_REGISTRY,
+) {
+  const url = `${registry.replace(/\/+$/, '')}/vite-plus/${version}`;
+  const spec = (await (await download(url, fetchImpl)).json()).dependencies?.vite;
   assert.ok(
     typeof spec === 'string' && spec.startsWith(CORE_ALIAS_PREFIX),
     `vite-plus@${version} does not alias vite to @voidzero-dev/vite-plus-core: ${spec}`,
@@ -163,8 +166,10 @@ async function main() {
       encoding: 'utf8',
     }),
   );
-  const assets = await verifyRelease(version, release);
-  const coreVersion = await publishedCoreVersion(version);
+  const [assets, coreVersion] = await Promise.all([
+    verifyRelease(version, release),
+    publishedCoreVersion(version),
+  ]);
   const packages = [
     { name: 'vite-plus', version },
     { name: '@voidzero-dev/vite-plus-core', version: coreVersion },
diff --git a/.github/scripts/upgrade-deps.ts b/.github/scripts/upgrade-deps.ts
index 434ee3c50b..573467e315 100644
--- a/.github/scripts/upgrade-deps.ts
+++ b/.github/scripts/upgrade-deps.ts
@@ -2,7 +2,11 @@ import { execFileSync } from 'node:child_process';
 import fs from 'node:fs';
 import path from 'node:path';
 
-import { decodeCoreVersion, encodeCoreVersion } from '../../packages/tools/src/core-version.ts';
+import {
+  CORE_PACKAGE_NAME,
+  encodeCoreVersion,
+  tryDecodeCoreVersion,
+} from '../../packages/tools/src/core-version.ts';
 import {
   findLatestStableVersionForMajor,
   getBundledDependencyRanges,
@@ -550,18 +554,11 @@ function syncCoreVersion(pkg: { version?: unknown; bundledVersions?: Record.tgz');
+  return tgzVersion('vite-plus');
 }
 
 /**
@@ -28,18 +28,16 @@ export function vitePlusTgzVersion(): string {
  * it differs from {@link vitePlusTgzVersion} (rfcs/core-version-alignment.md).
  */
 export function vitePlusCoreTgzVersion(): string {
-  return tgzVersion(
-    /^voidzero-dev-vite-plus-core-(\d.*)\.tgz$/,
-    'voidzero-dev-vite-plus-core-.tgz',
-  );
+  return tgzVersion('voidzero-dev-vite-plus-core');
 }
 
-function tgzVersion(pattern: RegExp, description: string): string {
+function tgzVersion(prefix: string): string {
+  const pattern = new RegExp(`^${prefix}-(\\d.*)\\.tgz$`);
   const version = readdirSync(tgzDir)
     .map((entry) => pattern.exec(entry)?.[1])
     .find(Boolean);
   if (!version) {
-    throw new Error(`No ${description} found in ${tgzDir}`);
+    throw new Error(`No ${prefix}-.tgz found in ${tgzDir}`);
   }
   return version;
 }
diff --git a/packages/cli/src/migration/migrator/catalog.ts b/packages/cli/src/migration/migrator/catalog.ts
index cfc1e9a6f7..8401e9e0b5 100644
--- a/packages/cli/src/migration/migrator/catalog.ts
+++ b/packages/cli/src/migration/migrator/catalog.ts
@@ -25,11 +25,13 @@ import {
   managedOverridePackages,
   managedPeerDependencyRuleKeys,
   mergeManagedPnpmPeerDependencyRules,
+  peerDependencyRulesHaveKey,
   pruneLegacyWrapperAliases,
   removeManagedVitestEntry,
   removeYamlMapVitestEntry,
   rewriteMonorepoProject,
   shouldDropProviderOverrideKey,
+  stalePeerDependencyRuleKeys,
   workspaceUsesWebdriverioProvider,
 } from '../migrator.ts';
 import { type MigrationReport } from '../report.ts';
@@ -41,6 +43,7 @@ import {
   VITEST_IS_MANAGED_OVERRIDE,
   isPlainRecord,
   pnpmOverrideKey,
+  readPackageJsonIfExists,
   vitePeerDependencyRuleRequired,
   type CatalogDependencyResolver,
   type PackageJsonDependencyField,
@@ -372,15 +375,9 @@ export function rewritePnpmWorkspaceYaml(
         allowAny.add(scalarString(allowAnyNode));
       }
     }
-    // Common case: drop any lingering managed `vitest` allowAny entry.
-    if (!usesVitest && VITEST_IS_MANAGED_OVERRIDE) {
-      allowAny.items = allowAny.items.filter((n) => n.value !== 'vitest');
-    }
-    // A `vite` entry only hides real incompatibilities once the core alias
-    // satisfies `vite` peer ranges (see `vitePeerDependencyRuleRequired`).
-    if (!vitePeerDependencyRuleRequired()) {
-      allowAny.items = allowAny.items.filter((n) => n.value !== 'vite');
-    }
+    // Drop lingering managed `vitest` and stale `vite` entries.
+    const stalePeerRuleKeys = stalePeerDependencyRuleKeys(usesVitest);
+    allowAny.items = allowAny.items.filter((n) => !stalePeerRuleKeys.includes(n.value));
     const peerRuleKeys = managedPeerDependencyRuleKeys(usesVitest);
     const existing = new Set(allowAny.items.map((n) => n.value));
     for (const key of peerRuleKeys) {
@@ -400,12 +397,8 @@ export function rewritePnpmWorkspaceYaml(
     } else {
       allowedVersions = new YAMLMap, Scalar>();
     }
-    // Common case: drop any lingering managed `vitest` allowedVersions entry.
-    if (!usesVitest) {
-      removeYamlMapVitestEntry(allowedVersions);
-    }
-    if (!vitePeerDependencyRuleRequired()) {
-      allowedVersions.delete('vite');
+    for (const key of stalePeerRuleKeys) {
+      allowedVersions.delete(key);
     }
     for (const key of peerRuleKeys) {
       // - vite: '*'
@@ -1227,7 +1220,6 @@ export function rewriteRootWorkspacePackageJson(
       // to a `catalog:` reference. See https://github.com/oven-sh/bun/issues/8406.
       setDirectViteEdge(pkg, true, catalogDependencyResolver);
     } else if (packageManager === PackageManager.pnpm) {
-      const overrideKeys = Object.keys(managedOverridePackages(workspaceUsesVitest));
       const usePnpmWorkspaceSettings = pnpmSupportsWorkspaceSettings(pnpmVersion ?? '');
       if (!usePnpmWorkspaceSettings) {
         // Strip selector-shaped overrides (e.g. `parent>@vitest/browser-playwright`)
@@ -1248,6 +1240,7 @@ export function rewriteRootWorkspacePackageJson(
           ),
         };
       } else {
+        const overrideKeys = Object.keys(managedOverridePackages(workspaceUsesVitest));
         for (const key of [...overrideKeys, ...PROVIDER_OVERRIDE_DROP_NAMES]) {
           if (pkg.resolutions?.[key]) {
             delete pkg.resolutions[key];
@@ -1386,27 +1379,16 @@ export function readPnpmWorkspacePeerDependencyRules(
   return doc?.peerDependencyRules;
 }
 
-function rulesHaveVite(
-  rules: { allowAny?: unknown; allowedVersions?: unknown } | null | undefined,
-): boolean {
-  return (
-    (Array.isArray(rules?.allowAny) && rules.allowAny.includes('vite')) ||
-    (isPlainRecord(rules?.allowedVersions) && rules.allowedVersions.vite !== undefined)
-  );
-}
-
 // Whether the root pnpm config (the package.json `pnpm` field or
 // pnpm-workspace.yaml) holds a `vite` peer rule.
 export function hasVitePeerDependencyRule(projectPath: string): boolean {
-  if (rulesHaveVite(readPnpmWorkspacePeerDependencyRules(projectPath))) {
+  if (peerDependencyRulesHaveKey(readPnpmWorkspacePeerDependencyRules(projectPath), ['vite'])) {
     return true;
   }
-  const packageJsonPath = path.join(projectPath, 'package.json');
-  if (!fs.existsSync(packageJsonPath)) {
-    return false;
-  }
-  const pkg = readJsonFile(packageJsonPath) as { pnpm?: PnpmPackageJsonSettings };
-  return rulesHaveVite(pkg.pnpm?.peerDependencyRules);
+  const pkg = readPackageJsonIfExists(path.join(projectPath, 'package.json')) as
+    | { pnpm?: PnpmPackageJsonSettings }
+    | undefined;
+  return peerDependencyRulesHaveKey(pkg?.pnpm?.peerDependencyRules, ['vite']);
 }
 
 // Capture the `vite` peer rule before a migration step. Call the returned
diff --git a/packages/cli/src/migration/migrator/shared.ts b/packages/cli/src/migration/migrator/shared.ts
index c0d8a1fa00..17ba284ad7 100644
--- a/packages/cli/src/migration/migrator/shared.ts
+++ b/packages/cli/src/migration/migrator/shared.ts
@@ -5,7 +5,11 @@ import * as prompts from '@voidzero-dev/vite-plus-prompts';
 import type { OxlintConfig } from 'oxlint';
 import { coerce, satisfies, tryParse } from 'verkit';
 
-import { VITEST_VERSION, VITE_PLUS_OVERRIDE_PACKAGES } from '../../utils/constants.ts';
+import {
+  VITEST_VERSION,
+  VITE_PLUS_CORE_ALIAS_PREFIX,
+  VITE_PLUS_OVERRIDE_PACKAGES,
+} from '../../utils/constants.ts';
 import { readJsonFile } from '../../utils/json.ts';
 import { detectPackageMetadata } from '../../utils/package.ts';
 import { displayRelative } from '../../utils/path.ts';
@@ -211,8 +215,6 @@ export const VITEST_BROWSER_DEP_NAMES = [
 // and must be left untouched.
 export const VITEST_IS_MANAGED_OVERRIDE = 'vitest' in VITE_PLUS_OVERRIDE_PACKAGES;
 
-const CORE_ALIAS_PREFIX = 'npm:@voidzero-dev/vite-plus-core@';
-
 /**
  * Whether pnpm still needs the managed `vite` peer rule (`allowAny: [vite]`,
  * `allowedVersions: { vite: '*' }`) for the given core alias.
@@ -228,10 +230,10 @@ const CORE_ALIAS_PREFIX = 'npm:@voidzero-dev/vite-plus-core@';
 export function vitePeerDependencyRuleRequired(
   viteSpec: string | undefined = VITE_PLUS_OVERRIDE_PACKAGES.vite,
 ): boolean {
-  if (!viteSpec?.startsWith(CORE_ALIAS_PREFIX)) {
+  if (!viteSpec?.startsWith(VITE_PLUS_CORE_ALIAS_PREFIX)) {
     return true;
   }
-  const version = tryParse(viteSpec.slice(CORE_ALIAS_PREFIX.length));
+  const version = tryParse(viteSpec.slice(VITE_PLUS_CORE_ALIAS_PREFIX.length));
   return (
     !version ||
     version.major < 8 ||
diff --git a/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts b/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts
index df9ecae5e5..51f07e7bb4 100644
--- a/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts
+++ b/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts
@@ -29,6 +29,7 @@ import {
   managedOverridePackages,
   managedPeerDependencyRuleKeys,
   mergeManagedPnpmPeerDependencyRules,
+  peerDependencyRulesHaveKey,
   migratePnpmSettingsToWorkspaceYaml,
   normalizeVitestPeerCatalogSpec,
   pnpmPackageJsonSettingsPending,
@@ -46,6 +47,7 @@ import {
   rewritePnpmWorkspaceYaml,
   rewriteYarnrcYml,
   setDirectViteEdge,
+  stalePeerDependencyRuleKeys,
   takePnpmWorkspaceSettings,
   trackVitePeerDependencyRuleRemoval,
   vitestEcosystemCatalogReferencesPending,
@@ -65,7 +67,6 @@ import {
   managedOverrideKey,
   pnpmMajor,
   pnpmOverrideKey,
-  vitePeerDependencyRuleRequired,
   type CatalogDependencyResolver,
   type ManagedOverrideKeyStyle,
   type PnpmPackageJsonSettings,
@@ -235,24 +236,12 @@ function pnpmPeerDependencyRulesSatisfyVitePlus(
     | undefined,
   usesVitest: boolean,
 ): boolean {
-  const allowAny = new Set(peerDependencyRules?.allowAny ?? []);
-  const allowedVersions = peerDependencyRules?.allowedVersions ?? {};
-  // Common case: a lingering managed `vitest` peer rule is NOT satisfied.
-  if (
-    !usesVitest &&
-    VITEST_IS_MANAGED_OVERRIDE &&
-    (allowAny.has('vitest') || allowedVersions.vitest !== undefined)
-  ) {
-    return false;
-  }
-  // A stale `vite` peer rule is not satisfied once the core alias satisfies
-  // `vite` peer ranges. It must be removed (see `vitePeerDependencyRuleRequired`).
-  if (
-    !vitePeerDependencyRuleRequired() &&
-    (allowAny.has('vite') || allowedVersions.vite !== undefined)
-  ) {
+  // A lingering managed `vitest` rule or a stale `vite` rule is NOT satisfied.
+  if (peerDependencyRulesHaveKey(peerDependencyRules, stalePeerDependencyRuleKeys(usesVitest))) {
     return false;
   }
+  const allowAny = new Set(peerDependencyRules?.allowAny ?? []);
+  const allowedVersions = peerDependencyRules?.allowedVersions ?? {};
   const ruleKeys = managedPeerDependencyRuleKeys(usesVitest);
   return ruleKeys.every((key) => allowAny.has(key) && allowedVersions[key] === '*');
 }
diff --git a/packages/cli/src/migration/migrator/vitest-ecosystem.ts b/packages/cli/src/migration/migrator/vitest-ecosystem.ts
index cfa1797b25..35824336a7 100644
--- a/packages/cli/src/migration/migrator/vitest-ecosystem.ts
+++ b/packages/cli/src/migration/migrator/vitest-ecosystem.ts
@@ -27,6 +27,7 @@ import {
   REMOVE_PACKAGES,
   VITEST_BROWSER_DEP_NAMES,
   VITEST_IS_MANAGED_OVERRIDE,
+  isPlainRecord,
   pnpmOverrideKey,
   vitePeerDependencyRuleRequired,
   type CatalogDependencyResolver,
@@ -540,40 +541,36 @@ function managedVitestOverrideKeys(keyStyle: ManagedOverrideKeyStyle): string[]
   return keyStyle === 'pnpm-ranged' ? ['vitest', pnpmOverrideKey('vitest')] : ['vitest'];
 }
 
-// Remove the managed `vitest` entry from pnpm peerDependencyRules (its
-// `allowAny` array entry and `allowedVersions.vitest`), in place. Works on both
-// the package.json `pnpm.peerDependencyRules` JSON shape and the same shape read
-// back from pnpm-workspace.yaml.
-export function removeVitestPeerDependencyRule(peerDependencyRules: {
-  allowAny?: string[];
-  allowedVersions?: Record;
-}): void {
-  if (!VITEST_IS_MANAGED_OVERRIDE) {
-    return;
+// The pnpm peerDependencyRules keys that migration removes:
+// - `vitest`, a lingering managed rule, when the workspace does not use Vitest.
+// - `vite`, once its core alias satisfies `vite` peer ranges (see
+//   `vitePeerDependencyRuleRequired`). Then the rule only hides real
+//   incompatibilities. Migration cannot tell whether an earlier migration or the
+//   user wrote the entry; its only purpose was to accept the `1.x` alias, so
+//   both go.
+export function stalePeerDependencyRuleKeys(usesVitest: boolean): string[] {
+  const keys: string[] = [];
+  if (!usesVitest && VITEST_IS_MANAGED_OVERRIDE) {
+    keys.push('vitest');
   }
-  if (Array.isArray(peerDependencyRules.allowAny)) {
-    peerDependencyRules.allowAny = peerDependencyRules.allowAny.filter((key) => key !== 'vitest');
-  }
-  if (peerDependencyRules.allowedVersions) {
-    delete peerDependencyRules.allowedVersions.vitest;
+  if (!vitePeerDependencyRuleRequired()) {
+    keys.push('vite');
   }
+  return keys;
 }
 
-// Remove every `vite` entry from pnpm peerDependencyRules, in place. Once the
-// core version encodes the bundled Vite (see `vitePeerDependencyRuleRequired`),
-// the rule only hides real incompatibilities. Migration cannot tell whether an
-// earlier migration or the user wrote the entry; its only purpose was to accept
-// the `1.x` alias, so both go.
-export function removeVitePeerDependencyRule(peerDependencyRules: {
-  allowAny?: string[];
-  allowedVersions?: Record;
-}): void {
-  if (Array.isArray(peerDependencyRules.allowAny)) {
-    peerDependencyRules.allowAny = peerDependencyRules.allowAny.filter((key) => key !== 'vite');
-  }
-  if (peerDependencyRules.allowedVersions) {
-    delete peerDependencyRules.allowedVersions.vite;
-  }
+// Whether pnpm peerDependencyRules hold an `allowAny` or `allowedVersions` entry
+// for one of `keys`. Works on both the package.json `pnpm.peerDependencyRules`
+// JSON shape and the same shape read back from pnpm-workspace.yaml.
+export function peerDependencyRulesHaveKey(
+  rules: { allowAny?: unknown; allowedVersions?: unknown } | null | undefined,
+  keys: readonly string[],
+): boolean {
+  return keys.some(
+    (key) =>
+      (Array.isArray(rules?.allowAny) && rules.allowAny.includes(key)) ||
+      (isPlainRecord(rules?.allowedVersions) && rules.allowedVersions[key] !== undefined),
+  );
 }
 
 // The pnpm peerDependencyRules keys vite-plus manages: the managed override
@@ -590,33 +587,25 @@ export function mergeManagedPnpmPeerDependencyRules(
   rules: PnpmPeerDependencyRules | undefined,
   usesVitest: boolean,
 ): PnpmPeerDependencyRules | undefined {
-  const seed: PnpmPeerDependencyRules = { ...rules };
-  if (Array.isArray(seed.allowAny)) {
-    seed.allowAny = [...seed.allowAny];
-  }
-  if (seed.allowedVersions) {
-    seed.allowedVersions = { ...seed.allowedVersions };
-  }
-  // Common case: drop a lingering managed `vitest` peer rule.
-  if (!usesVitest) {
-    removeVitestPeerDependencyRule(seed);
-  }
-  if (!vitePeerDependencyRuleRequired()) {
-    removeVitePeerDependencyRule(seed);
-  }
+  const stale = new Set(stalePeerDependencyRuleKeys(usesVitest));
   const keys = managedPeerDependencyRuleKeys(usesVitest);
-  const next: PnpmPeerDependencyRules = {
-    ...seed,
-    allowAny: [...new Set([...(seed.allowAny ?? []), ...keys])],
-    allowedVersions: {
-      ...seed.allowedVersions,
-      ...Object.fromEntries(keys.map((key) => [key, '*'])),
-    },
+  const allowAny = [
+    ...new Set([
+      ...(Array.isArray(rules?.allowAny) ? rules.allowAny : []).filter((key) => !stale.has(key)),
+      ...keys,
+    ]),
+  ];
+  const allowedVersions = {
+    ...Object.fromEntries(
+      Object.entries(rules?.allowedVersions ?? {}).filter(([key]) => !stale.has(key)),
+    ),
+    ...Object.fromEntries(keys.map((key) => [key, '*'])),
   };
-  if (next.allowAny?.length === 0) {
+  const next: PnpmPeerDependencyRules = { ...rules, allowAny, allowedVersions };
+  if (allowAny.length === 0) {
     delete next.allowAny;
   }
-  if (next.allowedVersions && Object.keys(next.allowedVersions).length === 0) {
+  if (Object.keys(allowedVersions).length === 0) {
     delete next.allowedVersions;
   }
   return Object.keys(next).length > 0 ? next : undefined;
diff --git a/packages/cli/src/resolve-core.ts b/packages/cli/src/resolve-core.ts
index 61b511851f..92d20fcbed 100644
--- a/packages/cli/src/resolve-core.ts
+++ b/packages/cli/src/resolve-core.ts
@@ -2,53 +2,51 @@ import { readFileSync } from 'node:fs';
 import { createRequire } from 'node:module';
 import { join } from 'node:path';
 
+import { VITE_PLUS_CORE_ALIAS_PREFIX, VITE_PLUS_CORE_NAME } from './utils/constants.ts';
 import { readNearestPackageJson } from './utils/package.ts';
 
-const CORE_PACKAGE_NAME = '@voidzero-dev/vite-plus-core';
-const CORE_ALIAS_PREFIX = `npm:${CORE_PACKAGE_NAME}@`;
-
-function checkCoreVersion(packageJsonPath: string, expectedVersion: string | undefined): void {
+/**
+ * Check that a resolved `vite` is the core package, at `expectedVersion` when
+ * one is given. Returns the core version.
+ */
+function checkCoreVersion(
+  packageJsonPath: string,
+  expectedVersion: string | undefined,
+): string | undefined {
   const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
     name?: string;
     version?: string;
   };
   if (
-    pkg.name !== CORE_PACKAGE_NAME ||
+    pkg.name !== VITE_PLUS_CORE_NAME ||
     (expectedVersion !== undefined && pkg.version !== expectedVersion)
   ) {
     const expected =
-      expectedVersion === undefined ? CORE_PACKAGE_NAME : `${CORE_PACKAGE_NAME}@${expectedVersion}`;
+      expectedVersion === undefined
+        ? VITE_PLUS_CORE_NAME
+        : `${VITE_PLUS_CORE_NAME}@${expectedVersion}`;
     throw new Error(
       `Expected ${expected}, but found ${pkg.name}@${pkg.version} at ${packageJsonPath}. ` +
         'Run `vp migrate` to align the Vite alias, then run `vp install`.',
     );
   }
+  return pkg.version;
 }
 
 /**
  * Read the core version that the selected CLI pins through its `vite` alias.
  * The core version encodes the bundled Vite version, so it differs from the
  * CLI version (rfcs/core-version-alignment.md). Packing turns the repository's
- * `workspace:` spec into the exact alias. A spec without an exact version
- * falls back to the version of the core that the CLI resolves, so a stale
- * project core still fails in the repository layout.
+ * `workspace:` spec into the exact alias. Any other spec pins no version.
  */
-function readExpectedCoreVersion(
-  cliPackageJsonPath: string,
-  corePackageJsonPath: string,
-): string | undefined {
+function readAliasCoreVersion(cliPackageJsonPath: string): string | undefined {
   const { dependencies } = JSON.parse(readFileSync(cliPackageJsonPath, 'utf8')) as {
     dependencies?: Record;
   };
   const spec = dependencies?.vite;
-  if (spec?.startsWith(CORE_ALIAS_PREFIX)) {
-    return spec.slice(CORE_ALIAS_PREFIX.length);
-  }
-  const core = JSON.parse(readFileSync(corePackageJsonPath, 'utf8')) as {
-    name?: string;
-    version?: string;
-  };
-  return core.name === CORE_PACKAGE_NAME ? core.version : undefined;
+  return spec?.startsWith(VITE_PLUS_CORE_ALIAS_PREFIX)
+    ? spec.slice(VITE_PLUS_CORE_ALIAS_PREFIX.length)
+    : undefined;
 }
 
 function checkProjectCoreVersion(
@@ -98,12 +96,13 @@ export function resolveCore(
   }
   // Read the selected CLI's installed manifest. A static JSON import is inlined
   // during the build, before CI and preview packers can stamp a new version.
-  const expectedVersion = readExpectedCoreVersion(
-    cliRequire.resolve('vite-plus/package.json'),
+  // The project's core must match the core that the CLI resolves, so a stale
+  // project core also fails when the CLI alias pins no version.
+  const coreVersion = checkCoreVersion(
     corePackageJsonPath,
+    readAliasCoreVersion(cliRequire.resolve('vite-plus/package.json')),
   );
-  checkCoreVersion(corePackageJsonPath, expectedVersion);
-  checkProjectCoreVersion(cwd, corePackageJsonPath, expectedVersion);
+  checkProjectCoreVersion(cwd, corePackageJsonPath, coreVersion);
 
   return cliRequire.resolve(`vite${subpath}`);
 }
diff --git a/packages/cli/src/utils/__tests__/constants.spec.ts b/packages/cli/src/utils/__tests__/constants.spec.ts
index 5570a483a0..2c4e4b4b1d 100644
--- a/packages/cli/src/utils/__tests__/constants.spec.ts
+++ b/packages/cli/src/utils/__tests__/constants.spec.ts
@@ -25,32 +25,21 @@ describe('Vite+ dependency versions', () => {
     );
   });
 
-  it('keeps the core version for a release VP_VERSION', async () => {
-    vi.stubEnv('VP_VERSION', '1.0.1');
-    vi.stubEnv('VP_CORE_VERSION', '');
-    vi.stubEnv('VP_OVERRIDE_PACKAGES', '');
-    vi.resetModules();
-
-    const { VITE_PLUS_CORE_VERSION } = await import('../constants.js');
-
-    expect(VITE_PLUS_CORE_VERSION).toBe(corePkg.version);
-  });
-
-  it.each(['0.0.0', '0.0.0-commit.0c515e3'])(
-    'uses a preview VP_VERSION %s for the core too',
+  it.each(['1.0.1', '0.0.0', '0.0.0-commit.0c515e3'])(
+    'keeps the core manifest version for VP_VERSION %s',
     async (version) => {
       vi.stubEnv('VP_VERSION', version);
       vi.stubEnv('VP_CORE_VERSION', '');
       vi.stubEnv('VP_OVERRIDE_PACKAGES', '');
       vi.resetModules();
 
-      const { VITE_PLUS_OVERRIDE_PACKAGES } = await import('../constants.js');
+      const { VITE_PLUS_CORE_VERSION } = await import('../constants.js');
 
-      expect(VITE_PLUS_OVERRIDE_PACKAGES.vite).toBe(`npm:@voidzero-dev/vite-plus-core@${version}`);
+      expect(VITE_PLUS_CORE_VERSION).toBe(corePkg.version);
     },
   );
 
-  it('prefers VP_CORE_VERSION over a preview VP_VERSION', async () => {
+  it('uses VP_CORE_VERSION for the core alias', async () => {
     vi.stubEnv('VP_VERSION', '0.0.0');
     vi.stubEnv('VP_CORE_VERSION', '8.3.599');
     vi.stubEnv('VP_OVERRIDE_PACKAGES', '');
diff --git a/packages/cli/src/utils/constants.ts b/packages/cli/src/utils/constants.ts
index cb71dcfbe2..9c0191b224 100644
--- a/packages/cli/src/utils/constants.ts
+++ b/packages/cli/src/utils/constants.ts
@@ -5,13 +5,14 @@ import cliPkg from '../../package.json' with { type: 'json' };
 
 export const VITE_PLUS_NAME = 'vite-plus';
 export const VITE_PLUS_VERSION = process.env.VP_VERSION || cliPkg.version;
+export const VITE_PLUS_CORE_NAME = '@voidzero-dev/vite-plus-core';
+// Projects and the CLI alias `vite` to the core: `vite: npm:@voidzero-dev/vite-plus-core@`.
+export const VITE_PLUS_CORE_ALIAS_PREFIX = `npm:${VITE_PLUS_CORE_NAME}@`;
 
 // The core version encodes the bundled Vite version (rfcs/core-version-alignment.md),
-// so it differs from the vite-plus version. Preview builds publish every package
-// at one `0.0.0-
` version, so a preview VP_VERSION also selects the core.
-export const VITE_PLUS_CORE_VERSION =
-  process.env.VP_CORE_VERSION ||
-  (process.env.VP_VERSION?.startsWith('0.0.0') ? process.env.VP_VERSION : corePkg.version);
+// so it differs from the vite-plus version. Preview builds stamp the core manifest
+// before the build, so `corePkg.version` is already the preview version.
+export const VITE_PLUS_CORE_VERSION = process.env.VP_CORE_VERSION || corePkg.version;
 
 // Mirrors Vite's DEFAULT_CONFIG_FILES order so readers and writers target the same file.
 export const VITE_CONFIG_FILES = [
@@ -35,7 +36,7 @@ export const TSDOWN_MIGRATION_SKILL_URL =
 export const VITE_PLUS_OVERRIDE_PACKAGES: Record = process.env.VP_OVERRIDE_PACKAGES
   ? JSON.parse(process.env.VP_OVERRIDE_PACKAGES)
   : {
-      vite: `npm:@voidzero-dev/vite-plus-core@${VITE_PLUS_CORE_VERSION}`,
+      vite: `${VITE_PLUS_CORE_ALIAS_PREFIX}${VITE_PLUS_CORE_VERSION}`,
       // The runner inlines its assertion and runner state. Its remaining
       // official dependencies use exact versions; community providers and
       // Istanbul internals have their own version ranges.
diff --git a/packages/tools/src/core-version.ts b/packages/tools/src/core-version.ts
index 6b59de36ef..4917b71c2f 100644
--- a/packages/tools/src/core-version.ts
+++ b/packages/tools/src/core-version.ts
@@ -24,8 +24,10 @@ import { readFileSync } from 'node:fs';
 import { parseArgs } from 'node:util';
 
 export const CORE_PACKAGE_NAME = '@voidzero-dev/vite-plus-core';
-export const MAX_RELEASE_COUNTER = 98;
-export const LOCAL_BUILD_COUNTER = 99;
+/** The `vite` alias to the core: `npm:@voidzero-dev/vite-plus-core@`. */
+export const CORE_ALIAS_PREFIX = `npm:${CORE_PACKAGE_NAME}@`;
+const MAX_RELEASE_COUNTER = 98;
+const LOCAL_BUILD_COUNTER = 99;
 
 const PRERELEASE_TAG = 'vp';
 const VERSION_RE =
@@ -60,7 +62,7 @@ function parseVersion(version: string): ParsedVersion {
   };
 }
 
-export function isPrereleaseVersion(version: string): boolean {
+function isPrereleaseVersion(version: string): boolean {
   return parseVersion(version).prerelease.length > 0;
 }
 
@@ -96,7 +98,7 @@ export function decodeCoreVersion(core: string): DecodedCoreVersion {
   };
 }
 
-function tryDecodeCoreVersion(core: string): DecodedCoreVersion | undefined {
+export function tryDecodeCoreVersion(core: string): DecodedCoreVersion | undefined {
   try {
     return decodeCoreVersion(core);
   } catch {
@@ -117,8 +119,9 @@ function nextCounter(values: Iterable): number {
  *
  * @param vite - The bundled Vite version.
  * @param vitePlus - The `vite-plus` version of the release.
- * @param published - Every published core version. Versions that do not decode
- *   (the `0.x` and `1.x` lines) are ignored.
+ * @param published - Every published core version. Versions from before the
+ *   alignment (the `0.x` and `1.x` lines) never match a bundled Vite version,
+ *   so they do not affect the counters.
  */
 export function nextCoreVersion({
   vite,
@@ -165,12 +168,9 @@ export function nextCoreVersion({
     return base;
   }
   const k = nextCounter(
-    published.flatMap((version) => {
-      const match = version.startsWith(`${base}-${PRERELEASE_TAG}.`)
-        ? tryDecodeCoreVersion(version)
-        : undefined;
-      return match?.k === undefined ? [] : [match.k];
-    }),
+    decoded.flatMap((entry) =>
+      entry.vite === vite && entry.n === n && entry.k !== undefined ? [entry.k] : [],
+    ),
   );
   return `${base}-${PRERELEASE_TAG}.${k}`;
 }

From 759dc0619e22825ae83d39bc183b70e47e5c287c Mon Sep 17 00:00:00 2001
From: "MK (fengmk2)" 
Date: Tue, 10 Nov 2026 10:09:24 +0800
Subject: [PATCH 07/10] fix(release): reject a core version that another
 release published

- Record the vite-plus version in the core manifest (`vitePlusVersion`).
  `core-version.ts prepare` writes it with the selected core version, and
  `check --release` fails unless the published core version is absent or
  belongs to the same vite-plus release. This covers two release PRs that
  select the same counter and a manual vite-plus bump.
- Start the counter at `.vp.0` when the bundled Vite is a prerelease,
  and check the prerelease pairing only for a release.
- Keep a user `allowedVersions.vite` range during migration. Migration only
  removes the managed `'*'` value and `allowAny: [vite]`.
- Add a strict pnpm peer check case: a Vite 8 plugin installs through the
  core alias, and a Vite 7 plugin still fails.

Co-Authored-By: Claude Opus 5.5 
---
 .../scripts/__tests__/upgrade-deps.spec.ts    |   8 ++
 .github/scripts/upgrade-deps.ts               |   4 +-
 .github/workflows/prepare_release.yml         |  11 +-
 .../check-peers.mjs                           |  27 ++++
 .../core_peer_dependency_checks/package.json  |  11 ++
 .../plugins/current/package.json              |   7 ++
 .../plugins/legacy/package.json               |   7 ++
 .../snapshots.toml                            |  10 ++
 ...core_peer_dependency_checks_pnpm_strict.md |  18 +++
 docs/guide/migrate-rules.md                   |   2 +-
 .../vite-peer-dependency-rule.spec.ts         |  37 ++++++
 .../cli/src/migration/migrator/catalog.ts     |  13 +-
 .../migration/migrator/vite-plus-bootstrap.ts |   6 +-
 .../migration/migrator/vitest-ecosystem.ts    |  39 ++++--
 packages/core/BUNDLING.md                     |   3 +-
 .../tools/src/__tests__/core-version.spec.ts  |  74 +++++++++--
 packages/tools/src/core-version.ts            | 118 ++++++++++++++----
 rfcs/core-version-alignment.md                |  62 ++++++---
 18 files changed, 379 insertions(+), 78 deletions(-)
 create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/check-peers.mjs
 create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/package.json
 create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/plugins/current/package.json
 create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/plugins/legacy/package.json
 create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/snapshots.toml
 create mode 100644 crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/snapshots/core_peer_dependency_checks_pnpm_strict.md

diff --git a/.github/scripts/__tests__/upgrade-deps.spec.ts b/.github/scripts/__tests__/upgrade-deps.spec.ts
index d1ad856851..d2fe3246af 100644
--- a/.github/scripts/__tests__/upgrade-deps.spec.ts
+++ b/.github/scripts/__tests__/upgrade-deps.spec.ts
@@ -301,6 +301,14 @@ test('restarts the core version counter when the bundled Vite changes', () => {
   });
 });
 
+test('starts the vp counter when the bundled Vite is a prerelease', () => {
+  const fixture = createSyncFixture();
+  writeJson(fixture.vitePath, { ...readJson(fixture.vitePath), version: '9.0.0-beta.3' });
+  fixture.run();
+  expect(readJson(fixture.corePath).version).toBe('9.0.0-beta.3.vp.0');
+  expect(readJson(fixture.corePath).bundledVersions).toEqual({ vite: '9.0.0-beta.3' });
+});
+
 test('fails without writing core when the Vite source has no version', () => {
   const fixture = createSyncFixture();
   const { version: _, ...vite } = readJson(fixture.vitePath);
diff --git a/.github/scripts/upgrade-deps.ts b/.github/scripts/upgrade-deps.ts
index 573467e315..f2adad0046 100644
--- a/.github/scripts/upgrade-deps.ts
+++ b/.github/scripts/upgrade-deps.ts
@@ -4,7 +4,7 @@ import path from 'node:path';
 
 import {
   CORE_PACKAGE_NAME,
-  encodeCoreVersion,
+  initialCoreVersion,
   tryDecodeCoreVersion,
 } from '../../packages/tools/src/core-version.ts';
 import {
@@ -557,7 +557,7 @@ function syncCoreVersion(pkg: { version?: unknown; bundledVersions?: Record> "$GITHUB_OUTPUT"
+          echo "Updated packages/core/package.json"
           update_json packages/cli/package.json
-          update_json packages/core/package.json "$CORE_VERSION"
           update_json packages/prompts/package.json
           update_json packages/create-vite-plus/package.json
           update_toml packages/cli/binding/Cargo.toml
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/check-peers.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/check-peers.mjs
new file mode 100644
index 0000000000..320cb0010b
--- /dev/null
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/check-peers.mjs
@@ -0,0 +1,27 @@
+import assert from 'node:assert/strict';
+import { spawnSync } from 'node:child_process';
+import { readFileSync, writeFileSync } from 'node:fs';
+import { createRequire } from 'node:module';
+
+// The core version encodes the bundled Vite version, so pnpm checks `vite`
+// peer ranges against the bundled Vite (rfcs/core-version-alignment.md).
+const require = createRequire(import.meta.url);
+assert.equal(require('vite/package.json').name, '@voidzero-dev/vite-plus-core');
+assert.ok(require.resolve('vite-plugin-current/package.json'));
+console.log('pnpm strict peer check accepts a plugin that supports the bundled Vite');
+
+// A plugin that requires an older Vite major must still fail the check.
+const project = JSON.parse(readFileSync('package.json', 'utf8'));
+project.devDependencies['vite-plugin-legacy'] = 'file:./plugins/legacy';
+writeFileSync('package.json', `${JSON.stringify(project, null, 2)}\n`);
+const result = spawnSync('vp', ['install', '--ignore-scripts'], {
+  encoding: 'utf8',
+  timeout: 110_000,
+});
+assert.ifError(result.error);
+const output = result.stdout + result.stderr;
+assert.notEqual(result.status, 0, output);
+assert.match(output, /ERR_PNPM_PEER_DEP_ISSUES/);
+assert.match(output, /vite-plugin-legacy/);
+assert.doesNotMatch(output, /vite-plugin-current/);
+console.log('pnpm strict peer check rejects a plugin that requires Vite 7');
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/package.json
new file mode 100644
index 0000000000..30a092e9fd
--- /dev/null
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/package.json
@@ -0,0 +1,11 @@
+{
+  "name": "core-peer-dependency-checks",
+  "private": true,
+  "type": "module",
+  "packageManager": "pnpm@11.24.0",
+  "devDependencies": {
+    "vite": "npm:@voidzero-dev/vite-plus-core@latest",
+    "vite-plugin-current": "file:./plugins/current",
+    "vite-plus": "latest"
+  }
+}
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/plugins/current/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/plugins/current/package.json
new file mode 100644
index 0000000000..792b751a6c
--- /dev/null
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/plugins/current/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "vite-plugin-current",
+  "version": "1.0.0",
+  "peerDependencies": {
+    "vite": "^8.0.0"
+  }
+}
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/plugins/legacy/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/plugins/legacy/package.json
new file mode 100644
index 0000000000..849e171e3d
--- /dev/null
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/plugins/legacy/package.json
@@ -0,0 +1,7 @@
+{
+  "name": "vite-plugin-legacy",
+  "version": "1.0.0",
+  "peerDependencies": {
+    "vite": "^7.0.0"
+  }
+}
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/snapshots.toml
new file mode 100644
index 0000000000..d99c3de477
--- /dev/null
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/snapshots.toml
@@ -0,0 +1,10 @@
+[[case]]
+name = "core_peer_dependency_checks_pnpm_strict"
+vp = "global"
+local-registry = true
+comment = "The core version encodes the bundled Vite version, so strict pnpm peer checks accept a Vite 8 plugin through the vite alias and still reject a Vite 7 plugin."
+steps = [
+  { argv = ["vpt", "write-file", "pnpm-workspace.yaml", "strictPeerDependencies: true\nminimumReleaseAge: 0\n"], snapshot = false },
+  { argv = ["vp", "install", "--ignore-scripts"], timeout = 120000, snapshot = false },
+  { argv = ["node", "check-peers.mjs"], timeout = 120000 },
+]
diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/snapshots/core_peer_dependency_checks_pnpm_strict.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/snapshots/core_peer_dependency_checks_pnpm_strict.md
new file mode 100644
index 0000000000..ac25240c2e
--- /dev/null
+++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/core_peer_dependency_checks/snapshots/core_peer_dependency_checks_pnpm_strict.md
@@ -0,0 +1,18 @@
+# core_peer_dependency_checks_pnpm_strict
+
+The core version encodes the bundled Vite version, so strict pnpm peer checks accept a Vite 8 plugin through the vite alias and still reject a Vite 7 plugin.
+
+## `vpt write-file pnpm-workspace.yaml 'strictPeerDependencies: true
+minimumReleaseAge: 0
+'`
+
+
+## `vp install --ignore-scripts`
+
+
+## `node check-peers.mjs`
+
+```
+pnpm strict peer check accepts a plugin that supports the bundled Vite
+pnpm strict peer check rejects a plugin that requires Vite 7
+```
diff --git a/docs/guide/migrate-rules.md b/docs/guide/migrate-rules.md
index afdc45005e..e3db354bcf 100644
--- a/docs/guide/migrate-rules.md
+++ b/docs/guide/migrate-rules.md
@@ -382,7 +382,7 @@ Migration converts the Volta `volta.node` pin to `.node-version`, because Vite+
 **Peer rules.** The core version encodes the bundled Vite version: core `8.3.500` bundles Vite `8.3.5` (see the [RFC](https://github.com/voidzero-dev/vite-plus/blob/main/rfcs/core-version-alignment.md)). pnpm checks `vite` peer ranges against the `vite` alias, so a plugin that supports the bundled Vite passes, and a plugin that does not support it fails.
 
 - Migration does not add `vite` to `peerDependencyRules.allowAny` or `peerDependencyRules.allowedVersions`.
-- Migration removes an existing `vite` entry from both fields, also an entry that you wrote. The only purpose of that entry was to accept the `1.x` core alias. It removes a field, or `peerDependencyRules`, when no entry remains. Other entries stay. The migrate summary names the removed rule.
+- Migration removes `vite` from `allowAny` and removes `allowedVersions.vite` when its value is `'*'`, also when you wrote the entry. The only purpose of those entries was to accept the `1.x` core alias. Another `allowedVersions.vite` range, such as `'7 || 8'`, stays. Migration removes a field, or `peerDependencyRules`, when no entry remains. Other entries stay. The migrate summary names the removed rule.
 - For a preview build (`0.0.0-commit.`) or a Vite+ prerelease core (such as `8.4.0-vp.0`), migration keeps writing the `vite` rule. Package managers do not accept these versions for ranges such as `^8.0.0`.
 - After migration, a plugin whose `vite` peer range excludes the bundled Vite (for example `^7.0.0`) produces a peer warning. With `strictPeerDependencies`, the install fails. Update the plugin, or add a rule scoped to that plugin, such as `allowedVersions: { 'vite-plugin-foo>vite': '*' }`. Migration removes only the plain `vite` key.
 
diff --git a/packages/cli/src/migration/__tests__/vite-peer-dependency-rule.spec.ts b/packages/cli/src/migration/__tests__/vite-peer-dependency-rule.spec.ts
index cf35000c8c..a840367953 100644
--- a/packages/cli/src/migration/__tests__/vite-peer-dependency-rule.spec.ts
+++ b/packages/cli/src/migration/__tests__/vite-peer-dependency-rule.spec.ts
@@ -182,6 +182,43 @@ describe('pnpm vite peer rule', () => {
       expect(report.vitePeerDependencyRuleRemoved).toBe(false);
     });
 
+    it('keeps a user vite range in pnpm-workspace.yaml', () => {
+      writeVitePlusProject(tmpDir, [
+        'peerDependencyRules:',
+        '  allowAny:',
+        '    - vite',
+        '  allowedVersions:',
+        "    vite: '7 || 8'",
+      ]);
+      const report = createMigrationReport();
+
+      ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir), report);
+
+      expect(readWorkspaceRules(tmpDir)).toEqual({ allowedVersions: { vite: '7 || 8' } });
+      expect(report.vitePeerDependencyRuleRemoved).toBe(true);
+      expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(false);
+    });
+
+    it('keeps a user vite range in package.json before pnpm 10.6.2', () => {
+      fs.writeFileSync(
+        path.join(tmpDir, 'package.json'),
+        JSON.stringify({
+          name: 'test',
+          devDependencies: { 'vite-plus': 'latest' },
+          pnpm: {
+            overrides: { react: '18.3.1' },
+            peerDependencyRules: { allowedVersions: { vite: '^8.0.0' } },
+          },
+        }),
+      );
+      const report = createMigrationReport();
+
+      ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, '10.6.1'), report);
+
+      expect(readPackageJsonRules(tmpDir)).toEqual({ allowedVersions: { vite: '^8.0.0' } });
+      expect(report.vitePeerDependencyRuleRemoved).toBe(false);
+    });
+
     it('removes the vite rule from package.json before pnpm 10.6.2', () => {
       fs.writeFileSync(
         path.join(tmpDir, 'package.json'),
diff --git a/packages/cli/src/migration/migrator/catalog.ts b/packages/cli/src/migration/migrator/catalog.ts
index 8401e9e0b5..a94e959657 100644
--- a/packages/cli/src/migration/migrator/catalog.ts
+++ b/packages/cli/src/migration/migrator/catalog.ts
@@ -25,7 +25,8 @@ import {
   managedOverridePackages,
   managedPeerDependencyRuleKeys,
   mergeManagedPnpmPeerDependencyRules,
-  peerDependencyRulesHaveKey,
+  isStaleAllowedVersion,
+  peerDependencyRulesHaveStaleEntry,
   pruneLegacyWrapperAliases,
   removeManagedVitestEntry,
   removeYamlMapVitestEntry,
@@ -398,7 +399,9 @@ export function rewritePnpmWorkspaceYaml(
       allowedVersions = new YAMLMap, Scalar>();
     }
     for (const key of stalePeerRuleKeys) {
-      allowedVersions.delete(key);
+      if (isStaleAllowedVersion(key, allowedVersions.get(key), stalePeerRuleKeys)) {
+        allowedVersions.delete(key);
+      }
     }
     for (const key of peerRuleKeys) {
       // - vite: '*'
@@ -1382,13 +1385,15 @@ export function readPnpmWorkspacePeerDependencyRules(
 // Whether the root pnpm config (the package.json `pnpm` field or
 // pnpm-workspace.yaml) holds a `vite` peer rule.
 export function hasVitePeerDependencyRule(projectPath: string): boolean {
-  if (peerDependencyRulesHaveKey(readPnpmWorkspacePeerDependencyRules(projectPath), ['vite'])) {
+  if (
+    peerDependencyRulesHaveStaleEntry(readPnpmWorkspacePeerDependencyRules(projectPath), ['vite'])
+  ) {
     return true;
   }
   const pkg = readPackageJsonIfExists(path.join(projectPath, 'package.json')) as
     | { pnpm?: PnpmPackageJsonSettings }
     | undefined;
-  return peerDependencyRulesHaveKey(pkg?.pnpm?.peerDependencyRules, ['vite']);
+  return peerDependencyRulesHaveStaleEntry(pkg?.pnpm?.peerDependencyRules, ['vite']);
 }
 
 // Capture the `vite` peer rule before a migration step. Call the returned
diff --git a/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts b/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts
index 51f07e7bb4..807d599270 100644
--- a/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts
+++ b/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts
@@ -29,7 +29,7 @@ import {
   managedOverridePackages,
   managedPeerDependencyRuleKeys,
   mergeManagedPnpmPeerDependencyRules,
-  peerDependencyRulesHaveKey,
+  peerDependencyRulesHaveStaleEntry,
   migratePnpmSettingsToWorkspaceYaml,
   normalizeVitestPeerCatalogSpec,
   pnpmPackageJsonSettingsPending,
@@ -237,7 +237,9 @@ function pnpmPeerDependencyRulesSatisfyVitePlus(
   usesVitest: boolean,
 ): boolean {
   // A lingering managed `vitest` rule or a stale `vite` rule is NOT satisfied.
-  if (peerDependencyRulesHaveKey(peerDependencyRules, stalePeerDependencyRuleKeys(usesVitest))) {
+  if (
+    peerDependencyRulesHaveStaleEntry(peerDependencyRules, stalePeerDependencyRuleKeys(usesVitest))
+  ) {
     return false;
   }
   const allowAny = new Set(peerDependencyRules?.allowAny ?? []);
diff --git a/packages/cli/src/migration/migrator/vitest-ecosystem.ts b/packages/cli/src/migration/migrator/vitest-ecosystem.ts
index 35824336a7..70cd8df17c 100644
--- a/packages/cli/src/migration/migrator/vitest-ecosystem.ts
+++ b/packages/cli/src/migration/migrator/vitest-ecosystem.ts
@@ -559,17 +559,30 @@ export function stalePeerDependencyRuleKeys(usesVitest: boolean): string[] {
   return keys;
 }
 
-// Whether pnpm peerDependencyRules hold an `allowAny` or `allowedVersions` entry
-// for one of `keys`. Works on both the package.json `pnpm.peerDependencyRules`
-// JSON shape and the same shape read back from pnpm-workspace.yaml.
-export function peerDependencyRulesHaveKey(
+// Whether an `allowedVersions` entry is stale. Migration only ever wrote `'*'`
+// for `vite`, so another `vite` range is the user's own constraint and stays.
+export function isStaleAllowedVersion(
+  key: string,
+  value: unknown,
+  staleKeys: readonly string[],
+): boolean {
+  return staleKeys.includes(key) && (key !== 'vite' || value === '*');
+}
+
+// Whether pnpm peerDependencyRules hold a stale `allowAny` or `allowedVersions`
+// entry for one of `keys`. Works on both the package.json
+// `pnpm.peerDependencyRules` JSON shape and the same shape read back from
+// pnpm-workspace.yaml.
+export function peerDependencyRulesHaveStaleEntry(
   rules: { allowAny?: unknown; allowedVersions?: unknown } | null | undefined,
   keys: readonly string[],
 ): boolean {
-  return keys.some(
-    (key) =>
-      (Array.isArray(rules?.allowAny) && rules.allowAny.includes(key)) ||
-      (isPlainRecord(rules?.allowedVersions) && rules.allowedVersions[key] !== undefined),
+  return (
+    (Array.isArray(rules?.allowAny) && rules.allowAny.some((key) => keys.includes(key))) ||
+    (isPlainRecord(rules?.allowedVersions) &&
+      Object.entries(rules.allowedVersions).some(([key, value]) =>
+        isStaleAllowedVersion(key, value, keys),
+      ))
   );
 }
 
@@ -587,17 +600,21 @@ export function mergeManagedPnpmPeerDependencyRules(
   rules: PnpmPeerDependencyRules | undefined,
   usesVitest: boolean,
 ): PnpmPeerDependencyRules | undefined {
-  const stale = new Set(stalePeerDependencyRuleKeys(usesVitest));
+  const stale = stalePeerDependencyRuleKeys(usesVitest);
   const keys = managedPeerDependencyRuleKeys(usesVitest);
   const allowAny = [
     ...new Set([
-      ...(Array.isArray(rules?.allowAny) ? rules.allowAny : []).filter((key) => !stale.has(key)),
+      ...(Array.isArray(rules?.allowAny) ? rules.allowAny : []).filter(
+        (key) => !stale.includes(key),
+      ),
       ...keys,
     ]),
   ];
   const allowedVersions = {
     ...Object.fromEntries(
-      Object.entries(rules?.allowedVersions ?? {}).filter(([key]) => !stale.has(key)),
+      Object.entries(rules?.allowedVersions ?? {}).filter(
+        ([key, value]) => !isStaleAllowedVersion(key, value, stale),
+      ),
     ),
     ...Object.fromEntries(keys.map((key) => [key, '*'])),
   };
diff --git a/packages/core/BUNDLING.md b/packages/core/BUNDLING.md
index 814b075f8c..6ab1d13899 100644
--- a/packages/core/BUNDLING.md
+++ b/packages/core/BUNDLING.md
@@ -420,6 +420,7 @@ The core version encodes `bundledVersions.vite`, so it differs from the `vite-pl
 
 `n` counts the stable releases on one Vite version, from 0 to 98. `k` counts the prereleases on one base. [`packages/tools/src/core-version.ts`](../tools/src/core-version.ts) implements the scheme:
 
-- `prepare_release.yml` runs `core-version.ts next` to select the next unpublished version from the registry.
+- `prepare_release.yml` runs `core-version.ts prepare` to select the next unpublished version from the registry. It writes that version and the `vite-plus` version (`vitePlusVersion`) to `package.json`.
+- `release.yml` runs `core-version.ts check --release`. It fails when the registry already holds the core version for a different `vite-plus` release.
 - `.github/scripts/upgrade-deps.ts` resets `version` to `n = 0` when the bundled Vite changes.
 - `__tests__/core-version.spec.ts` checks that the committed `version` encodes `bundledVersions.vite`.
diff --git a/packages/tools/src/__tests__/core-version.spec.ts b/packages/tools/src/__tests__/core-version.spec.ts
index 961aa5c73f..71a9ce9282 100644
--- a/packages/tools/src/__tests__/core-version.spec.ts
+++ b/packages/tools/src/__tests__/core-version.spec.ts
@@ -2,8 +2,10 @@ import { describe, expect, test } from 'vitest';
 
 import {
   assertCoreVersionMatchesVite,
+  assertCoreVersionUnclaimed,
   decodeCoreVersion,
   encodeCoreVersion,
+  initialCoreVersion,
   nextCoreVersion,
 } from '../core-version.ts';
 
@@ -104,8 +106,15 @@ describe('nextCoreVersion', () => {
   });
 });
 
-function manifest(version: string, vite = '8.3.5') {
-  return { version, bundledVersions: { vite } };
+describe('initialCoreVersion', () => {
+  test('starts the counter for a stable and a prerelease Vite', () => {
+    expect(initialCoreVersion('8.4.2')).toBe('8.4.200');
+    expect(initialCoreVersion('9.0.0-beta.3')).toBe('9.0.0-beta.3.vp.0');
+  });
+});
+
+function manifest(version: string, vite = '8.3.5', vitePlusVersion?: string) {
+  return { version, bundledVersions: { vite }, vitePlusVersion };
 }
 
 describe('assertCoreVersionMatchesVite', () => {
@@ -129,21 +138,64 @@ describe('assertCoreVersionMatchesVite', () => {
     );
   });
 
-  test('rejects mixed prerelease and stable versions', () => {
-    expect(() => assertCoreVersionMatchesVite(manifest('8.3.500'), '1.5.0-rc.0')).toThrow(
-      'both be prereleases',
-    );
-    expect(() => assertCoreVersionMatchesVite(manifest('8.3.500-vp.0'), '1.5.0')).toThrow(
-      'both be prereleases',
-    );
+  test('allows a Vite prerelease with a stable vite-plus between releases', () => {
+    expect(() =>
+      assertCoreVersionMatchesVite(manifest('9.0.0-beta.3.vp.0', '9.0.0-beta.3'), '1.9.0'),
+    ).not.toThrow();
+  });
+
+  test('rejects mixed prerelease and stable versions for a release', () => {
+    expect(() =>
+      assertCoreVersionMatchesVite(manifest('8.3.500', '8.3.5', '1.5.0-rc.0'), '1.5.0-rc.0', {
+        release: true,
+      }),
+    ).toThrow('both be prereleases');
+    expect(() =>
+      assertCoreVersionMatchesVite(manifest('8.3.500-vp.0', '8.3.5', '1.5.0'), '1.5.0', {
+        release: true,
+      }),
+    ).toThrow('both be prereleases');
   });
 
   test('rejects the local build counter for a release', () => {
     expect(() =>
-      assertCoreVersionMatchesVite(manifest('8.3.599'), '1.5.0', { release: true }),
+      assertCoreVersionMatchesVite(manifest('8.3.599', '8.3.5', '1.5.0'), '1.5.0', {
+        release: true,
+      }),
     ).toThrow('reserved for local builds');
     expect(() =>
-      assertCoreVersionMatchesVite(manifest('8.3.598'), '1.5.0', { release: true }),
+      assertCoreVersionMatchesVite(manifest('8.3.598', '8.3.5', '1.5.0'), '1.5.0', {
+        release: true,
+      }),
     ).not.toThrow();
   });
+
+  test('requires the manifest to record the released vite-plus version', () => {
+    expect(() =>
+      assertCoreVersionMatchesVite(manifest('8.3.500'), '1.5.0', { release: true }),
+    ).toThrow('records vite-plus (none)');
+    expect(() =>
+      assertCoreVersionMatchesVite(manifest('8.3.500', '8.3.5', '1.4.0'), '1.5.0', {
+        release: true,
+      }),
+    ).toThrow('records vite-plus 1.4.0, but the release is vite-plus 1.5.0');
+  });
+});
+
+describe('assertCoreVersionUnclaimed', () => {
+  test('accepts an unpublished core version and a retry of the same release', () => {
+    expect(() => assertCoreVersionUnclaimed(undefined, '8.3.501', '1.5.1')).not.toThrow();
+    expect(() =>
+      assertCoreVersionUnclaimed({ vitePlusVersion: '1.5.1' }, '8.3.501', '1.5.1'),
+    ).not.toThrow();
+  });
+
+  test('rejects a core version that another release published', () => {
+    expect(() =>
+      assertCoreVersionUnclaimed({ vitePlusVersion: '1.5.1' }, '8.3.501', '1.6.0'),
+    ).toThrow('already published for vite-plus 1.5.1');
+    expect(() => assertCoreVersionUnclaimed({}, '8.3.500', '1.6.0')).toThrow(
+      'already published for vite-plus (unknown)',
+    );
+  });
 });
diff --git a/packages/tools/src/core-version.ts b/packages/tools/src/core-version.ts
index 4917b71c2f..93635e5484 100644
--- a/packages/tools/src/core-version.ts
+++ b/packages/tools/src/core-version.ts
@@ -13,14 +13,18 @@
  * is reserved for local and CI builds, so they never collide with a published
  * version. `k` counts the Vite+ prereleases on one base.
  *
+ * A release records its `vite-plus` version in the core manifest
+ * (`vitePlusVersion`). The release check uses it to tell a retry of the same
+ * release from a core version that another release already published.
+ *
  * Usage:
- *   node packages/tools/src/core-version.ts next --vite-plus 
+ *   node packages/tools/src/core-version.ts prepare --vite-plus 
  *   node packages/tools/src/core-version.ts local
  *   node packages/tools/src/core-version.ts check [--release]
  */
 
 import { execFileSync } from 'node:child_process';
-import { readFileSync } from 'node:fs';
+import { readFileSync, writeFileSync } from 'node:fs';
 import { parseArgs } from 'node:util';
 
 export const CORE_PACKAGE_NAME = '@voidzero-dev/vite-plus-core';
@@ -78,6 +82,14 @@ export function encodeCoreVersion(vite: string, n: number): string {
   return `${major}.${minor}.${patch * 100 + n}`;
 }
 
+/**
+ * Returns the first core version for a bundled Vite version: counter 0 on a
+ * stable Vite, `.vp.0` on a Vite prerelease.
+ */
+export function initialCoreVersion(vite: string): string {
+  return isPrereleaseVersion(vite) ? `${vite}.${PRERELEASE_TAG}.0` : encodeCoreVersion(vite, 0);
+}
+
 /** Decodes a core version into the bundled Vite version and its counters. */
 export function decodeCoreVersion(core: string): DecodedCoreVersion {
   const { major, minor, patch, prerelease } = parseVersion(core);
@@ -175,13 +187,23 @@ export function nextCoreVersion({
   return `${base}-${PRERELEASE_TAG}.${k}`;
 }
 
+interface CoreManifest {
+  version: string;
+  bundledVersions?: { vite?: string };
+  vitePlusVersion?: string;
+}
+
 /**
- * Asserts that a core manifest encodes its bundled Vite version, and that the
- * core version is a prerelease exactly when the `vite-plus` version is one.
- * With `release`, also rejects the local build counter.
+ * Asserts that a core manifest encodes its bundled Vite version. With
+ * `release`, also asserts that:
+ * - the core version is a prerelease exactly when the `vite-plus` version is
+ *   one. Between releases, main can bundle a Vite prerelease with a stable
+ *   `vite-plus`.
+ * - the core version does not use the local build counter.
+ * - the manifest records this `vite-plus` version. `prepare` writes it.
  */
 export function assertCoreVersionMatchesVite(
-  pkg: { version: string; bundledVersions?: { vite?: string } },
+  pkg: CoreManifest,
   vitePlus: string,
   { release = false }: { release?: boolean } = {},
 ): void {
@@ -193,27 +215,53 @@ export function assertCoreVersionMatchesVite(
   if (decoded.vite !== vite) {
     throw new Error(
       `${CORE_PACKAGE_NAME}@${pkg.version} encodes Vite ${decoded.vite}, but it bundles Vite ${vite}. ` +
-        `Set its version to ${isPrereleaseVersion(vite) ? `${vite}.${PRERELEASE_TAG}.0` : encodeCoreVersion(vite, 0)}.`,
+        `Set its version to ${initialCoreVersion(vite)}.`,
     );
   }
+  if (!release) {
+    return;
+  }
   const isCorePrerelease = decoded.k !== undefined;
   if (isCorePrerelease !== isPrereleaseVersion(vitePlus)) {
     throw new Error(
       `${CORE_PACKAGE_NAME}@${pkg.version} and vite-plus@${vitePlus} must both be prereleases or both be stable`,
     );
   }
-  if (release && decoded.n === LOCAL_BUILD_COUNTER) {
+  if (decoded.n === LOCAL_BUILD_COUNTER) {
     throw new Error(
       `${CORE_PACKAGE_NAME}@${pkg.version} uses the counter ${LOCAL_BUILD_COUNTER}, which is reserved for local builds`,
     );
   }
+  if (pkg.vitePlusVersion !== vitePlus) {
+    throw new Error(
+      `${CORE_PACKAGE_NAME}@${pkg.version} records vite-plus ${pkg.vitePlusVersion ?? '(none)'}, but the release is vite-plus ${vitePlus}. ` +
+        'Run the Prepare Release workflow to select the core version.',
+    );
+  }
 }
 
-function readManifest(relativePath: string): {
-  version: string;
-  bundledVersions?: { vite?: string };
-} {
-  return JSON.parse(readFileSync(new URL(relativePath, import.meta.url), 'utf8'));
+/**
+ * Asserts that the registry does not already hold the core version for a
+ * different release. `published` is the published manifest of that version, or
+ * `undefined` when it is not published. A retry of the same release passes.
+ */
+export function assertCoreVersionUnclaimed(
+  published: { vitePlusVersion?: string } | undefined,
+  core: string,
+  vitePlus: string,
+): void {
+  if (published && published.vitePlusVersion !== vitePlus) {
+    throw new Error(
+      `${CORE_PACKAGE_NAME}@${core} is already published for vite-plus ${published.vitePlusVersion ?? '(unknown)'}. ` +
+        'Run the Prepare Release workflow again to select an unpublished core version.',
+    );
+  }
+}
+
+const CORE_MANIFEST_URL = new URL('../../core/package.json', import.meta.url);
+
+function readManifest(url: URL): CoreManifest {
+  return JSON.parse(readFileSync(url, 'utf8'));
 }
 
 function readPublishedCoreVersions(): string[] {
@@ -230,7 +278,25 @@ function readPublishedCoreVersions(): string[] {
   return versions;
 }
 
-function main(argv: string[]): void {
+async function readPublishedCoreManifest(
+  core: string,
+): Promise<{ vitePlusVersion?: string } | undefined> {
+  const registry = (process.env.PUBLISH_REGISTRY ?? 'https://registry.npmjs.org').replace(
+    /\/+$/,
+    '',
+  );
+  const url = `${registry}/${CORE_PACKAGE_NAME.replace('/', '%2f')}/${core}`;
+  const response = await fetch(url, { signal: AbortSignal.timeout(60_000) });
+  if (response.status === 404) {
+    return undefined;
+  }
+  if (!response.ok) {
+    throw new Error(`Registry returned HTTP ${response.status} for ${url}`);
+  }
+  return (await response.json()) as { vitePlusVersion?: string };
+}
+
+async function main(argv: string[]): Promise {
   const { positionals, values } = parseArgs({
     args: argv,
     allowPositionals: true,
@@ -239,38 +305,48 @@ function main(argv: string[]): void {
       release: { type: 'boolean', default: false },
     },
   });
-  const core = readManifest('../../core/package.json');
+  const core = readManifest(CORE_MANIFEST_URL);
   const vite = core.bundledVersions?.vite;
   if (!vite) {
     throw new Error(`packages/core/package.json has no bundledVersions.vite`);
   }
 
   switch (positionals[0]) {
-    case 'next': {
+    case 'prepare': {
       const vitePlus = values['vite-plus'];
       if (!vitePlus) {
-        throw new Error('next requires --vite-plus ');
+        throw new Error('prepare requires --vite-plus ');
       }
-      console.log(nextCoreVersion({ vite, vitePlus, published: readPublishedCoreVersions() }));
+      core.version = nextCoreVersion({ vite, vitePlus, published: readPublishedCoreVersions() });
+      core.vitePlusVersion = vitePlus;
+      writeFileSync(CORE_MANIFEST_URL, `${JSON.stringify(core, null, 2)}\n`);
+      console.log(core.version);
       break;
     }
     case 'local':
       console.log(encodeCoreVersion(vite, LOCAL_BUILD_COUNTER));
       break;
     case 'check': {
-      const cli = readManifest('../../cli/package.json');
+      const cli = readManifest(new URL('../../cli/package.json', import.meta.url));
       assertCoreVersionMatchesVite(core, cli.version, { release: values.release });
+      if (values.release) {
+        assertCoreVersionUnclaimed(
+          await readPublishedCoreManifest(core.version),
+          core.version,
+          cli.version,
+        );
+      }
       console.log(`${CORE_PACKAGE_NAME}@${core.version} encodes Vite ${vite}`);
       break;
     }
     default:
-      throw new Error(`Unknown command: ${positionals[0] ?? ''}. Use next, local, or check.`);
+      throw new Error(`Unknown command: ${positionals[0] ?? ''}. Use prepare, local, or check.`);
   }
 }
 
 if (import.meta.main) {
   try {
-    main(process.argv.slice(2));
+    await main(process.argv.slice(2));
   } catch (error) {
     console.error(`[core-version] ${(error as Error).message}`);
     process.exit(1);
diff --git a/rfcs/core-version-alignment.md b/rfcs/core-version-alignment.md
index f50d9883aa..25728d0377 100644
--- a/rfcs/core-version-alignment.md
+++ b/rfcs/core-version-alignment.md
@@ -224,9 +224,11 @@ accepts the warnings for that period. Migration keeps the pnpm peer rule for
 prerelease core versions (see Migration).
 
 A Vite+ stable release bundles a stable Vite. Only a Vite+ prerelease may
-bundle a Vite prerelease. CI rejects a core prerelease on a `vite-plus`
-version without a prerelease identifier, and a stable core on a `vite-plus`
-prerelease.
+bundle a Vite prerelease. The release check rejects a core prerelease on a
+`vite-plus` version without a prerelease identifier, and a stable core on a
+`vite-plus` prerelease. Between releases, main can bundle a Vite prerelease
+with a stable `vite-plus` version (for example core `9.0.0-beta.3.vp.0` with
+`vite-plus` `1.9.0`), so this pairing check runs only for a release.
 
 Since `v1.0.0`, Vite+ published no prereleases. The last ones are
 `v1.0.0-rc.0` and `v1.0.0-rc.1`. The prerelease rule matters mostly for the
@@ -265,18 +267,24 @@ lockfile. It does not read the core version.
 
 ### Where the version is computed and checked
 
-Add a small module, `packages/tools/src/core-version.ts`, with four pure
+Add a small module, `packages/tools/src/core-version.ts`, with pure
 functions and a CLI entry:
 
 - `encodeCoreVersion(vite, n)` returns the stable core version.
+- `initialCoreVersion(vite)` returns the first core version for a bundled
+  Vite: `n = 0` on a stable Vite, `.vp.0` on a Vite prerelease.
 - `decodeCoreVersion(core)` returns `{ vite, n, k }` or an error.
 - `nextCoreVersion({ vite, vitePlus, published })` returns the core version
   for a release. Its inputs are the bundled Vite version, the `vite-plus`
   version, and the published core versions. It applies the counter rules
   below.
-- `assertCoreVersionMatchesVite(pkg, vitePlus)` asserts that `pkg.version`
-  decodes to `pkg.bundledVersions.vite`. It also asserts that the core
-  version is a prerelease only when the `vite-plus` version is a prerelease.
+- `assertCoreVersionMatchesVite(pkg, vitePlus, { release })` asserts that
+  `pkg.version` decodes to `pkg.bundledVersions.vite`. With `release`, it also
+  asserts the prerelease pairing, rejects `n = 99`, and requires
+  `pkg.vitePlusVersion` to equal the released `vite-plus` version.
+- `assertCoreVersionUnclaimed(published, core, vitePlus)` rejects a core
+  version that the registry already holds for a different `vite-plus`
+  version. A retry of the same release passes.
 
 The name `checkCoreVersion` is not used, because
 `packages/cli/src/resolve-core.ts` already defines a function with that name
@@ -298,15 +306,26 @@ The committed `packages/core/package.json` must always encode the committed
 
 1. `.github/scripts/upgrade-deps.ts` already reads
    `packages/core/package.json` to sync `postcss` and `fsevents`. After a
-   bundled Vite change it sets `version` to `encodeCoreVersion(vite, 0)`.
+   bundled Vite change it sets `version` to `initialCoreVersion(vite)`.
    Renovate ignores `vite` (`.github/renovate.json`), so this script is the
    only automated path that changes the bundled Vite.
-2. `prepare_release.yml` computes the release version. It reads the
+2. `prepare_release.yml` runs `core-version.ts prepare`. It reads the
    published core versions
-   (`npm view @voidzero-dev/vite-plus-core versions --json`) and calls
-   `nextCoreVersion` with the input `vite-plus` version. It then writes the
-   result with the existing `update_json` helper and prints both versions in
-   the release PR body.
+   (`npm view @voidzero-dev/vite-plus-core versions --json`), calls
+   `nextCoreVersion` with the input `vite-plus` version, and writes the
+   result and `vitePlusVersion` to the core manifest. The workflow prints
+   both versions in the release PR body.
+
+The counter comes from the registry when the release PR is prepared, so it
+can be stale at publish time. Two release PRs prepared before either
+publishes get the same core version, and a manual `vite-plus` bump skips the
+selection. The publish step skips a version that is already published, so
+the CLI could then pin a core tarball from another commit. The
+`vitePlusVersion` field closes this gap: `release.yml` runs
+`core-version.ts check --release`, which reads the published manifest of the
+core version and fails unless the version is unpublished or was published for
+the same `vite-plus` version. The field also maps a published core back to
+its `vite-plus` release.
 
 A unit test in `packages/core/__tests__/` runs `assertCoreVersionMatchesVite`
 against the committed manifest. It skips the check when the version starts
@@ -418,13 +437,16 @@ Peer rules change only for pnpm:
   `rewritePnpmWorkspaceYaml` in `catalog.ts`, and the writers in
   `orchestrators.ts` and `catalog.ts` stop adding `vite` to `allowAny` and
   `allowedVersions` when the target core version is a stable encoded version
-  (major 8 or higher, no prerelease identifier). They remove an existing
-  `vite` entry from both maps. They delete an empty `peerDependencyRules`
-  object or YAML map. Other entries stay, including `vitest`.
-- The writers cannot tell whether an earlier migration or the user wrote a
-  `vite` entry, so they remove every `vite` entry. The only purpose of that
-  entry is to accept the `1.x` alias. `vp migrate` prints one line that names
-  the removed rule, and `docs/guide/migrate-rules.md` describes the removal.
+  (major 8 or higher, no prerelease identifier). They remove `vite` from
+  `allowAny` and remove `allowedVersions.vite` when its value is `'*'`. They
+  delete an empty `peerDependencyRules` object or YAML map. Other entries
+  stay, including `vitest`.
+- The writers cannot tell whether an earlier migration or the user wrote
+  `allowAny: [vite]` or `allowedVersions.vite: '*'`, so they remove both. The
+  only purpose of those entries is to accept the `1.x` alias. Migration only
+  ever wrote `'*'`, so another `allowedVersions.vite` range is the user's own
+  constraint and stays. `vp migrate` prints one line that names the removed
+  rule, and `docs/guide/migrate-rules.md` describes the removal.
 - When the target core version is not a stable encoded version (a preview
   `0.0.0-commit.` or a Vite+ prerelease such as `8.4.0-vp.0`), the
   writers keep the current behavior and write the `vite` rule.

From 490586194eafe86a2bb5279342ee56ebb99e5e15 Mon Sep 17 00:00:00 2001
From: "MK (fengmk2)" 
Date: Tue, 10 Nov 2026 10:11:18 +0800
Subject: [PATCH 08/10] docs: cover follow-ups from the core version review

- Read the matching core alias with `npm view vite-plus@
  dependencies.vite` when updating manually, and explain how to handle
  dependency bot updates that change only one of the two packages.
- Run the core release check in the release check job too, so a stale or
  duplicate core version fails before the build.
- Note that main can hold an unpublished core version, and link the CLI's
  stable-core rule to the version scheme module.

Co-Authored-By: Claude Opus 5.5 
---
 .github/workflows/release.yml                 | 13 +++++++++++++
 CONTRIBUTING.md                               |  2 ++
 docs/guide/upgrade-project.md                 | 13 ++++++++++++-
 packages/cli/src/migration/migrator/shared.ts |  3 +++
 4 files changed, 30 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d671da3207..29343b0246 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -40,6 +40,19 @@ jobs:
         id: core-version
         run: echo "version=$(node -p "require('./packages/core/package.json').version")" >> "$GITHUB_OUTPUT"
 
+      # Fail before the build and the approval wait when the core version is
+      # stale or another release already published it. The Release job checks
+      # again before it publishes.
+      - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
+        if: steps.version.outputs.changed == 'true'
+        with:
+          node-version-file: .node-version
+          package-manager-cache: false
+
+      - name: Check core version
+        if: steps.version.outputs.changed == 'true'
+        run: node packages/tools/src/core-version.ts check --release
+
   build-rust:
     name: Build bindings and binaries
     needs: check
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7ef0bb5d8b..7261ed3774 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -184,6 +184,8 @@ pnpm local-registry --pack --serve
 
 `pnpm local-registry:ps` lists any registry processes still running; `pnpm local-registry:kill` stops them all and removes their leftover temp caches. See the [tool documentation](packages/tools/README.md#local-npm-registry) for implementation details and test integration.
 
+The core package has its own version, which encodes the bundled Vite version (see [Core Version](packages/core/BUNDLING.md#core-version)). Between releases, `packages/core/package.json` can hold a version that npm does not have yet, for example after a bundled Vite upgrade. A locally built `vp create` or `vp migrate` without the local registry then writes a `vite` alias that npm cannot install. Use the local registry, which serves the packed core at that version.
+
 ### Global CLI (Rust) changes
 
 `pnpm link` only swaps the JS side; the `vp` binary on `PATH` (and the Rust-backed commands it handles directly, such as package-manager commands) is still the installed binary. For changes to the Rust global CLI (`crates/`), follow [the source installation steps](#install-the-vite-global-cli-from-source-code), and combine with `pnpm link` when the change spans both layers.
diff --git a/docs/guide/upgrade-project.md b/docs/guide/upgrade-project.md
index efec5a0d4f..969bf6e6fe 100644
--- a/docs/guide/upgrade-project.md
+++ b/docs/guide/upgrade-project.md
@@ -18,7 +18,18 @@ On a project that is already on Vite+, migrate does a toolchain version upgrade
 
 ## Manually Updating
 
-Update `vite-plus` and the `vite` alias to `@voidzero-dev/vite-plus-core` together. The two packages have different versions: the core version encodes the bundled Vite version (for example, `8.3.500` for Vite `8.3.5`). After you update `vite-plus`, set the alias to the version that `vp toolchain vite-plus-core` shows. Update these entries wherever they are declared in your workspace, including overrides or catalogs, then install dependencies to refresh the lockfile. Also [update the Vitest pin](#updating-the-vitest-pin) to match the bundled version.
+Update `vite-plus` and the `vite` alias to `@voidzero-dev/vite-plus-core` together. The two packages have different versions: the core version encodes the bundled Vite version (for example, `8.3.500` for Vite `8.3.5`). Read the alias that a `vite-plus` version uses before you install it:
+
+```bash
+npm view vite-plus@ dependencies.vite
+# npm:@voidzero-dev/vite-plus-core@
+```
+
+Set the `vite` alias to that value. Update these entries wherever they are declared in your workspace, including overrides or catalogs, then install dependencies to refresh the lockfile. Also [update the Vitest pin](#updating-the-vitest-pin) to match the bundled version. After the install, `vp toolchain vite-plus-core` shows the same core version.
+
+### Dependency Bots
+
+Dependabot and Renovate see `vite-plus` and the `vite` alias as two separate dependencies with different versions. A bot can update one without the other. Vite+ then stops before it runs a Vite command and asks you to run `vp migrate`. Before you merge a bot update, check that the `vite` alias matches the output of `npm view vite-plus@ dependencies.vite`. If it does not match, run `vp migrate` on the bot's branch. Migrate pins the matching alias, the overrides, and the Vitest version.
 
 Without the global CLI, run the `vp` commands on this page through your package manager, for example `pnpm exec vp toolchain vitest`.
 
diff --git a/packages/cli/src/migration/migrator/shared.ts b/packages/cli/src/migration/migrator/shared.ts
index 17ba284ad7..2f64783922 100644
--- a/packages/cli/src/migration/migrator/shared.ts
+++ b/packages/cli/src/migration/migrator/shared.ts
@@ -226,6 +226,9 @@ export const VITEST_IS_MANAGED_OVERRIDE = 'vitest' in VITE_PLUS_OVERRIDE_PACKAGE
  * (`0.0.0-commit.`), a Vite+ prerelease (`8.4.0-vp.0` fails `^8.0.0`), a
  * `1.x` core from before the alignment, a dist-tag, or a `file:` or URL spec
  * whose version is unknown.
+ *
+ * Major 8 is the first aligned core major. `packages/tools/src/core-version.ts`
+ * owns the version scheme; the CLI cannot import it, so keep the two in sync.
  */
 export function vitePeerDependencyRuleRequired(
   viteSpec: string | undefined = VITE_PLUS_OVERRIDE_PACKAGES.vite,

From 7f41a5a785e09daa843762d76f02bf94b63bd694 Mon Sep 17 00:00:00 2001
From: "MK (fengmk2)" 
Date: Tue, 10 Nov 2026 10:32:48 +0800
Subject: [PATCH 09/10] refactor(core): use a patch factor of 1000 for the core
 version

Follow the oxlint-tsgolint convention, which Vite+ bundles: the core patch
is `vitePatch * 1000 + n`, with `n` from 0. Core 8.3.5000 is the first
release on Vite 8.3.5, and local and CI builds use counter 999 (8.3.5999).
The counter now allows 999 stable releases per Vite patch.

The RFC also compares the `.vp.` prerelease form with encoding the
counter in the Vite prerelease number (`9.0.0-beta.3000`). The encoded form
passes prerelease floors such as `^9.0.0-beta.5` by mistake, so the RFC
keeps `.vp.`.

Co-Authored-By: Claude Opus 5.5 
---
 .claude/skills/release-manager/SKILL.md       |   2 +-
 .../scripts/__tests__/homebrew-formula.mjs    |   4 +-
 .../scripts/__tests__/upgrade-deps.spec.ts    |  18 +-
 .github/workflows/reusable-ecosystem-ci.yml   |   2 +-
 .github/workflows/test-vp-create.yml          |   2 +-
 MAINTENANCE.md                                |   2 +-
 README.md                                     |   2 +-
 docs/guide/local-cli.md                       |   2 +-
 docs/guide/migrate-rules.md                   |   2 +-
 docs/guide/upgrade-project.md                 |   2 +-
 packages/cli/README.md                        |   2 +-
 .../cli/src/__tests__/resolve-core.spec.ts    |   8 +-
 .../vite-peer-dependency-rule.spec.ts         |   6 +-
 packages/cli/src/migration/migrator/shared.ts |   2 +-
 .../src/sync-versions/__tests__/plan.spec.ts  |  10 +-
 .../sync-versions/__tests__/protocol.spec.ts  |   6 +-
 .../cli/src/utils/__tests__/constants.spec.ts |   6 +-
 packages/core/BUNDLING.md                     |  16 +-
 packages/core/package.json                    |   2 +-
 .../tools/src/__tests__/core-version.spec.ts  |  58 +++----
 packages/tools/src/core-version.ts            |  20 ++-
 rfcs/core-version-alignment.md                | 159 +++++++++++-------
 22 files changed, 190 insertions(+), 143 deletions(-)

diff --git a/.claude/skills/release-manager/SKILL.md b/.claude/skills/release-manager/SKILL.md
index c9f37f3a25..a70b485a80 100644
--- a/.claude/skills/release-manager/SKILL.md
+++ b/.claude/skills/release-manager/SKILL.md
@@ -48,7 +48,7 @@ gh workflow run prepare_release.yml --repo voidzero-dev/vite-plus -f version=X.Y
 
 The workflow bumps `packages/cli/package.json`, `packages/core/package.json`, `packages/prompts/package.json`, `packages/cli/binding/Cargo.toml`, and `crates/vp_global_cli/Cargo.toml`, refreshes `Cargo.lock`, and opens a PR titled `release: vX.Y.Z` from branch `release/vX.Y.Z`. The PR body ends with `Merging this PR will trigger the release workflow.` and that line must survive every later edit.
 
-`@voidzero-dev/vite-plus-core` does not use `X.Y.Z`. Its version encodes the bundled Vite version (for example, `8.3.500` for Vite `8.3.5`; see [Core Version](../../../packages/core/BUNDLING.md#core-version)). The workflow selects it from the registry and names it in the first line of the PR body. In the steps below, `` is that version. Read it from the release branch with `node -p "require('./packages/core/package.json').version"`.
+`@voidzero-dev/vite-plus-core` does not use `X.Y.Z`. Its version encodes the bundled Vite version (for example, `8.3.5000` for Vite `8.3.5`; see [Core Version](../../../packages/core/BUNDLING.md#core-version)). The workflow selects it from the registry and names it in the first line of the PR body. In the steps below, `` is that version. Read it from the release branch with `node -p "require('./packages/core/package.json').version"`.
 
 ## 2. Sync release versions (required every release)
 
diff --git a/.github/scripts/__tests__/homebrew-formula.mjs b/.github/scripts/__tests__/homebrew-formula.mjs
index b78520b545..784eab93c2 100644
--- a/.github/scripts/__tests__/homebrew-formula.mjs
+++ b/.github/scripts/__tests__/homebrew-formula.mjs
@@ -153,9 +153,9 @@ await test('reads the core version from the published CLI alias', async () => {
   assert.equal(
     await publishedCoreVersion(
       version,
-      manifest({ vite: 'npm:@voidzero-dev/vite-plus-core@8.3.501' }),
+      manifest({ vite: 'npm:@voidzero-dev/vite-plus-core@8.3.5001' }),
     ),
-    '8.3.501',
+    '8.3.5001',
   );
   await assert.rejects(
     publishedCoreVersion(version, manifest({ vite: '^8.0.0' })),
diff --git a/.github/scripts/__tests__/upgrade-deps.spec.ts b/.github/scripts/__tests__/upgrade-deps.spec.ts
index d2fe3246af..588d33186a 100644
--- a/.github/scripts/__tests__/upgrade-deps.spec.ts
+++ b/.github/scripts/__tests__/upgrade-deps.spec.ts
@@ -188,7 +188,7 @@ function createSyncFixture() {
   const metaDir = join(root, 'meta');
   const versionsPath = join(metaDir, 'versions.json');
   writeJson(corePath, {
-    version: '8.3.501',
+    version: '8.3.5001',
     dependencies: { 'yuku-parser': '^0.9.3', postcss: '^8.5.6' },
     optionalDependencies: { fsevents: '~2.3.3' },
     devDependencies: { 'rolldown-plugin-dts': '^0.28.0' },
@@ -243,7 +243,7 @@ test('pins the bundled parser, syncs Vite ranges, and preserves upgrade metadata
   run();
   expect(readJson(corePath)).toEqual({
     // The bundled Vite did not change, so the release counter stays.
-    version: '8.3.501',
+    version: '8.3.5001',
     dependencies: { 'yuku-parser': '0.10.3', postcss: '^8.5.28' },
     optionalDependencies: { fsevents: '~2.4.0' },
     devDependencies: { 'rolldown-plugin-dts': '^0.28.0' },
@@ -282,22 +282,22 @@ test('restarts the core version counter when the bundled Vite changes', () => {
   const { corePath, versionsPath, metaDir, run } = fixture;
   writeJson(fixture.vitePath, { ...readJson(fixture.vitePath), version: '8.4.2' });
   run();
-  expect(readJson(corePath).version).toBe('8.4.200');
+  expect(readJson(corePath).version).toBe('8.4.2000');
   expect(readJson(corePath).bundledVersions).toEqual({ vite: '8.4.2' });
   expect(readJson(versionsPath)['@voidzero-dev/vite-plus-core']).toEqual({
-    old: '8.3.501',
-    new: '8.4.200',
+    old: '8.3.5001',
+    new: '8.4.2000',
   });
   expect(readFileSync(join(metaDir, 'pr-body.md'), 'utf8')).toContain(
-    '| `@voidzero-dev/vite-plus-core` | `8.3.501` | `8.4.200` |',
+    '| `@voidzero-dev/vite-plus-core` | `8.3.5001` | `8.4.2000` |',
   );
 
   // A second pass keeps the recorded old version and the encoded version.
   run();
-  expect(readJson(corePath).version).toBe('8.4.200');
+  expect(readJson(corePath).version).toBe('8.4.2000');
   expect(readJson(versionsPath)['@voidzero-dev/vite-plus-core']).toEqual({
-    old: '8.3.501',
-    new: '8.4.200',
+    old: '8.3.5001',
+    new: '8.4.2000',
   });
 });
 
diff --git a/.github/workflows/reusable-ecosystem-ci.yml b/.github/workflows/reusable-ecosystem-ci.yml
index 23f2bfd7c6..743a956650 100644
--- a/.github/workflows/reusable-ecosystem-ci.yml
+++ b/.github/workflows/reusable-ecosystem-ci.yml
@@ -86,7 +86,7 @@ jobs:
           # (including bun) resolving the standard
           # `vite -> npm:@voidzero-dev/vite-plus-core@` alias. Pin
           # the CLI to 0.0.0 and core to its local version (release counter
-          # 99, for example 8.3.599) so a correctly installed local build is
+          # 999, for example 8.3.5999) so a correctly installed local build is
           # always distinguishable from any published version
           # (verify-install.ts asserts it), even on a release commit that
           # leaves packages/{core,cli} at a published version. The local core
diff --git a/.github/workflows/test-vp-create.yml b/.github/workflows/test-vp-create.yml
index bd47280ebd..8837a1bdd4 100644
--- a/.github/workflows/test-vp-create.yml
+++ b/.github/workflows/test-vp-create.yml
@@ -108,7 +108,7 @@ jobs:
           # (including bun) resolving the standard
           # `vite -> npm:@voidzero-dev/vite-plus-core@` alias. Pin
           # the CLI to 0.0.0 and core to its local version (release counter
-          # 99, for example 8.3.599) so a correctly installed local build is
+          # 999, for example 8.3.5999) so a correctly installed local build is
           # always distinguishable from any published version, even on a
           # release commit that leaves packages/{core,cli} at a published
           # version. The local core version still encodes the bundled Vite,
diff --git a/MAINTENANCE.md b/MAINTENANCE.md
index 6edaf31e23..037ed4986c 100644
--- a/MAINTENANCE.md
+++ b/MAINTENANCE.md
@@ -24,7 +24,7 @@ pnpm add vite-plus@0.0.0-commit.
 
 The [release workflow](.github/workflows/release.yml) publishes platform packages before `@voidzero-dev/vite-plus-core`, then publishes `vite-plus`. It checks that the required package versions and tarballs are available before continuing to dependent steps.
 
-`@voidzero-dev/vite-plus-core` has its own version, which encodes the bundled Vite version (for example, `8.3.500` for Vite `8.3.5`). The [prepare release workflow](.github/workflows/prepare_release.yml) selects it from the registry and prints both versions in the release PR. The platform packages, `vite-plus`, and `create-vite-plus` use the `vite-plus` version. See [Core Version](packages/core/BUNDLING.md#core-version).
+`@voidzero-dev/vite-plus-core` has its own version, which encodes the bundled Vite version (for example, `8.3.5000` for Vite `8.3.5`). The [prepare release workflow](.github/workflows/prepare_release.yml) selects it from the registry and prints both versions in the release PR. The platform packages, `vite-plus`, and `create-vite-plus` use the `vite-plus` version. See [Core Version](packages/core/BUNDLING.md#core-version).
 
 The [propagation helper](.github/scripts/wait-for-npm-packages.ts) defaults to a 10-minute timeout and an additional 60-second wait for CDN propagation after availability checks pass.
 
diff --git a/README.md b/README.md
index 9e08310875..ec0f5e7a82 100644
--- a/README.md
+++ b/README.md
@@ -203,7 +203,7 @@ If you are manually migrating a project to Vite+, install these dev dependencies
 vp install -D vite-plus
 ```
 
-Add package-manager overrides so that other packages use the Vite+ versions. Alias `vite` to `@voidzero-dev/vite-plus-core`, setting `` to the core version that the installed `vite-plus` uses, shown by `vp toolchain vite-plus-core`. The core version encodes the bundled Vite version (for example, `8.3.500` for Vite `8.3.5`), so package managers check `vite` peer ranges against the bundled Vite. Pin `vitest` to ``, using the bundled version shown by `vp toolchain vitest`. The project and `vp test` then use the same Vitest copy. Without the pin, a dependency or workspace package can install a different Vitest version. The two versions can use separate mocks, `expect` functions, and runner states:
+Add package-manager overrides so that other packages use the Vite+ versions. Alias `vite` to `@voidzero-dev/vite-plus-core`, setting `` to the core version that the installed `vite-plus` uses, shown by `vp toolchain vite-plus-core`. The core version encodes the bundled Vite version (for example, `8.3.5000` for Vite `8.3.5`), so package managers check `vite` peer ranges against the bundled Vite. Pin `vitest` to ``, using the bundled version shown by `vp toolchain vitest`. The project and `vp test` then use the same Vitest copy. Without the pin, a dependency or workspace package can install a different Vitest version. The two versions can use separate mocks, `expect` functions, and runner states:
 
 ```json
 "overrides": {
diff --git a/docs/guide/local-cli.md b/docs/guide/local-cli.md
index 5a055eecfd..532a1e3b23 100644
--- a/docs/guide/local-cli.md
+++ b/docs/guide/local-cli.md
@@ -113,7 +113,7 @@ If you also declare `vitest` directly, set its dependency entry to the exact ver
 Dependencies and plugins can import `vite` or `vitest` directly, even when your own code imports from `vite-plus`. These overrides align their dependencies with the toolchain Vite+ uses:
 
 - The `vite` alias directs those imports to Vite+'s core package. Separate Vite instances can break runtime identity checks: [issue #1391](https://github.com/voidzero-dev/vite-plus/issues/1391) reported TanStack Start returning 404s because an `instanceof` check crossed two copies. [PR #2617](https://github.com/voidzero-dev/vite-plus/pull/2617) addresses the CLI side by sharing Vite through the same alias.
-- The core version encodes the bundled Vite version. Its major and minor are the Vite major and minor, and its patch is the Vite patch times 100 plus a release counter. For example, core `8.3.500` bundles Vite `8.3.5`. Package managers compare this version with the `vite` peer ranges of plugins, so a plugin that supports the bundled Vite passes the check, and a plugin that does not support it still gets a peer warning. [Issue #2806](https://github.com/voidzero-dev/vite-plus/issues/2806) describes the earlier `1.x` versions, which failed every check.
+- The core version encodes the bundled Vite version. Its major and minor are the Vite major and minor, and its patch is the Vite patch times 1000 plus a release counter, the same convention that `oxlint-tsgolint` uses for TypeScript. For example, core `8.3.5000` bundles Vite `8.3.5`. Package managers compare this version with the `vite` peer ranges of plugins, so a plugin that supports the bundled Vite passes the check, and a plugin that does not support it still gets a peer warning. [Issue #2806](https://github.com/voidzero-dev/vite-plus/issues/2806) describes the earlier `1.x` versions, which failed every check.
 - The exact `vitest` pin keeps dependencies and `vp test` on the same Vitest version, avoiding separate mocks, `expect` instances, and runner state. [PR #2365](https://github.com/voidzero-dev/vite-plus/pull/2365) documents this requirement for manual installation.
 
 Keep the core alias aligned with the core version of your installed `vite-plus` and update the Vitest pin to match its bundled version when upgrading. [Issue #2356](https://github.com/voidzero-dev/vite-plus/issues/2356) describes how dependency bots can update these packages independently and leave incompatible versions installed together.
diff --git a/docs/guide/migrate-rules.md b/docs/guide/migrate-rules.md
index e3db354bcf..65e0252cb9 100644
--- a/docs/guide/migrate-rules.md
+++ b/docs/guide/migrate-rules.md
@@ -379,7 +379,7 @@ Migration converts the Volta `volta.node` pin to `.node-version`, because Vite+
 - pnpm accepts the logical default catalog as either top-level `catalog` or `catalogs.default`, but not both. Migration preserves the existing form and never creates the other form beside it.
 - When an existing named catalog already owns `vite-plus`, `vite`, or `vitest`, migration reuses that managed toolchain catalog for newly added dependencies and overrides. It creates a top-level default catalog only when no managed or default catalog can be reused.
 
-**Peer rules.** The core version encodes the bundled Vite version: core `8.3.500` bundles Vite `8.3.5` (see the [RFC](https://github.com/voidzero-dev/vite-plus/blob/main/rfcs/core-version-alignment.md)). pnpm checks `vite` peer ranges against the `vite` alias, so a plugin that supports the bundled Vite passes, and a plugin that does not support it fails.
+**Peer rules.** The core version encodes the bundled Vite version: core `8.3.5000` bundles Vite `8.3.5` (see the [RFC](https://github.com/voidzero-dev/vite-plus/blob/main/rfcs/core-version-alignment.md)). pnpm checks `vite` peer ranges against the `vite` alias, so a plugin that supports the bundled Vite passes, and a plugin that does not support it fails.
 
 - Migration does not add `vite` to `peerDependencyRules.allowAny` or `peerDependencyRules.allowedVersions`.
 - Migration removes `vite` from `allowAny` and removes `allowedVersions.vite` when its value is `'*'`, also when you wrote the entry. The only purpose of those entries was to accept the `1.x` core alias. Another `allowedVersions.vite` range, such as `'7 || 8'`, stays. Migration removes a field, or `peerDependencyRules`, when no entry remains. Other entries stay. The migrate summary names the removed rule.
diff --git a/docs/guide/upgrade-project.md b/docs/guide/upgrade-project.md
index 969bf6e6fe..22ec3523d2 100644
--- a/docs/guide/upgrade-project.md
+++ b/docs/guide/upgrade-project.md
@@ -18,7 +18,7 @@ On a project that is already on Vite+, migrate does a toolchain version upgrade
 
 ## Manually Updating
 
-Update `vite-plus` and the `vite` alias to `@voidzero-dev/vite-plus-core` together. The two packages have different versions: the core version encodes the bundled Vite version (for example, `8.3.500` for Vite `8.3.5`). Read the alias that a `vite-plus` version uses before you install it:
+Update `vite-plus` and the `vite` alias to `@voidzero-dev/vite-plus-core` together. The two packages have different versions: the core version encodes the bundled Vite version (for example, `8.3.5000` for Vite `8.3.5`). Read the alias that a `vite-plus` version uses before you install it:
 
 ```bash
 npm view vite-plus@ dependencies.vite
diff --git a/packages/cli/README.md b/packages/cli/README.md
index c75fd08e08..9ae8bdd73a 100644
--- a/packages/cli/README.md
+++ b/packages/cli/README.md
@@ -198,7 +198,7 @@ If you are manually migrating a project to Vite+, install these dev dependencies
 vp install -D vite-plus
 ```
 
-Add package-manager overrides so that other packages use the Vite+ versions. Alias `vite` to `@voidzero-dev/vite-plus-core`, setting `` to the core version that the installed `vite-plus` uses, shown by `vp toolchain vite-plus-core`. The core version encodes the bundled Vite version (for example, `8.3.500` for Vite `8.3.5`), so package managers check `vite` peer ranges against the bundled Vite. Pin `vitest` to ``, using the bundled version shown by `vp toolchain vitest`. The project and `vp test` then use the same Vitest copy. Without the pin, a dependency or workspace package can install a different Vitest version. The two versions can use separate mocks, `expect` functions, and runner states:
+Add package-manager overrides so that other packages use the Vite+ versions. Alias `vite` to `@voidzero-dev/vite-plus-core`, setting `` to the core version that the installed `vite-plus` uses, shown by `vp toolchain vite-plus-core`. The core version encodes the bundled Vite version (for example, `8.3.5000` for Vite `8.3.5`), so package managers check `vite` peer ranges against the bundled Vite. Pin `vitest` to ``, using the bundled version shown by `vp toolchain vitest`. The project and `vp test` then use the same Vitest copy. Without the pin, a dependency or workspace package can install a different Vitest version. The two versions can use separate mocks, `expect` functions, and runner states:
 
 ```json
 "overrides": {
diff --git a/packages/cli/src/__tests__/resolve-core.spec.ts b/packages/cli/src/__tests__/resolve-core.spec.ts
index 86cb6dfc2a..948938944a 100644
--- a/packages/cli/src/__tests__/resolve-core.spec.ts
+++ b/packages/cli/src/__tests__/resolve-core.spec.ts
@@ -99,7 +99,7 @@ describe('resolveCore', () => {
   });
 
   it.each([
-    ['0.0.0', '8.3.599'],
+    ['0.0.0', '8.3.5999'],
     ['0.0.0-commit.1234567', '0.0.0-commit.1234567'],
   ])('uses the installed CLI manifest after repacking as %s', (version, coreVersion) => {
     writeCli(join(root, 'cli'), version, `npm:@voidzero-dev/vite-plus-core@${coreVersion}`);
@@ -112,11 +112,11 @@ describe('resolveCore', () => {
   it.each(['project', 'bundled'])(
     'still rejects a mismatched %s core after repacking the CLI',
     (location) => {
-      writeCli(join(root, 'cli'), '0.0.0', 'npm:@voidzero-dev/vite-plus-core@8.3.599');
-      writeCore(bundled, '@voidzero-dev/vite-plus-core', '8.3.599');
+      writeCli(join(root, 'cli'), '0.0.0', 'npm:@voidzero-dev/vite-plus-core@8.3.5999');
+      writeCore(bundled, '@voidzero-dev/vite-plus-core', '8.3.5999');
       writeCore(location === 'project' ? projectCore : bundled);
       expect(() => resolveCore('', project, cliModule)).toThrow(
-        `Expected @voidzero-dev/vite-plus-core@8.3.599, but found @voidzero-dev/vite-plus-core@${CORE_VERSION}`,
+        `Expected @voidzero-dev/vite-plus-core@8.3.5999, but found @voidzero-dev/vite-plus-core@${CORE_VERSION}`,
       );
     },
   );
diff --git a/packages/cli/src/migration/__tests__/vite-peer-dependency-rule.spec.ts b/packages/cli/src/migration/__tests__/vite-peer-dependency-rule.spec.ts
index a840367953..ba9207575a 100644
--- a/packages/cli/src/migration/__tests__/vite-peer-dependency-rule.spec.ts
+++ b/packages/cli/src/migration/__tests__/vite-peer-dependency-rule.spec.ts
@@ -26,7 +26,7 @@ const {
   vitePeerDependencyRuleRequired,
 } = await import('../migrator.js');
 
-const STABLE_CORE = 'npm:@voidzero-dev/vite-plus-core@8.3.500';
+const STABLE_CORE = 'npm:@voidzero-dev/vite-plus-core@8.3.5000';
 const PREVIEW_CORE = 'npm:@voidzero-dev/vite-plus-core@0.0.0-commit.0c515e3';
 
 type PeerDependencyRules = { allowAny?: string[]; allowedVersions?: Record };
@@ -103,9 +103,9 @@ describe('vitePeerDependencyRuleRequired', () => {
     ['npm:@voidzero-dev/vite-plus-core@0.0.0', true],
     [PREVIEW_CORE, true],
     ['npm:@voidzero-dev/vite-plus-core@1.4.0', true],
-    ['npm:@voidzero-dev/vite-plus-core@8.3.500+build.1', true],
+    ['npm:@voidzero-dev/vite-plus-core@8.3.5000+build.1', true],
     ['npm:@voidzero-dev/vite-plus-core@latest', true],
-    ['file:../voidzero-dev-vite-plus-core-8.3.599.tgz', true],
+    ['file:../voidzero-dev-vite-plus-core-8.3.5999.tgz', true],
     ['https://pkg.pr.new/voidzero-dev/vite-plus/@voidzero-dev/vite-plus-core@1891', true],
   ])('%s requires the rule: %s', (spec, required) => {
     expect(vitePeerDependencyRuleRequired(spec)).toBe(required);
diff --git a/packages/cli/src/migration/migrator/shared.ts b/packages/cli/src/migration/migrator/shared.ts
index 2f64783922..6d45114212 100644
--- a/packages/cli/src/migration/migrator/shared.ts
+++ b/packages/cli/src/migration/migrator/shared.ts
@@ -219,7 +219,7 @@ export const VITEST_IS_MANAGED_OVERRIDE = 'vitest' in VITE_PLUS_OVERRIDE_PACKAGE
  * Whether pnpm still needs the managed `vite` peer rule (`allowAny: [vite]`,
  * `allowedVersions: { vite: '*' }`) for the given core alias.
  *
- * A stable core version encodes the bundled Vite version (`8.3.500` bundles
+ * A stable core version encodes the bundled Vite version (`8.3.5000` bundles
  * Vite `8.3.5`, see rfcs/core-version-alignment.md). Package managers check
  * `vite` peer ranges against it correctly, so the rule would only hide real
  * incompatibilities. Every other spec still needs the rule: a preview
diff --git a/packages/cli/src/sync-versions/__tests__/plan.spec.ts b/packages/cli/src/sync-versions/__tests__/plan.spec.ts
index 8b4b4751b1..4bdd99da93 100644
--- a/packages/cli/src/sync-versions/__tests__/plan.spec.ts
+++ b/packages/cli/src/sync-versions/__tests__/plan.spec.ts
@@ -10,7 +10,7 @@ import {
 
 const toolchain: SyncVersionsToolchain = {
   vitePlus: '0.4.0',
-  vitePlusCore: '8.3.500',
+  vitePlusCore: '8.3.5000',
   vitest: '5.0.0',
 };
 
@@ -51,7 +51,7 @@ describe('planSyncVersions', () => {
         '@vitest/coverage-v8': '5.0.0',
         '@vitest/eslint-plugin': '1.6.0',
         '@vitest/coverage-c8': '0.33.0',
-        '@voidzero-dev/vite-plus-core': '8.3.500',
+        '@voidzero-dev/vite-plus-core': '8.3.5000',
         'vite-plus': '0.4.0',
         vitest: '5.0.0',
       },
@@ -78,7 +78,7 @@ describe('planSyncVersions', () => {
       devDependencies: {
         '@vitest/ui': 'npm:@vitest/ui@5.0.0',
         'vite-plus': 'npm:@scope/vite-plus-fork@0.3.0',
-        vite: 'npm:@voidzero-dev/vite-plus-core@8.3.500',
+        vite: 'npm:@voidzero-dev/vite-plus-core@8.3.5000',
         vitest: 'npm:vitest@5.0.0',
       },
     });
@@ -114,7 +114,7 @@ describe('planSyncVersions', () => {
       catalog: { '@vitest/coverage-v8': '5.0.0' },
       catalogs: {
         toolchain: {
-          vite: 'npm:@voidzero-dev/vite-plus-core@8.3.500',
+          vite: 'npm:@voidzero-dev/vite-plus-core@8.3.5000',
           'vite-plus': '0.4.0',
         },
       },
@@ -139,7 +139,7 @@ describe('planSyncVersions', () => {
         .replace("vitest: '^4.1.11'", "vitest: '5.0.0'")
         .replace(
           'vite: "npm:@voidzero-dev/vite-plus-core@0.3.0"',
-          'vite: "npm:@voidzero-dev/vite-plus-core@8.3.500"',
+          'vite: "npm:@voidzero-dev/vite-plus-core@8.3.5000"',
         )
         .replace('vite-plus: 0.3.0', 'vite-plus: 0.4.0'),
     );
diff --git a/packages/cli/src/sync-versions/__tests__/protocol.spec.ts b/packages/cli/src/sync-versions/__tests__/protocol.spec.ts
index b95c7e61ef..9cbf0828fb 100644
--- a/packages/cli/src/sync-versions/__tests__/protocol.spec.ts
+++ b/packages/cli/src/sync-versions/__tests__/protocol.spec.ts
@@ -6,7 +6,7 @@ const manifest = {
   schemaVersion: 1,
   nodes: [
     { id: 'vite-plus', version: '0.4.0' },
-    { id: 'vite-plus-core', version: '8.3.500' },
+    { id: 'vite-plus-core', version: '8.3.5000' },
     { id: 'vitest', version: '5.0.0' },
   ],
 };
@@ -53,7 +53,7 @@ describe('toolchainFromManifest', () => {
   it('reads the exact Vite+, core, and Vitest versions', () => {
     expect(toolchainFromManifest(manifest)).toEqual({
       vitePlus: '0.4.0',
-      vitePlusCore: '8.3.500',
+      vitePlusCore: '8.3.5000',
       vitest: '5.0.0',
     });
   });
@@ -73,7 +73,7 @@ describe('toolchainFromManifest', () => {
       schemaVersion: 1,
       nodes: [
         { id: 'vite-plus', version: '0.4.0' },
-        { id: 'vite-plus-core', version: '8.3.500' },
+        { id: 'vite-plus-core', version: '8.3.5000' },
         { id: 'vitest', version: '' },
       ],
     },
diff --git a/packages/cli/src/utils/__tests__/constants.spec.ts b/packages/cli/src/utils/__tests__/constants.spec.ts
index 2c4e4b4b1d..42768cb67f 100644
--- a/packages/cli/src/utils/__tests__/constants.spec.ts
+++ b/packages/cli/src/utils/__tests__/constants.spec.ts
@@ -41,7 +41,7 @@ describe('Vite+ dependency versions', () => {
 
   it('uses VP_CORE_VERSION for the core alias', async () => {
     vi.stubEnv('VP_VERSION', '0.0.0');
-    vi.stubEnv('VP_CORE_VERSION', '8.3.599');
+    vi.stubEnv('VP_CORE_VERSION', '8.3.5999');
     vi.stubEnv('VP_OVERRIDE_PACKAGES', '');
     vi.resetModules();
 
@@ -49,8 +49,8 @@ describe('Vite+ dependency versions', () => {
       await import('../constants.js');
 
     expect(VITE_PLUS_VERSION).toBe('0.0.0');
-    expect(VITE_PLUS_CORE_VERSION).toBe('8.3.599');
-    expect(VITE_PLUS_OVERRIDE_PACKAGES.vite).toBe('npm:@voidzero-dev/vite-plus-core@8.3.599');
+    expect(VITE_PLUS_CORE_VERSION).toBe('8.3.5999');
+    expect(VITE_PLUS_OVERRIDE_PACKAGES.vite).toBe('npm:@voidzero-dev/vite-plus-core@8.3.5999');
   });
 
   it('preserves explicit prerelease overrides', async () => {
diff --git a/packages/core/BUNDLING.md b/packages/core/BUNDLING.md
index 6ab1d13899..1e0607fca3 100644
--- a/packages/core/BUNDLING.md
+++ b/packages/core/BUNDLING.md
@@ -316,7 +316,7 @@ dist/
 1. Update the Vite pin in [`packages/tools/.upstream-versions.json`](../tools/.upstream-versions.json) and review the resolver patch against the new source
 2. Follow the [upstream sync and build instructions](../../CONTRIBUTING.md#pull-upstream-dependencies) to prepare the source, regenerate Vite types, and rebuild the packages
 3. Verify `bundledVersions.vite` in `package.json` is updated
-4. Set `version` in `package.json` to the encoded version for the new Vite version, for example `8.4.200` for Vite `8.4.2` (see [Core Version](#core-version)). The upgrade-deps workflow does this step automatically
+4. Set `version` in `package.json` to the encoded version for the new Vite version, for example `8.4.2000` for Vite `8.4.2` (see [Core Version](#core-version)). The upgrade-deps workflow does this step automatically
 5. Test with `pnpm test`
 
 ### Updating Rolldown
@@ -411,14 +411,14 @@ This is automatically updated by `mergePackageJson()` during each build.
 
 The core version encodes `bundledVersions.vite`, so it differs from the `vite-plus` version. Package managers compare the version of the `vite` alias target with the `vite` peer ranges of plugins, so the encoded version makes those checks match the bundled Vite. See [the RFC](../../rfcs/core-version-alignment.md) for the design.
 
-| Release                                 | Core version                                       | Example                          |
-| --------------------------------------- | -------------------------------------------------- | -------------------------------- |
-| Stable `vite-plus`                      | `..` | Vite `8.3.5`, `n = 1`: `8.3.501` |
-| `vite-plus` prerelease, stable Vite     | `-vp.`                | `8.4.0-vp.0`                     |
-| `vite-plus` prerelease, Vite prerelease | `.vp.`                            | `9.0.0-beta.3.vp.0`              |
-| Local and CI builds                     | `n = 99`                                           | `8.3.599`                        |
+| Release                                 | Core version                                        | Example                           |
+| --------------------------------------- | --------------------------------------------------- | --------------------------------- |
+| Stable `vite-plus`                      | `..` | Vite `8.3.5`, `n = 1`: `8.3.5001` |
+| `vite-plus` prerelease, stable Vite     | `-vp.`                 | `8.4.0-vp.0`                      |
+| `vite-plus` prerelease, Vite prerelease | `.vp.`                             | `9.0.0-beta.3.vp.0`               |
+| Local and CI builds                     | `n = 999`                                           | `8.3.5999`                        |
 
-`n` counts the stable releases on one Vite version, from 0 to 98. `k` counts the prereleases on one base. [`packages/tools/src/core-version.ts`](../tools/src/core-version.ts) implements the scheme:
+`n` counts the stable releases on one Vite version, from 0 to 998. `k` counts the prereleases on one base. This follows the [`oxlint-tsgolint` versioning](https://github.com/oxc-project/tsgolint#versioning), where `7.0.2000` is the first release on TypeScript `7.0.2`. [`packages/tools/src/core-version.ts`](../tools/src/core-version.ts) implements the scheme:
 
 - `prepare_release.yml` runs `core-version.ts prepare` to select the next unpublished version from the registry. It writes that version and the `vite-plus` version (`vitePlusVersion`) to `package.json`.
 - `release.yml` runs `core-version.ts check --release`. It fails when the registry already holds the core version for a different `vite-plus` release.
diff --git a/packages/core/package.json b/packages/core/package.json
index 33833f0297..40a5db5b3e 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@voidzero-dev/vite-plus-core",
-  "version": "8.3.700",
+  "version": "8.3.7000",
   "description": "The Unified Toolchain for the Web",
   "homepage": "https://viteplus.dev/guide",
   "bugs": {
diff --git a/packages/tools/src/__tests__/core-version.spec.ts b/packages/tools/src/__tests__/core-version.spec.ts
index 71a9ce9282..a0f7dff6e3 100644
--- a/packages/tools/src/__tests__/core-version.spec.ts
+++ b/packages/tools/src/__tests__/core-version.spec.ts
@@ -13,24 +13,24 @@ const PUBLISHED_BEFORE_ALIGNMENT = ['0.1.21-alpha.7', '0.0.2-test.1', '1.0.0-rc.
 
 describe('encodeCoreVersion', () => {
   test('encodes the Vite patch and the release counter in the patch', () => {
-    expect(encodeCoreVersion('8.3.5', 0)).toBe('8.3.500');
-    expect(encodeCoreVersion('8.3.5', 1)).toBe('8.3.501');
+    expect(encodeCoreVersion('8.3.5', 0)).toBe('8.3.5000');
+    expect(encodeCoreVersion('8.3.5', 1)).toBe('8.3.5001');
     expect(encodeCoreVersion('8.4.0', 0)).toBe('8.4.0');
-    expect(encodeCoreVersion('8.4.1', 0)).toBe('8.4.100');
-    expect(encodeCoreVersion('8.3.5', 99)).toBe('8.3.599');
+    expect(encodeCoreVersion('8.4.1', 0)).toBe('8.4.1000');
+    expect(encodeCoreVersion('8.3.5', 999)).toBe('8.3.5999');
   });
 
   test('rejects a Vite prerelease and an out-of-range counter', () => {
     expect(() => encodeCoreVersion('9.0.0-beta.3', 0)).toThrow('Vite prerelease');
-    expect(() => encodeCoreVersion('8.3.5', 100)).toThrow('from 0 to 99');
-    expect(() => encodeCoreVersion('8.3.5', -1)).toThrow('from 0 to 99');
+    expect(() => encodeCoreVersion('8.3.5', 1000)).toThrow('from 0 to 999');
+    expect(() => encodeCoreVersion('8.3.5', -1)).toThrow('from 0 to 999');
   });
 });
 
 describe('decodeCoreVersion', () => {
   test('decodes stable and prerelease core versions', () => {
-    expect(decodeCoreVersion('8.3.500')).toEqual({ vite: '8.3.5', n: 0 });
-    expect(decodeCoreVersion('8.3.501')).toEqual({ vite: '8.3.5', n: 1 });
+    expect(decodeCoreVersion('8.3.5000')).toEqual({ vite: '8.3.5', n: 0 });
+    expect(decodeCoreVersion('8.3.5001')).toEqual({ vite: '8.3.5', n: 1 });
     expect(decodeCoreVersion('8.4.1')).toEqual({ vite: '8.4.0', n: 1 });
     expect(decodeCoreVersion('8.4.0-vp.2')).toEqual({ vite: '8.4.0', n: 0, k: 2 });
     expect(decodeCoreVersion('9.0.0-beta.3.vp.1')).toEqual({ vite: '9.0.0-beta.3', k: 1 });
@@ -48,18 +48,18 @@ describe('nextCoreVersion', () => {
   test('starts the counter at 0 for a new Vite version', () => {
     expect(
       nextCoreVersion({ vite: '8.3.5', vitePlus: '1.5.0', published: PUBLISHED_BEFORE_ALIGNMENT }),
-    ).toBe('8.3.500');
+    ).toBe('8.3.5000');
   });
 
   test('increments the counter for each stable release on one Vite version', () => {
-    const published = [...PUBLISHED_BEFORE_ALIGNMENT, '8.3.500', '8.3.501', '8.3.600'];
-    expect(nextCoreVersion({ vite: '8.3.5', vitePlus: '1.5.2', published })).toBe('8.3.502');
-    expect(nextCoreVersion({ vite: '8.3.6', vitePlus: '1.6.1', published })).toBe('8.3.601');
+    const published = [...PUBLISHED_BEFORE_ALIGNMENT, '8.3.5000', '8.3.5001', '8.3.6000'];
+    expect(nextCoreVersion({ vite: '8.3.5', vitePlus: '1.5.2', published })).toBe('8.3.5002');
+    expect(nextCoreVersion({ vite: '8.3.6', vitePlus: '1.6.1', published })).toBe('8.3.6001');
     expect(nextCoreVersion({ vite: '8.4.0', vitePlus: '1.7.0', published })).toBe('8.4.0');
   });
 
   test('ignores prereleases and local versions for the stable counter', () => {
-    const published = ['8.4.0-vp.0', '8.4.0-vp.1', '8.4.99'];
+    const published = ['8.4.0-vp.0', '8.4.0-vp.1', '8.4.999'];
     expect(nextCoreVersion({ vite: '8.4.0', vitePlus: '1.7.0', published })).toBe('8.4.0');
   });
 
@@ -99,7 +99,7 @@ describe('nextCoreVersion', () => {
   });
 
   test('fails when the release counter is exhausted', () => {
-    const published = Array.from({ length: 99 }, (_, n) => encodeCoreVersion('8.3.5', n));
+    const published = Array.from({ length: 999 }, (_, n) => encodeCoreVersion('8.3.5', n));
     expect(() => nextCoreVersion({ vite: '8.3.5', vitePlus: '1.9.0', published })).toThrow(
       'the maximum',
     );
@@ -108,7 +108,7 @@ describe('nextCoreVersion', () => {
 
 describe('initialCoreVersion', () => {
   test('starts the counter for a stable and a prerelease Vite', () => {
-    expect(initialCoreVersion('8.4.2')).toBe('8.4.200');
+    expect(initialCoreVersion('8.4.2')).toBe('8.4.2000');
     expect(initialCoreVersion('9.0.0-beta.3')).toBe('9.0.0-beta.3.vp.0');
   });
 });
@@ -119,8 +119,8 @@ function manifest(version: string, vite = '8.3.5', vitePlusVersion?: string) {
 
 describe('assertCoreVersionMatchesVite', () => {
   test('accepts a core version that encodes the bundled Vite', () => {
-    expect(() => assertCoreVersionMatchesVite(manifest('8.3.500'), '1.5.0')).not.toThrow();
-    expect(() => assertCoreVersionMatchesVite(manifest('8.3.599'), '0.0.0')).not.toThrow();
+    expect(() => assertCoreVersionMatchesVite(manifest('8.3.5000'), '1.5.0')).not.toThrow();
+    expect(() => assertCoreVersionMatchesVite(manifest('8.3.5999'), '0.0.0')).not.toThrow();
     expect(() =>
       assertCoreVersionMatchesVite(manifest('8.4.0-vp.1', '8.4.0'), '1.7.0-rc.1'),
     ).not.toThrow();
@@ -130,8 +130,8 @@ describe('assertCoreVersionMatchesVite', () => {
   });
 
   test('rejects a core version that encodes another Vite version', () => {
-    expect(() => assertCoreVersionMatchesVite(manifest('8.3.400'), '1.5.0')).toThrow(
-      'Set its version to 8.3.500',
+    expect(() => assertCoreVersionMatchesVite(manifest('8.3.4000'), '1.5.0')).toThrow(
+      'Set its version to 8.3.5000',
     );
     expect(() => assertCoreVersionMatchesVite(manifest('1.4.0'), '1.4.0')).toThrow(
       'encodes Vite 1.4.0',
@@ -146,12 +146,12 @@ describe('assertCoreVersionMatchesVite', () => {
 
   test('rejects mixed prerelease and stable versions for a release', () => {
     expect(() =>
-      assertCoreVersionMatchesVite(manifest('8.3.500', '8.3.5', '1.5.0-rc.0'), '1.5.0-rc.0', {
+      assertCoreVersionMatchesVite(manifest('8.3.5000', '8.3.5', '1.5.0-rc.0'), '1.5.0-rc.0', {
         release: true,
       }),
     ).toThrow('both be prereleases');
     expect(() =>
-      assertCoreVersionMatchesVite(manifest('8.3.500-vp.0', '8.3.5', '1.5.0'), '1.5.0', {
+      assertCoreVersionMatchesVite(manifest('8.3.5000-vp.0', '8.3.5', '1.5.0'), '1.5.0', {
         release: true,
       }),
     ).toThrow('both be prereleases');
@@ -159,12 +159,12 @@ describe('assertCoreVersionMatchesVite', () => {
 
   test('rejects the local build counter for a release', () => {
     expect(() =>
-      assertCoreVersionMatchesVite(manifest('8.3.599', '8.3.5', '1.5.0'), '1.5.0', {
+      assertCoreVersionMatchesVite(manifest('8.3.5999', '8.3.5', '1.5.0'), '1.5.0', {
         release: true,
       }),
     ).toThrow('reserved for local builds');
     expect(() =>
-      assertCoreVersionMatchesVite(manifest('8.3.598', '8.3.5', '1.5.0'), '1.5.0', {
+      assertCoreVersionMatchesVite(manifest('8.3.5998', '8.3.5', '1.5.0'), '1.5.0', {
         release: true,
       }),
     ).not.toThrow();
@@ -172,10 +172,10 @@ describe('assertCoreVersionMatchesVite', () => {
 
   test('requires the manifest to record the released vite-plus version', () => {
     expect(() =>
-      assertCoreVersionMatchesVite(manifest('8.3.500'), '1.5.0', { release: true }),
+      assertCoreVersionMatchesVite(manifest('8.3.5000'), '1.5.0', { release: true }),
     ).toThrow('records vite-plus (none)');
     expect(() =>
-      assertCoreVersionMatchesVite(manifest('8.3.500', '8.3.5', '1.4.0'), '1.5.0', {
+      assertCoreVersionMatchesVite(manifest('8.3.5000', '8.3.5', '1.4.0'), '1.5.0', {
         release: true,
       }),
     ).toThrow('records vite-plus 1.4.0, but the release is vite-plus 1.5.0');
@@ -184,17 +184,17 @@ describe('assertCoreVersionMatchesVite', () => {
 
 describe('assertCoreVersionUnclaimed', () => {
   test('accepts an unpublished core version and a retry of the same release', () => {
-    expect(() => assertCoreVersionUnclaimed(undefined, '8.3.501', '1.5.1')).not.toThrow();
+    expect(() => assertCoreVersionUnclaimed(undefined, '8.3.5001', '1.5.1')).not.toThrow();
     expect(() =>
-      assertCoreVersionUnclaimed({ vitePlusVersion: '1.5.1' }, '8.3.501', '1.5.1'),
+      assertCoreVersionUnclaimed({ vitePlusVersion: '1.5.1' }, '8.3.5001', '1.5.1'),
     ).not.toThrow();
   });
 
   test('rejects a core version that another release published', () => {
     expect(() =>
-      assertCoreVersionUnclaimed({ vitePlusVersion: '1.5.1' }, '8.3.501', '1.6.0'),
+      assertCoreVersionUnclaimed({ vitePlusVersion: '1.5.1' }, '8.3.5001', '1.6.0'),
     ).toThrow('already published for vite-plus 1.5.1');
-    expect(() => assertCoreVersionUnclaimed({}, '8.3.500', '1.6.0')).toThrow(
+    expect(() => assertCoreVersionUnclaimed({}, '8.3.5000', '1.6.0')).toThrow(
       'already published for vite-plus (unknown)',
     );
   });
diff --git a/packages/tools/src/core-version.ts b/packages/tools/src/core-version.ts
index 93635e5484..7922c7ba96 100644
--- a/packages/tools/src/core-version.ts
+++ b/packages/tools/src/core-version.ts
@@ -5,11 +5,11 @@
  * `vite` peer ranges against the alias `vite: npm:@voidzero-dev/vite-plus-core@...`
  * correctly:
  *
- *   stable Vite+ release:          ..
+ *   stable Vite+ release:          ..
  *   Vite+ prerelease, stable Vite: -vp.
  *   Vite+ prerelease, Vite pre:    .vp.
  *
- * `n` counts the stable Vite+ releases on one Vite version (0 to 98). `n = 99`
+ * `n` counts the stable Vite+ releases on one Vite version (0 to 998). `n = 999`
  * is reserved for local and CI builds, so they never collide with a published
  * version. `k` counts the Vite+ prereleases on one base.
  *
@@ -30,8 +30,12 @@ import { parseArgs } from 'node:util';
 export const CORE_PACKAGE_NAME = '@voidzero-dev/vite-plus-core';
 /** The `vite` alias to the core: `npm:@voidzero-dev/vite-plus-core@`. */
 export const CORE_ALIAS_PREFIX = `npm:${CORE_PACKAGE_NAME}@`;
-const MAX_RELEASE_COUNTER = 98;
-const LOCAL_BUILD_COUNTER = 99;
+// Same convention as oxlint-tsgolint, which Vite+ bundles: `7.0.2000` is its
+// first release on TypeScript `7.0.2`, and `7.0.2001` the next one
+// (https://github.com/oxc-project/tsgolint#versioning).
+const PATCH_FACTOR = 1000;
+const MAX_RELEASE_COUNTER = 998;
+const LOCAL_BUILD_COUNTER = 999;
 
 const PRERELEASE_TAG = 'vp';
 const VERSION_RE =
@@ -79,7 +83,7 @@ export function encodeCoreVersion(vite: string, n: number): string {
   if (!Number.isInteger(n) || n < 0 || n > LOCAL_BUILD_COUNTER) {
     throw new Error(`The release counter must be an integer from 0 to ${LOCAL_BUILD_COUNTER}`);
   }
-  return `${major}.${minor}.${patch * 100 + n}`;
+  return `${major}.${minor}.${patch * PATCH_FACTOR + n}`;
 }
 
 /**
@@ -93,16 +97,16 @@ export function initialCoreVersion(vite: string): string {
 /** Decodes a core version into the bundled Vite version and its counters. */
 export function decodeCoreVersion(core: string): DecodedCoreVersion {
   const { major, minor, patch, prerelease } = parseVersion(core);
-  const stableVite = `${major}.${minor}.${Math.floor(patch / 100)}`;
+  const stableVite = `${major}.${minor}.${Math.floor(patch / PATCH_FACTOR)}`;
   if (prerelease.length === 0) {
-    return { vite: stableVite, n: patch % 100 };
+    return { vite: stableVite, n: patch % PATCH_FACTOR };
   }
   const k = prerelease.at(-1)!;
   if (prerelease.at(-2) !== PRERELEASE_TAG || !/^(?:0|[1-9]\d*)$/.test(k)) {
     throw new Error(`${core} is not an encoded core version`);
   }
   if (prerelease.length === 2) {
-    return { vite: stableVite, n: patch % 100, k: Number(k) };
+    return { vite: stableVite, n: patch % PATCH_FACTOR, k: Number(k) };
   }
   return {
     vite: `${major}.${minor}.${patch}-${prerelease.slice(0, -2).join('.')}`,
diff --git a/rfcs/core-version-alignment.md b/rfcs/core-version-alignment.md
index 25728d0377..41ac5f5d18 100644
--- a/rfcs/core-version-alignment.md
+++ b/rfcs/core-version-alignment.md
@@ -15,28 +15,34 @@ bundles. The core major and minor equal the bundled Vite major and minor. The
 core patch encodes the bundled Vite patch and a release counter:
 
 ```text
-stable Vite+ release:          ..
+stable Vite+ release:          ..
 Vite+ prerelease, stable Vite: -vp.
 Vite+ prerelease, Vite pre:    .vp.
 ```
 
 `n` counts the stable Vite+ releases that bundle the same Vite patch, from 0
-to 98. `n = 99` is reserved for local and CI builds. `k` counts the Vite+
+to 998. `n = 999` is reserved for local and CI builds. `k` counts the Vite+
 prereleases on the same base, from 0. Decoding is exact: the bundled Vite
-patch is `floor(core patch / 100)`.
+patch is `floor(core patch / 1000)`.
+
+This is the convention of `oxlint-tsgolint`, which Vite+ bundles. Its first
+release on TypeScript `7.0.2` is `7.0.2000`, and the next ones are `7.0.2001`,
+`7.0.2002`, and so on
+([tsgolint versioning](https://github.com/oxc-project/tsgolint#versioning)).
+One rule then applies to the versions that `vp --version` shows.
 
 | Vite+ release     | Bundled Vite   | Core version        |
 | ----------------- | -------------- | ------------------- |
-| `1.5.0` (first)   | `8.3.5`        | `8.3.500`           |
-| `1.5.1`           | `8.3.5`        | `8.3.501`           |
-| `1.6.0`           | `8.3.6`        | `8.3.600`           |
+| `1.5.0` (first)   | `8.3.5`        | `8.3.5000`          |
+| `1.5.1`           | `8.3.5`        | `8.3.5001`          |
+| `1.6.0`           | `8.3.6`        | `8.3.6000`          |
 | `1.7.0-rc.0`      | `8.4.0`        | `8.4.0-vp.0`        |
 | `1.7.0-rc.1`      | `8.4.0`        | `8.4.0-vp.1`        |
 | `1.7.0`           | `8.4.0`        | `8.4.0`             |
-| `1.7.1`           | `8.4.1`        | `8.4.100`           |
+| `1.7.1`           | `8.4.1`        | `8.4.1000`          |
 | `2.0.0-beta.0`    | `9.0.0-beta.3` | `9.0.0-beta.3.vp.0` |
 | `2.0.0-beta.1`    | `9.0.0-beta.3` | `9.0.0-beta.3.vp.1` |
-| Local or CI build | `8.3.5`        | `8.3.599`           |
+| Local or CI build | `8.3.5`        | `8.3.5999`          |
 
 `vite-plus` keeps its own version line (`1.x` today). The CLI keeps its exact
 alias dependency `vite: npm:@voidzero-dev/vite-plus-core@`.
@@ -125,19 +131,19 @@ Three options were compared against bundled Vite `8.3.5` with `vite-plus`
 - Option B: core major and minor = Vite major and minor; patch is a plain
   counter per Vite+ release (`8.3.0`, `8.3.1`, ...).
 - Option B+ (selected): like B, but the patch encodes the Vite patch
-  (`8.3.500`, `8.3.501`, ...).
+  (`8.3.5000`, `8.3.5001`, ...).
 - Option C: core = exact Vite version plus a prerelease tag
   (`8.3.5-vp.1.4.0`) or build metadata (`8.3.5+vp.1.4.0`).
 
-| Consumer range | Bundled `8.3.5` | A `8.4.0`    | B `8.3.0`    | B+ `8.3.500` | C `8.3.5-vp.1.4.0` (npm) |
-| -------------- | --------------- | ------------ | ------------ | ------------ | ------------------------ |
-| `^8.0.0`       | pass            | pass         | pass         | pass         | fail (wrong)             |
-| `^8.3.0`       | pass            | pass         | pass         | pass         | fail (wrong)             |
-| `>=8.3.2`      | pass            | pass         | fail (wrong) | pass         | fail (wrong)             |
-| `~8.3.0`       | pass            | fail (wrong) | pass         | pass         | fail (wrong)             |
-| `^7.0.0`       | fail            | fail         | fail         | fail         | fail                     |
-| `^8.4.0`       | fail            | pass (wrong) | fail         | fail         | fail                     |
-| `>=8.3.6`      | fail            | pass (wrong) | fail         | pass (wrong) | fail                     |
+| Consumer range | Bundled `8.3.5` | A `8.4.0`    | B `8.3.0`    | B+ `8.3.5000` | C `8.3.5-vp.1.4.0` (npm) |
+| -------------- | --------------- | ------------ | ------------ | ------------- | ------------------------ |
+| `^8.0.0`       | pass            | pass         | pass         | pass          | fail (wrong)             |
+| `^8.3.0`       | pass            | pass         | pass         | pass          | fail (wrong)             |
+| `>=8.3.2`      | pass            | pass         | fail (wrong) | pass          | fail (wrong)             |
+| `~8.3.0`       | pass            | fail (wrong) | pass         | pass          | fail (wrong)             |
+| `^7.0.0`       | fail            | fail         | fail         | fail          | fail                     |
+| `^8.4.0`       | fail            | pass (wrong) | fail         | fail          | fail                     |
+| `>=8.3.6`      | fail            | pass (wrong) | fail         | pass (wrong)  | fail                     |
 
 Option A fails on minor ranges in both directions. The `vite-plus` minor and
 the Vite minor drift apart: `v1.2.0` bundled Vite `8.3.4`, so core `8.2.0`
@@ -162,18 +168,18 @@ same Vite patch cannot publish. `v1.2.0` and `v1.3.0` both bundled Vite
 
 Option B+ is the selected scheme. Its residual false results are limited to
 patch floors above the bundled patch inside the same minor (`>=8.3.6` passes
-against `8.3.500`) and to exact pins (`8.3.5` fails against `8.3.500`). Both
+against `8.3.5000`) and to exact pins (`8.3.5` fails against `8.3.5000`). Both
 are rare in `vite` peer ranges. Vite+ follows Vite patch releases within days,
 so the window for the first case is short.
 
-The counter supports 99 stable Vite+ releases per Vite patch (`n` from 0 to
-98). `n = 99` is reserved for local and CI builds (see Local and CI builds).
+The counter supports 999 stable Vite+ releases per Vite patch (`n` from 0 to
+998). `n = 999` is reserved for local and CI builds (see Local and CI builds).
 Vite+ shipped five minor releases between 2026-09-28 and 2026-10-23 while Vite
 moved from `8.3.1` to `8.3.5`. Two releases on one Vite patch happened once
 (`v1.2.0` and `v1.3.0` on `8.3.4`).
 
 Core versions keep sorting correctly:
-`8.3.501 < 8.3.502-vp.0 < 8.3.502 < 8.3.600`.
+`8.3.5001 < 8.3.5002-vp.0 < 8.3.5002 < 8.3.6000`.
 
 ### Prereleases and new Vite majors
 
@@ -211,7 +217,7 @@ version is unique and in publish order. `semver@7.8.5` gives this order:
 ```text
 9.0.0-beta.3 < 9.0.0-beta.3.vp.0 < 9.0.0-beta.3.vp.1 < 9.0.0-beta.4
   < 9.0.0-rc.1.vp.0 < 9.0.0-vp.0 < 9.0.0-vp.1 < 9.0.0
-  < 9.0.100-vp.0 < 9.0.100
+  < 9.0.1000-vp.0 < 9.0.1000
 ```
 
 With `semver@7.8.5` and npm's strict prerelease rules, `9.0.0-beta.3.vp.0`
@@ -280,7 +286,7 @@ functions and a CLI entry:
   below.
 - `assertCoreVersionMatchesVite(pkg, vitePlus, { release })` asserts that
   `pkg.version` decodes to `pkg.bundledVersions.vite`. With `release`, it also
-  asserts the prerelease pairing, rejects `n = 99`, and requires
+  asserts the prerelease pairing, rejects `n = 999`, and requires
   `pkg.vitePlusVersion` to equal the released `vite-plus` version.
 - `assertCoreVersionUnclaimed(published, core, vitePlus)` rejects a core
   version that the registry already holds for a different `vite-plus`
@@ -294,7 +300,7 @@ Counter rules in `nextCoreVersion`:
 
 - Stable release: `n` is the highest `n` among published stable core versions
   with the same Vite version, plus 1, or 0 when none exists. If `n` would be
-  higher than 98, the function fails.
+  higher than 998, the function fails.
 - Prerelease on a stable Vite: the base is the version that the stable rule
   gives. `k` is the highest `k` among published `-vp.*` versions, plus
   1, or 0 when none exists.
@@ -341,36 +347,36 @@ local build is easy to detect. Core `0.0.0` does not encode a Vite version,
 and peer checks fail against it. These flows would not test the path without a
 `vite` peer rule that users get.
 
-These flows will set core to `encodeCoreVersion(vite, 99)`, for example
-`8.3.599` for Vite `8.3.5`. They keep `0.0.0` for the CLI, which
+These flows will set core to `encodeCoreVersion(vite, 999)`, for example
+`8.3.5999` for Vite `8.3.5`. They keep `0.0.0` for the CLI, which
 `ecosystem-ci/verify-install.ts` and `test-vp-create.yml` assert. The CLI
 reads the core version from `VP_CORE_VERSION` or from the core manifest, so
-`vp create` and `vp migrate` in these flows write the `8.3.599` alias and
+`vp create` and `vp migrate` in these flows write the `8.3.5999` alias and
 remove the `vite` peer rule.
 
-`nextCoreVersion` never returns `n = 99`, and `release.yml` rejects it.
+`nextCoreVersion` never returns `n = 999`, and `release.yml` rejects it.
 Thus a local version never reaches the registry, and the local registry
 cannot mix it up with a published version.
 
 ### Release pipeline changes
 
-| File                                                                                                                             | Change                                                                                                                                                                                                                                                                                                                                                                        |
-| -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `.github/workflows/prepare_release.yml`                                                                                          | Compute the core version as described above; stop passing `VERSION` to `update_json packages/core/package.json`                                                                                                                                                                                                                                                               |
-| `.github/workflows/release.yml`                                                                                                  | Read `CORE_VERSION` from `packages/core/package.json` at job start, and reject a version with `n = 99`; use it in "Publish core dependency tier", "Wait for core to propagate", the release notes, and the Discord embed. After publish, check that the core `latest` dist-tag equals `CORE_VERSION`. The version check against `unpkg.com/vite-plus@latest` stays on the CLI |
-| `.github/workflows/reusable-release-build.yml`                                                                                   | Stamp `packages/core/package.json` only for preview versions (`0.0.0-commit.*`). A release checkout already carries the encoded version. Update the verify step to the same rule                                                                                                                                                                                              |
-| `packages/core/build.ts`                                                                                                         | Pass `cliPkgJson.version` instead of `pkgJson.version` to `rewriteRolldownBindingRequires`. The platform packages publish at the CLI version, and the guard must compare against that version (`packages/core/build-support/rewrite-rolldown-binding.ts`)                                                                                                                     |
-| `packages/cli/publish-native-addons.ts`                                                                                          | No change: it mirrors the CLI's platform pins into core by name and does not use the core version                                                                                                                                                                                                                                                                             |
-| `.github/workflows/publish-preview.yml`, `publish-preview-register.yml`                                                          | No change: the bridge serves every workspace package as `0.0.0-commit.`, and the stamp before `pnpm pack` keeps the alias on that version. The sticky comment keeps printing one version for both packages                                                                                                                                                               |
-| `.github/workflows/test-vp-create.yml`, `reusable-ecosystem-ci.yml`                                                              | Keep `0.0.0` for the CLI. Set core to `encodeCoreVersion(vite, 99)` and pass it as `VP_CORE_VERSION` where the workflow passes `VP_VERSION` (see Local and CI builds)                                                                                                                                                                                                         |
-| `packages/tools/src/local-npm-registry.ts`, `pack-local-vite-plus.ts`                                                            | No change: both serve the packed tarball's own version                                                                                                                                                                                                                                                                                                                        |
-| `.github/scripts/upgrade-deps.ts`                                                                                                | Reset the core version after a bundled Vite change                                                                                                                                                                                                                                                                                                                            |
-| `.github/renovate.json`                                                                                                          | No change                                                                                                                                                                                                                                                                                                                                                                     |
-| `.github/scripts/update-homebrew-formula.mjs`                                                                                    | Read the core version from the `vite` alias of the published `vite-plus` manifest, and wait for core at that version                                                                                                                                                                                                                                                          |
-| `.claude/skills/release-manager/SKILL.md`                                                                                        | Read both versions from the release branch; publish checks use the core version for core                                                                                                                                                                                                                                                                                      |
-| `packages/core/BUNDLING.md`                                                                                                      | Document the version scheme next to `bundledVersions`                                                                                                                                                                                                                                                                                                                         |
-| `MAINTENANCE.md`                                                                                                                 | Mention the two versions in the release description                                                                                                                                                                                                                                                                                                                           |
-| `README.md`, `packages/cli/README.md`, `docs/guide/local-cli.md`, `docs/guide/upgrade-project.md`, `docs/guide/migrate-rules.md` | Replace `` in alias examples with `` and point to `vp toolchain vite-plus-core`; describe the new pnpm peer rule behavior                                                                                                                                                                                                                    |
+| File                                                                                                                             | Change                                                                                                                                                                                                                                                                                                                                                                         |
+| -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `.github/workflows/prepare_release.yml`                                                                                          | Compute the core version as described above; stop passing `VERSION` to `update_json packages/core/package.json`                                                                                                                                                                                                                                                                |
+| `.github/workflows/release.yml`                                                                                                  | Read `CORE_VERSION` from `packages/core/package.json` at job start, and reject a version with `n = 999`; use it in "Publish core dependency tier", "Wait for core to propagate", the release notes, and the Discord embed. After publish, check that the core `latest` dist-tag equals `CORE_VERSION`. The version check against `unpkg.com/vite-plus@latest` stays on the CLI |
+| `.github/workflows/reusable-release-build.yml`                                                                                   | Stamp `packages/core/package.json` only for preview versions (`0.0.0-commit.*`). A release checkout already carries the encoded version. Update the verify step to the same rule                                                                                                                                                                                               |
+| `packages/core/build.ts`                                                                                                         | Pass `cliPkgJson.version` instead of `pkgJson.version` to `rewriteRolldownBindingRequires`. The platform packages publish at the CLI version, and the guard must compare against that version (`packages/core/build-support/rewrite-rolldown-binding.ts`)                                                                                                                      |
+| `packages/cli/publish-native-addons.ts`                                                                                          | No change: it mirrors the CLI's platform pins into core by name and does not use the core version                                                                                                                                                                                                                                                                              |
+| `.github/workflows/publish-preview.yml`, `publish-preview-register.yml`                                                          | No change: the bridge serves every workspace package as `0.0.0-commit.`, and the stamp before `pnpm pack` keeps the alias on that version. The sticky comment keeps printing one version for both packages                                                                                                                                                                |
+| `.github/workflows/test-vp-create.yml`, `reusable-ecosystem-ci.yml`                                                              | Keep `0.0.0` for the CLI. Set core to `encodeCoreVersion(vite, 999)` and pass it as `VP_CORE_VERSION` where the workflow passes `VP_VERSION` (see Local and CI builds)                                                                                                                                                                                                         |
+| `packages/tools/src/local-npm-registry.ts`, `pack-local-vite-plus.ts`                                                            | No change: both serve the packed tarball's own version                                                                                                                                                                                                                                                                                                                         |
+| `.github/scripts/upgrade-deps.ts`                                                                                                | Reset the core version after a bundled Vite change                                                                                                                                                                                                                                                                                                                             |
+| `.github/renovate.json`                                                                                                          | No change                                                                                                                                                                                                                                                                                                                                                                      |
+| `.github/scripts/update-homebrew-formula.mjs`                                                                                    | Read the core version from the `vite` alias of the published `vite-plus` manifest, and wait for core at that version                                                                                                                                                                                                                                                           |
+| `.claude/skills/release-manager/SKILL.md`                                                                                        | Read both versions from the release branch; publish checks use the core version for core                                                                                                                                                                                                                                                                                       |
+| `packages/core/BUNDLING.md`                                                                                                      | Document the version scheme next to `bundledVersions`                                                                                                                                                                                                                                                                                                                          |
+| `MAINTENANCE.md`                                                                                                                 | Mention the two versions in the release description                                                                                                                                                                                                                                                                                                                            |
+| `README.md`, `packages/cli/README.md`, `docs/guide/local-cli.md`, `docs/guide/upgrade-project.md`, `docs/guide/migrate-rules.md` | Replace `` in alias examples with `` and point to `vp toolchain vite-plus-core`; describe the new pnpm peer rule behavior                                                                                                                                                                                                                     |
 
 ### Package-manager behavior
 
@@ -416,7 +422,7 @@ resolved the alias and listed it as `vite@1.4.0`. Whether Bun checks peer
 ranges in other layouts is unverified.
 
 The current `vite-plus` manifest triggers the npm warning on its own, because
-`vitest` is a dependency with a `vite` peer range. With core `8.3.500` that
+`vitest` is a dependency with a `vite` peer range. With core `8.3.5000` that
 range is satisfied, so a project that depends only on `vite-plus` installs
 without the warning.
 
@@ -470,13 +476,13 @@ rules document must say so.
 
 ### Downgrade
 
-A project on core `8.3.500` can move back to `vite-plus@1.4.0` by running
+A project on core `8.3.5000` can move back to `vite-plus@1.4.0` by running
 that CLI's `vp migrate`. The older CLI rewrites the alias to
 `npm:@voidzero-dev/vite-plus-core@1.4.0` and re-adds the pnpm `vite` peer
 rule, because its logic is self-contained. Its `resolveCore` check compares
 the core version with its own version, so it reports a mismatch until the
 alias is realigned. The newer CLI reports the same for a project still on
-`1.4.0`: "Expected `@voidzero-dev/vite-plus-core@8.3.500`, found `1.4.0`.
+`1.4.0`: "Expected `@voidzero-dev/vite-plus-core@8.3.5000`, found `1.4.0`.
 Run `vp migrate`." Exact pins make the registry ordering irrelevant for the
 downgrade.
 
@@ -506,7 +512,7 @@ The encoded form keeps patch floors correct up to the bundled patch.
 The encoded form does not make every core version look different from a Vite
 version. On Vite patch 0, the core patch is `n` alone (`8.4.0`, `8.4.1`,
 ...), and core `8.4.1` looks like Vite `8.4.1`. Decoding still gives the
-correct Vite version (`floor(1 / 100) = 0`). Peer checks stay in the residual
+correct Vite version (`floor(1 / 1000) = 0`). Peer checks stay in the residual
 error class of the selected scheme: `>=8.4.1` passes against core `8.4.1`.
 
 ### Exact Vite version with a prerelease tag or build metadata
@@ -514,6 +520,43 @@ error class of the selected scheme: `>=8.4.1` passes against core `8.4.1`.
 Fails npm peer checks and cannot publish two Vite+ releases on one Vite
 patch. See the comparison.
 
+### Encode the counter in the Vite prerelease number
+
+A core on a Vite prerelease could use the same `* 1000` rule as a stable
+core: Vite `9.0.0-beta.3` would give `9.0.0-beta.3000`, then
+`9.0.0-beta.3001`. This gives one shape for every core version and one
+identifier less.
+
+The stable form needs the counter in the patch, because an extra identifier
+would make the version a prerelease, and npm does not match a prerelease
+against ranges such as `^8.0.0`. A Vite prerelease is already a prerelease,
+so the counter can follow it as `.vp.`. That version sorts directly after
+the Vite version and before the next one. The encoded number sorts after
+later Vite prereleases:
+
+```text
+9.0.0-beta.3 < 9.0.0-beta.3.vp.0 < 9.0.0-beta.4
+9.0.0-beta.3 < 9.0.0-beta.4 < 9.0.0-beta.3000
+```
+
+`semver@7.8.6` gives these peer results:
+
+| Consumer range   | `9.0.0-beta.3.vp.0` | `9.0.0-beta.3000` |
+| ---------------- | ------------------- | ----------------- |
+| `^9.0.0-beta.1`  | pass                | pass              |
+| `^9.0.0-beta.3`  | pass                | pass              |
+| `^9.0.0-beta.5`  | fail                | pass (wrong)      |
+| `>=9.0.0-beta.5` | fail                | pass (wrong)      |
+| `^9.0.0-rc.0`    | fail                | fail              |
+
+The `.vp.` form has no residual false result for prerelease floors. The
+encoded form adds the stable scheme's residual error to them. Plugins use such
+floors during a Vite beta: `@sveltejs/vite-plugin-svelte` declares
+`^8.0.0-beta.7 || ^8.0.0`. A core that bundles Vite `8.0.0-beta.3` as
+`8.0.0-beta.3000` would pass that check although its Vite is too old.
+`oxlint-tsgolint` publishes no prerelease versions, so its convention does
+not decide this case.
+
 ### Publish core only when the bundled Vite changes
 
 Core would use the exact Vite version. Core also bundles Rolldown, tsdown,
@@ -533,18 +576,18 @@ manifest `version` takes part in the check.
 ## Risks
 
 - Confusion with upstream Vite. `npm ls vite` prints
-  `vite@npm:@voidzero-dev/vite-plus-core@8.3.500`, and readers may take
-  `8.3.500` for a Vite version. On Vite patch 0, the core version (`8.4.1`)
+  `vite@npm:@voidzero-dev/vite-plus-core@8.3.5000`, and readers may take
+  `8.3.5000` for a Vite version. On Vite patch 0, the core version (`8.4.1`)
   looks exactly like a Vite version. The decoding rule is one division, and
   `vp toolchain` prints the real Vite version. Documentation must show both.
 - Bundled Vite downgrade. If Vite+ goes back to an older Vite version, the
-  core version goes down, for example from `8.4.0` to `8.3.501`. The counter
+  core version goes down, for example from `8.4.0` to `8.3.5001`. The counter
   rules still give a unique version, and exact pins make the lower version
   install correctly. `release.yml` publishes core with `--tag latest`. How
   npm sets `latest` when the published version is lower than the current
   `latest` is unverified. The dist-tag check after publish (see Release
   pipeline changes) makes a wrong tag visible.
-- Registry ordering. `8.3.500` sorts above every published `1.x` version, so
+- Registry ordering. `8.3.5000` sorts above every published `1.x` version, so
   `latest` moves forward as usual and a range on core would jump majors.
   Vite+ documents exact pins only. The stale `alpha` and `test` dist-tags
   point at `0.x` versions and can be removed or left alone.
@@ -559,7 +602,7 @@ manifest `version` takes part in the check.
   non-blocker. Migration keeps the pnpm rule for preview cores. Thus preview
   builds cannot show the new peer behavior, and the preview check in
   `.claude/skills/test-pkg-pr-new-migrate/` does not test it. Verification
-  before a release uses the local registry with the `n = 99` local version
+  before a release uses the local registry with the `n = 999` local version
   (see Local and CI builds).
 - Tools that read the `vite` version. Vite's `version` export is rewritten in
   `packages/core/build.ts` to import `../../package.json` relative to Vite's
@@ -569,7 +612,7 @@ manifest `version` takes part in the check.
   `node_modules/vite/package.json` directly, such as
   `readInstalledRawViteVersion` in `vite-plus-bootstrap.ts`, keys on
   `name === '@voidzero-dev/vite-plus-core'` and reads `bundledVersions.vite`.
-  Third-party code that reads `version` from that file now sees `8.3.500`
+  Third-party code that reads `version` from that file now sees `8.3.5000`
   instead of `1.4.0`, which is closer to the truth; whether any such code
   gates features on it is unverified.
 - Binding guards. The Rolldown binding guard in core compares the installed
@@ -596,8 +639,8 @@ package has the same problem, and none is in scope.
 
 ## Decisions from review
 
-- Local and CI builds use the reserved `n = 99`, and the release counter stops
-  at 98 (see Local and CI builds).
+- Local and CI builds use the reserved `n = 999`, and the release counter stops
+  at 998 (see Local and CI builds).
 - Vite+ prereleases use the `vp.` identifier instead of a copy of the
   `vite-plus` prerelease identifiers (see Prereleases and new Vite majors).
 - The `vitest` peer rule is removed in a follow-up with its own fixture

From b74344f7f0d6b5a3ef1c2240fb033dff0e6042ef Mon Sep 17 00:00:00 2001
From: "MK (fengmk2)" 
Date: Tue, 10 Nov 2026 11:08:44 +0800
Subject: [PATCH 10/10] ci: read the local core version from the core manifest
 only

The local CI builds set the core version after the CLI build, so the CLI
constant and its toolchain manifest kept the committed version and
VP_CORE_VERSION had to correct the alias. Stamp the local core version
before the build in test-vp-create and ecosystem CI instead. The CLI build,
its toolchain manifest, and the packed `vite` alias then all read
packages/core/package.json, and VP_CORE_VERSION is removed. The native
build cache key does not include the core manifest.

Co-Authored-By: Claude Opus 5.5 
---
 .github/workflows/reusable-ecosystem-ci.yml   | 19 +++++++----
 .github/workflows/test-vp-create.yml          | 29 ++++++++--------
 .../cli/src/utils/__tests__/constants.spec.ts | 16 ---------
 packages/cli/src/utils/constants.ts           |  6 ++--
 rfcs/core-version-alignment.md                | 33 ++++++++++---------
 5 files changed, 48 insertions(+), 55 deletions(-)

diff --git a/.github/workflows/reusable-ecosystem-ci.yml b/.github/workflows/reusable-ecosystem-ci.yml
index 743a956650..36e943060c 100644
--- a/.github/workflows/reusable-ecosystem-ci.yml
+++ b/.github/workflows/reusable-ecosystem-ci.yml
@@ -68,6 +68,15 @@ jobs:
         with:
           name: windows-cli-binaries
 
+      # Stamp the local core version (release counter 999, for example
+      # 8.3.5999) before the build, so the CLI build, its toolchain manifest,
+      # and the packed `vite` alias all read it from packages/core/package.json
+      # (rfcs/core-version-alignment.md). The native build cache key does not
+      # include that file. The local version still encodes the bundled Vite,
+      # so peer checks run as they do for a release.
+      - name: Set local core version
+        run: (cd packages/core && npm pkg set version="$(node ../tools/src/core-version.ts local)")
+
       - name: Build with upstream
         uses: ./.github/actions/build-upstream
         with:
@@ -85,15 +94,11 @@ jobs:
           # and installs them like a real release, with every package manager
           # (including bun) resolving the standard
           # `vite -> npm:@voidzero-dev/vite-plus-core@` alias. Pin
-          # the CLI to 0.0.0 and core to its local version (release counter
-          # 999, for example 8.3.5999) so a correctly installed local build is
+          # the CLI to 0.0.0 (core already has its local version, see "Set
+          # local core version") so a correctly installed local build is
           # always distinguishable from any published version
           # (verify-install.ts asserts it), even on a release commit that
-          # leaves packages/{core,cli} at a published version. The local core
-          # version still encodes the bundled Vite, so peer checks run as they
-          # do for a release (rfcs/core-version-alignment.md).
-          CORE_VERSION=$(node packages/tools/src/core-version.ts local)
-          (cd packages/core && npm pkg set version="$CORE_VERSION")
+          # leaves packages/{core,cli} at a published version.
           (cd packages/cli && npm pkg set version=0.0.0)
           cd packages/core && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
           cd packages/cli && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
diff --git a/.github/workflows/test-vp-create.yml b/.github/workflows/test-vp-create.yml
index 8837a1bdd4..a8f46dfa2e 100644
--- a/.github/workflows/test-vp-create.yml
+++ b/.github/workflows/test-vp-create.yml
@@ -93,6 +93,15 @@ jobs:
           path: ./rolldown/packages/rolldown/src
           merge-multiple: true
 
+      # Stamp the local core version (release counter 999, for example
+      # 8.3.5999) before the build, so the CLI build, its toolchain manifest,
+      # and the packed `vite` alias all read it from packages/core/package.json
+      # (rfcs/core-version-alignment.md). The native build cache key does not
+      # include that file. The local version still encodes the bundled Vite,
+      # so peer checks run as they do for a release.
+      - name: Set local core version
+        run: (cd packages/core && npm pkg set version="$(node ../tools/src/core-version.ts local)")
+
       - name: Build with upstream
         uses: ./.github/actions/build-upstream
         with:
@@ -107,15 +116,11 @@ jobs:
           # and installs them like a real release, with every package manager
           # (including bun) resolving the standard
           # `vite -> npm:@voidzero-dev/vite-plus-core@` alias. Pin
-          # the CLI to 0.0.0 and core to its local version (release counter
-          # 999, for example 8.3.5999) so a correctly installed local build is
+          # the CLI to 0.0.0 (core already has its local version, see "Set
+          # local core version") so a correctly installed local build is
           # always distinguishable from any published version, even on a
           # release commit that leaves packages/{core,cli} at a published
-          # version. The local core version still encodes the bundled Vite,
-          # so peer checks run as they do for a release
-          # (rfcs/core-version-alignment.md).
-          CORE_VERSION=$(node packages/tools/src/core-version.ts local)
-          (cd packages/core && npm pkg set version="$CORE_VERSION")
+          # version.
           (cd packages/cli && npm pkg set version=0.0.0)
           cd packages/core && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
           cd packages/cli && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
@@ -184,8 +189,7 @@ jobs:
     env:
       # The local registry serves the packed 0.0.0 build, so create pins the
       # exact version like a real release. The vp binary was built before the
-      # pack step pinned the package versions, so align the version explicitly.
-      # The "Set local core version" step sets VP_CORE_VERSION the same way.
+      # pack step pinned the CLI version, so align the version explicitly.
       VP_VERSION: '0.0.0'
       # Force full dependency rewriting so the library template's existing
       # vite-plus dep gets overridden with the local build
@@ -209,9 +213,6 @@ jobs:
           name: vite-plus-packages
           path: tmp/tgz
 
-      - name: Set local core version
-        run: echo "VP_CORE_VERSION=$(node packages/tools/src/core-version.ts local)" >> "$GITHUB_ENV"
-
       - name: Install vp CLI
         run: |
           mkdir -p target/release
@@ -246,8 +247,8 @@ jobs:
           echo "$handshake"
           echo "$handshake" | node -e 'const { env } = JSON.parse(require("node:fs").readFileSync(0, "utf8")); for (const [k, v] of Object.entries(env)) console.log(`${k}=${v}`)' >> "$GITHUB_ENV"
 
-      # No VP_OVERRIDE_PACKAGES: with VP_CORE_VERSION set, the product default
-      # override map already pins `vite` to npm:@voidzero-dev/vite-plus-core@
+      # No VP_OVERRIDE_PACKAGES: the build read the local core version, so the
+      # product default override map already pins `vite` to npm:@voidzero-dev/vite-plus-core@
       # and `vitest` to the bundled version (see VITE_PLUS_OVERRIDE_PACKAGES in
       # packages/cli/src/utils/constants.ts), and stays correct across vitest
       # bumps without editing this workflow.
diff --git a/packages/cli/src/utils/__tests__/constants.spec.ts b/packages/cli/src/utils/__tests__/constants.spec.ts
index 42768cb67f..8ee08f7152 100644
--- a/packages/cli/src/utils/__tests__/constants.spec.ts
+++ b/packages/cli/src/utils/__tests__/constants.spec.ts
@@ -11,7 +11,6 @@ describe('Vite+ dependency versions', () => {
 
   it('uses the concrete CLI and core versions by default', async () => {
     vi.stubEnv('VP_VERSION', '');
-    vi.stubEnv('VP_CORE_VERSION', '');
     vi.stubEnv('VP_OVERRIDE_PACKAGES', '');
     vi.resetModules();
 
@@ -29,7 +28,6 @@ describe('Vite+ dependency versions', () => {
     'keeps the core manifest version for VP_VERSION %s',
     async (version) => {
       vi.stubEnv('VP_VERSION', version);
-      vi.stubEnv('VP_CORE_VERSION', '');
       vi.stubEnv('VP_OVERRIDE_PACKAGES', '');
       vi.resetModules();
 
@@ -39,20 +37,6 @@ describe('Vite+ dependency versions', () => {
     },
   );
 
-  it('uses VP_CORE_VERSION for the core alias', async () => {
-    vi.stubEnv('VP_VERSION', '0.0.0');
-    vi.stubEnv('VP_CORE_VERSION', '8.3.5999');
-    vi.stubEnv('VP_OVERRIDE_PACKAGES', '');
-    vi.resetModules();
-
-    const { VITE_PLUS_CORE_VERSION, VITE_PLUS_OVERRIDE_PACKAGES, VITE_PLUS_VERSION } =
-      await import('../constants.js');
-
-    expect(VITE_PLUS_VERSION).toBe('0.0.0');
-    expect(VITE_PLUS_CORE_VERSION).toBe('8.3.5999');
-    expect(VITE_PLUS_OVERRIDE_PACKAGES.vite).toBe('npm:@voidzero-dev/vite-plus-core@8.3.5999');
-  });
-
   it('preserves explicit prerelease overrides', async () => {
     const vitePlusUrl = 'https://pkg.pr.new/voidzero-dev/vite-plus@1891';
     const viteCoreUrl =
diff --git a/packages/cli/src/utils/constants.ts b/packages/cli/src/utils/constants.ts
index 9c0191b224..9c09edbcc3 100644
--- a/packages/cli/src/utils/constants.ts
+++ b/packages/cli/src/utils/constants.ts
@@ -10,9 +10,9 @@ export const VITE_PLUS_CORE_NAME = '@voidzero-dev/vite-plus-core';
 export const VITE_PLUS_CORE_ALIAS_PREFIX = `npm:${VITE_PLUS_CORE_NAME}@`;
 
 // The core version encodes the bundled Vite version (rfcs/core-version-alignment.md),
-// so it differs from the vite-plus version. Preview builds stamp the core manifest
-// before the build, so `corePkg.version` is already the preview version.
-export const VITE_PLUS_CORE_VERSION = process.env.VP_CORE_VERSION || corePkg.version;
+// so it differs from the vite-plus version. Release, preview, and local CI builds
+// set the core manifest version before the build, so this is the packed version.
+export const VITE_PLUS_CORE_VERSION = corePkg.version;
 
 // Mirrors Vite's DEFAULT_CONFIG_FILES order so readers and writers target the same file.
 export const VITE_CONFIG_FILES = [
diff --git a/rfcs/core-version-alignment.md b/rfcs/core-version-alignment.md
index 41ac5f5d18..6e642d717b 100644
--- a/rfcs/core-version-alignment.md
+++ b/rfcs/core-version-alignment.md
@@ -256,16 +256,16 @@ change.
 
 Code that assumes the two versions are equal must change:
 
-| Location                                                | Current behavior                                                      | Required change                                                                                                                                                 |
-| ------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `packages/cli/src/utils/constants.ts`                   | `VITE_PLUS_OVERRIDE_PACKAGES.vite` uses `VITE_PLUS_VERSION`           | Add `VITE_PLUS_CORE_VERSION = process.env.VP_CORE_VERSION \|\| corePkg.version`, read from `packages/core/package.json` at build time, and use it for the alias |
-| `packages/cli/src/resolve-core.ts`                      | Expects the core `version` to equal the installed `vite-plus` version | Parse the alias in the installed `vite-plus/package.json` `dependencies.vite`; skip the version check for a `workspace:` spec (repository layout)               |
-| `packages/cli/src/sync-versions/protocol.ts`, `plan.ts` | Maps `@voidzero-dev/vite-plus-core` to `toolchain.vitePlus`           | Add `vitePlusCore` from the `vite-plus-core` manifest node and map the core name to it                                                                          |
-| `packages/cli/src/migration/migrator/*`                 | Alias written through `VITE_PLUS_OVERRIDE_PACKAGES`                   | No change beyond the constant; the summary in `collectToolchainVersionChanges` already shows the raw Vite version                                               |
-| `ecosystem-ci/patch-project.ts`                         | `VP_OVERRIDE_PACKAGES.vite` uses the `vite-plus` tarball version      | Use the packed core version                                                                                                                                     |
-| `packages/tools/src/install-global-cli.ts`              | Maps the alias version to `voidzero-dev-vite-plus-core-.tgz` | No change; the alias already carries the core version after pack                                                                                                |
-| `crates/vp_global_cli/src/commands/version.rs`          | Reads `bundledVersions` from the installed core manifest              | No change                                                                                                                                                       |
-| `packages/cli/toolchain.config.json`                    | `vite-plus-core` node uses `core-package` as its version source       | No change; `vp toolchain` shows the core version and the bundled Vite version as separate nodes                                                                 |
+| Location                                                | Current behavior                                                      | Required change                                                                                                                                   |
+| ------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `packages/cli/src/utils/constants.ts`                   | `VITE_PLUS_OVERRIDE_PACKAGES.vite` uses `VITE_PLUS_VERSION`           | Add `VITE_PLUS_CORE_VERSION = corePkg.version`, read from `packages/core/package.json` at build time, and use it for the alias                    |
+| `packages/cli/src/resolve-core.ts`                      | Expects the core `version` to equal the installed `vite-plus` version | Parse the alias in the installed `vite-plus/package.json` `dependencies.vite`; skip the version check for a `workspace:` spec (repository layout) |
+| `packages/cli/src/sync-versions/protocol.ts`, `plan.ts` | Maps `@voidzero-dev/vite-plus-core` to `toolchain.vitePlus`           | Add `vitePlusCore` from the `vite-plus-core` manifest node and map the core name to it                                                            |
+| `packages/cli/src/migration/migrator/*`                 | Alias written through `VITE_PLUS_OVERRIDE_PACKAGES`                   | No change beyond the constant; the summary in `collectToolchainVersionChanges` already shows the raw Vite version                                 |
+| `ecosystem-ci/patch-project.ts`                         | `VP_OVERRIDE_PACKAGES.vite` uses the `vite-plus` tarball version      | Use the packed core version                                                                                                                       |
+| `packages/tools/src/install-global-cli.ts`              | Maps the alias version to `voidzero-dev-vite-plus-core-.tgz` | No change; the alias already carries the core version after pack                                                                                  |
+| `crates/vp_global_cli/src/commands/version.rs`          | Reads `bundledVersions` from the installed core manifest              | No change                                                                                                                                         |
+| `packages/cli/toolchain.config.json`                    | `vite-plus-core` node uses `core-package` as its version source       | No change; `vp toolchain` shows the core version and the bundled Vite version as separate nodes                                                   |
 
 The `vp upgrade` command in `crates/vp_global_cli/src/commands/upgrade/`
 installs the global CLI from the published `vite-plus` package and its
@@ -349,10 +349,13 @@ and peer checks fail against it. These flows would not test the path without a
 
 These flows will set core to `encodeCoreVersion(vite, 999)`, for example
 `8.3.5999` for Vite `8.3.5`. They keep `0.0.0` for the CLI, which
-`ecosystem-ci/verify-install.ts` and `test-vp-create.yml` assert. The CLI
-reads the core version from `VP_CORE_VERSION` or from the core manifest, so
-`vp create` and `vp migrate` in these flows write the `8.3.5999` alias and
-remove the `vite` peer rule.
+`ecosystem-ci/verify-install.ts` and `test-vp-create.yml` assert. They set the
+core version before the build, so the CLI build, its toolchain manifest, and
+the packed `vite` alias all read it from `packages/core/package.json`. The
+native build cache key does not include that file. `vp create` and
+`vp migrate` in these flows write the `8.3.5999` alias and remove the `vite`
+peer rule. No environment variable overrides the core version: the core
+manifest is the only source.
 
 `nextCoreVersion` never returns `n = 999`, and `release.yml` rejects it.
 Thus a local version never reaches the registry, and the local registry
@@ -368,7 +371,7 @@ cannot mix it up with a published version.
 | `packages/core/build.ts`                                                                                                         | Pass `cliPkgJson.version` instead of `pkgJson.version` to `rewriteRolldownBindingRequires`. The platform packages publish at the CLI version, and the guard must compare against that version (`packages/core/build-support/rewrite-rolldown-binding.ts`)                                                                                                                      |
 | `packages/cli/publish-native-addons.ts`                                                                                          | No change: it mirrors the CLI's platform pins into core by name and does not use the core version                                                                                                                                                                                                                                                                              |
 | `.github/workflows/publish-preview.yml`, `publish-preview-register.yml`                                                          | No change: the bridge serves every workspace package as `0.0.0-commit.`, and the stamp before `pnpm pack` keeps the alias on that version. The sticky comment keeps printing one version for both packages                                                                                                                                                                |
-| `.github/workflows/test-vp-create.yml`, `reusable-ecosystem-ci.yml`                                                              | Keep `0.0.0` for the CLI. Set core to `encodeCoreVersion(vite, 999)` and pass it as `VP_CORE_VERSION` where the workflow passes `VP_VERSION` (see Local and CI builds)                                                                                                                                                                                                         |
+| `.github/workflows/test-vp-create.yml`, `reusable-ecosystem-ci.yml`                                                              | Keep `0.0.0` for the CLI. Set core to `encodeCoreVersion(vite, 999)` before the build (see Local and CI builds)                                                                                                                                                                                                                                                                |
 | `packages/tools/src/local-npm-registry.ts`, `pack-local-vite-plus.ts`                                                            | No change: both serve the packed tarball's own version                                                                                                                                                                                                                                                                                                                         |
 | `.github/scripts/upgrade-deps.ts`                                                                                                | Reset the core version after a bundled Vite change                                                                                                                                                                                                                                                                                                                             |
 | `.github/renovate.json`                                                                                                          | No change                                                                                                                                                                                                                                                                                                                                                                      |