Repository navigation
fix(devx): npm 10 crashes with edgesOut on the vendor-export probe and the blank scaffold — the probe installs with a pinned npm 11, the template overrides vitest - #23799
Conversation
…d npm 11 The --resolve leg measures a governed vendor's export surface, but it installed each scratch project with the runner's npm. npm 10's arborist crashes in #loadPeerSet (`edgesOut` of null) on the upstream peer graph vitest@4.1.12 introduced (reached through better-auth's optional vitest peer), so the leg read PREREQUISITE NOT MET on every run without measuring anything. The probe now fetches npm@11.21.0 into its scratch directory and installs every vendor with it. --legacy-peer-deps was measured to drop @better-auth/core's required peers (a false "does not load" finding), and --omit=peer does not avoid the crash. Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t Co-authored-by: Claude <noreply@anthropic.com>
npm 10's arborist crashes (`edgesOut` of null) installing a fresh blank
scaffold: optional `vitest` peers deeper in the closure resolve to
vitest@4.1.12, whose peer set contradicts itself. The template now routes
every vitest edge in the npm graph to the project's own declared range
with `"overrides": { "vitest": "$vitest" }`. npm 11 already dedupes the
tree to that vitest, and pnpm ignores the field.
Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t
Co-authored-by: Claude <noreply@anthropic.com>
…nge, not $vitest `"$vitest"` installs on npm 10 too, but npm then stops hoisting @objectstack/metadata-core and nests 17 copies of it (measured on 10.9.9 and 11.21.0). The literal `~4.0.0` installs the same tree npm 11 installs without any override; npm refuses it loudly (EOVERRIDE) if it ever differs from the direct dependency, which the template test pins. Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t Co-authored-by: Claude <noreply@anthropic.com>
…ride Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check
What this run could not see
Coarse fallback — 1 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
席位核对 · PATCH ROUND · PR #23799 (draft, head
|
…inned npm 11 node:22-slim bundles npm 10.9.9, whose arborist crashes with `edgesOut` on `npm install -g @objectstack/cli pg mysql2` since vitest@4.1.12 was published, and a global install reads no `overrides`. The install RUN now fetches npm@11.21.0 into /tmp with the image's npm, puts it first on PATH for that RUN only, asserts its version, and deletes it in the same layer, so the image keeps node:22-slim's npm and the installed set is unchanged. check-docs-image-tag anchors the driver install on `npm install -g` after a `RUN` or `&&` leader. The installer fetch (no `-g`) is not an anchor, and the self-test pins that the driver line is the one read. Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t Co-authored-by: Claude <noreply@anthropic.com>
席位核对 · ACCEPT · PR #23799 (draft, head
|
Fixes #23788
Clause-②: no
The Version Packages PR #23481 was red on two lockfile-less npm installs. Once those passed, a third install failed: the runtime image's global install. All three crash npm 10 with
npm error Cannot read properties of null (reading 'edgesOut'). This PR fixes all three onmain:--resolveprobe now installs with a pinned npm 11;"overrides": { "vitest": "~4.0.0" };docker/Dockerfile) now runs itsnpm install -gunder a pinned npm 11 (see Patch round below).Nothing here touches
changeset-release/main, a workflow,.npmrc,pnpm-workspace.yamlorpnpm-lock.yaml. The monorepo's own pnpm install is unaffected.The trigger
vitest@4.1.12, published 2026-10-15T14:51:24Z under theV4dist-tag:@vitest/*peer of 4.1.12 is pinned to exactly4.1.12, but it declares"@vitest/browser-webdriverio": "^4.1.11".@vitest/browser-webdriverio@4.1.12was never published; the newest 4.x is 4.1.11, last modified 2026-09-08."vitest": "4.1.11"exactly. The peer set therefore contradicts itself.node.parent(null) in#loadPeerSet(build-ideal-tree.js:1289on npm 10.9.4).Both installs reach vitest only through OPTIONAL peers:
better-auth@1.7.3peersvitest: ^2 || ^3 || ^4, and@objectstack/metadata-corepeersvitest: ^4.0.0. On npm 10, each resolves to vitest 4.1.12.Bisected with npm's
--before, npm 10.9.9, each leg in an empty directory with a fresh--cache:--before=2026-10-15T08:36:00Z--before=2026-10-15T14:50:00Z--before=2026-10-15T14:52:00Znpm install better-auth@1.7.3 --no-package-lock(the probe)edgesOut@objectstack/*set tolatestedgesOutThe issue's hypothesis holds in kind (an upstream publish in the window brings back npm 10's
#loadPeerSetcrash). The path is different from #22912's: there the rootvitestreachedvitest@*through vite 8's devtools peers. Here an optional peer meets one release's inconsistent peer set. The npm side has no fix: 10.9.9 is the newest npm 10. Upstream has no fix either: re-checked at the time of this PR, there is still no@vitest/browser-webdriverio@4.1.12and novitest@4.1.13.Before: reproduced
Node 22, each leg in an empty directory outside any checkout with a fresh
--cache:npm install better-auth@1.7.3 --no-package-lock --no-audit --no-fundin the probe'spackage.jsonedgesOutedgesOutpnpm check:vendor-export-contract-resolveat the base commitedgesOutmain's stamp^18.0.0-next.1edgesOut; fallback exit 1edgesOut^18.0.0-next.2(ETARGET, then fallback tolatest= 17.7.0)edgesOut(template withlatest)edgesOutlatest)Isolated manifests on npm 10.9.9, for reach:
{"dependencies":{"@objectstack/plugin-auth":"latest"}},{"dependencies":{"@objectstack/metadata-core":"latest"}}and theos initplugin manifest (@objectstack/spec,@objectstack/cli,typescript ^5.3.0,vitest ~4.0.0) each exit 1 withedgesOut. The blank template with itsvitestdevDependency removed still crashes, so the template's own vitest range is not the path in, and no range change there can fix it.The repair, and why each route
(ii) The probe:
scripts/check-vendor-export-contract.mjs.--resolvemeasures a vendor's export surface. It does not measure peer resolution. The probe now fetchesnpm@11.21.0(PROBE_INSTALLER_NPM, exact, at least two weeks old) into its scratch directory once per run, using the runner's npm. npm bundles its dependencies, so that install walks no peer graph. Every vendor install then runs under it.VERDICT: PASS — vendor export contract (registry resolution, installed with npm@11.21.0).Measured alternatives, not taken:
--legacy-peer-depsalso skips REQUIRED peers.@better-auth/core@1.7.3requiresbetter-call,jose,kyselyand others, and@better-auth/core/contextthen fails to import (Cannot find package 'better-call', npm 10.9.9). That would be a false "does not load" red.--omit=peer --omit=optionalstill exits 1: the crash happens while the ideal tree is built.The self-test now pins two things: the installer is exact with major 11 or later, and the probe's install argv is whole, so a flag that changes which packages land cannot creep in.
(i) The blank template:
packages/create-objectstack/src/templates/blank/package.json. A user-facing manifest is affected.npx create-objectstackfalls back to npm when pnpm is absent, and Node 22 bundles npm 10, so every such user's firstnpm installcrashed. The template now ships"overrides": { "vitest": "~4.0.0" }, equal to its owndevDependencies.vitest. It routes everyvitestedge in the npm graph to the project's declared range.name@versionfor every package) is identical to npm 11.21.0's without the override: 566 packages onmain's stamp, 563 on the fallback, 0 diff lines across npm 10.9.4, 10.9.9, 11.21.0 and 11.21.1.overrides(measured on pnpm 10.28.0), so pnpm installs are unchanged."$vitest". It also installs, but npm then stops hoisting@objectstack/metadata-coreand nests 17 copies of it (measured on npm 10.9.4, 10.9.9 and 11.21.0; on 10.9.9, a scoped{"@objectstack/metadata-core":{"vitest":"$vitest"}}and a version-keyed override do the same).EOVERRIDE: Override for vitest@^4.1.0 conflicts with direct dependency, measured on npm 10.9.9 and 11.21.0. The new template test pinsoverrides.vitest === devDependencies.vitest, so the two move together. The changeset tells users the same.(iii) Workflows: not needed. All three repairs are in non-governed files, so this PR touches no governed surface.
After
pnpm check:vendor-export-contract-resolve(HEAD, fresh npm cache)main's stamp, the CI step replayed^18.0.0-next.2, via the fallbackThe after-PASS lines:
✓ @better-auth/core/context @ @better-auth/core@1.7.3 (via "1.7.3", 2 symbol(s))and✓ better-auth/adapters @ better-auth@1.7.3 (via "1.7.3", 1 symbol(s)).Tests
pnpm --filter create-objectstack test: 18 files, 275 tests, passing.pnpm --filter create-objectstack typecheck: exit 0, after building the dependency closure.tsc --listFilesincludestemplate-consistency.test.ts.node scripts/check-vendor-export-contract.mjs --self-test: OK.node scripts/sync-scaffold-emission-policy.mjs --checkandnode scripts/sync-template-versions.mjs --check: exit 0.scripts/ablation-replace.mjswith an anchor that must hit, restored fromHEADand proven by blob hash:overridesblock deleted (on-disk count 1 → 0, JSON still parses),template-consistency.test.tsreads1 failed | 38 passed, and the failure is the new case;39 passed.src/templatesfrom disk, so nodist/is involved.pnpm check:docs-image-tag(patch round): self-test118 assertions, thenOK,2 driver(s) installed by docker/Dockerfile compared against 2 promised in docker/README.md (2 matched by name).Patch round: the runtime image (
docker/Dockerfile)With the probe and the scaffold fixed,
Scaffold with repo dist(job 116797775725, headc9134933e) got as far as building the official runtime image from this checkout. It then failed at line 69 of that file,RUN npm install -g @objectstack/cli@${OS_CLI_VERSION} "pg@^8.0.0" "mysql2@^3.0.0" && npm cache clean --force, with the sameedgesOutcrash onnode:22-slim's npm.overrides, so round 1 could not reach this line.docker-publish.ymlships this image, so the same line would also have broken the release's image publish.The change. The same RUN now does four things:
npm@11.21.0into/tmp/os-installer-npmwith the image's own npm. npm bundles its dependencies, so that fetch walks no peer graph.PATHfor that RUN only, and checkstest "$(npm --version)" = "11.21.0".npm install -g @objectstack/cli@${OS_CLI_VERSION} "pg@^8.0.0" "mysql2@^3.0.0".npm cache clean --force, then deletes the installer in the same layer.The version is the one
PROBE_INSTALLER_NPMholds for the same crash. The installed packages and their ranges are unchanged, and the image still shipsnode:22-slim's npm.Why this mechanism. Measured inside
node:22-slim(node v22.23.3, npm 10.9.9, global prefix/usr/local):/tmp, the fetched npm 11 resolves first onPATHand uses the same global prefix:npm config get prefixgives/usr/localandnpm root -ggives/usr/local/lib/node_modules. So the install lands where the image's npm would put it.npm install -g npm@…replaces the image's npm for every user of the image.npx npm@…leaves 20 MB in~/.npm/_npxafternpm cache clean --force(measured).--legacy-peer-depsalso skips required peers (measured on the probe).The gate.
pnpm check:docs-image-taganchored the driver line on the first trimmed line startingRUN npm install -g. The new RUN opens with the installer fetch, so on the new Dockerfile the old parse went red withDRIVER-ANCHOR-LOST(measured). The new parse:npm install -gafter aRUNor&&leader;&&;-g.On this tree it reads line 90 (
&& npm install -g \) and findspg@^8.0.0andmysql2@^3.0.0, matching 2 against 2 indocker/README.md.docker/README.mdis unchanged. The new self-test cases, on the image's exact shape:npm@11.21.0is not read as a driver;DRIVER-RANGE-MISMATCH);npm install -g npm@…ahead of the drivers is read as the install line and refused (DRIVER-UNDOCUMENTED), never skipped into a green.pnpm check:docs-image-tagexits 0, self-test118 assertions.Readings
Setup. A private
dockerd29.8.2 in this container.node:22-slimcame from the GCR and ECR mirrors of Docker Hub. Both serve index digestsha256:c3de60bf…; Docker Hub itself answered 429.NODE_EXTRA_CA_CERTS, so npm inside the build can reach the registry.docker/as CI uses it, and--no-cache.OS_CLI_VERSION=17.7.0OS_CLI_VERSION=18.0.0-next.2c9134933e's Dockerfilenpm error Cannot read properties of null (reading 'edgesOut')d19cc5888's Dockerfileadded 525 packagesadded 528 packages(on a working copy whose RUN was byte-identical; only a comment changed before the commit)--before=2026-10-15T14:50:00Zadded 525 packagesAfter image (
d19cc5888, 17.7.0) against the pre-trigger control:os --version@objectstack/cli/17.7.0 linux-x64 node-v22.23.3npm --version/usr/local/bin/npm)@objectstack corepack mysql2 npm pgpg/mysql2, resolved from the image'sdriver-sqlpg@8.23.1,mysql2@3.24.6docker-publish.yml's smoke:os --version, thennode -e "require('pg'); require('mysql2')"from the global tree/tmp/os-installer-npmname@version)The 3 version differences were each published after the control's cutoff:
@mongodb-js/saslprep@1.6.1(10-15 19:13Z),argparse@3.0.3(21:58Z) andjs-yaml@5.5.0(23:28Z). Neither image contains vitest.USER root, thenRUN npm install -g tedious) runs on the image's own npm 10.9.9 and installs on the after image (exit 0).docker-publish.ymlbuilds. The mechanism is architecture-independent JavaScript.Gates (local, on head
d19cc5888)origin/main(74b436263). On the branch tree,dispatch-gatesflagged it as STALE: 9 of the files it derives from changed onmainsince the branch point. So the union ran on a scratch commit,29e3f931e:origin/mainwith this branch merged locally and never pushed. It differs frommainby exactly this PR's 6 files, the same shape as the merge ref CI tests.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstacknamed 96 commands: round 1's 95, pluspnpm check:docs-image-tag.--ran:96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN.node scripts/pm/check-governed-merges.mjs --branch claude/issue-23788-npm10-edgesout(the register on both the branch tree andmain's): 0 of 6 paths hit the register. NOT governed, 335 changed lines.d19cc5888(one read at 04:27Z): all 43 check runs completed. Every one issuccessor skipped by its selector, includingScaffold with repo dist,Validate Package Dependencies,Check Changeset,Lint & Repo GatesandTest Core.What #23481 needs afterwards
Nothing is pushed to
changeset-release/main.release.yml'sversion-prjob regenerates that PR on its 6-hourlyschedule(cron0 */6 * * *), or on the relay'srefresh_version_pr: truedispatch. It does not run on a push tomain. Once this lands, the next regeneration carries all three fixes. A re-run of the PR's bot-triggered checks is a maintainer act. A plain re-run of the existing red run reuses that run's old commit, so it cannot see this fix.main's ownschedulerun ofvalidate-deps.yml, run 38843597390 at 03:03Z on46ca793edb, is red at the same step: "Verify the declared ranges cannot resolve past our import surface", exit 3, which is PREREQUISITE NOT MET. So this is not specific to the release branch.Acceptance notes
These are related findings, not fixed here:
os init/os createemit the same unfixed manifest class. Theos initplugin manifest crashes npm 10.9.9 withedgesOut(measured). Those emitters buildpackage.jsoninpackages/cli/src/commands/init.ts/create.ts, andcreate.tsis outside this claim's surface.@objectstack/*package reaching@objectstack/metadata-coreor@objectstack/plugin-authcrash the same way until upstream publishes a consistent vitest 4.x (measured:@objectstack/plugin-auth@latestand@objectstack/metadata-core@latesteach alone). npm 11 installs them.overrides.vitestcopiesSCAFFOLD_VITEST_RANGEby hand, guarded by the new test. APOLICY_STAMPSrow inscripts/sync-scaffold-emission-policy.mjswould generate it. That script is outside this claim's surface.content/docs/deployment/self-hosting.mdxoffers an "equivalent" Dockerfile,FROM node:22-slimplusRUN npm install -g @objectstack/cli@17.7.0. On that image's npm 10.9.9 the RUN exits 1 withedgesOut(measured). Its host recipe,npm install -g @objectstack/cli, is the same install. The file is outside this claim's surface.PREREQUISITE NOT METexits callprocess.exitinside thetry, so the scratch directory under the OS temp dir is not removed on those paths.Generated by Claude Code