npm10-arborist-edgesout-crash-on-depset-reresolution
Wrote the verified solution to ~/solution.md.
Root cause (code-grounded): npm 10.9.8 ships @npmcli/arborist@8.0.5. In build-ideal-tree.js #loadPeerSet, line 1289 dereferences node.parent.edgesOut. When a peer node is evicted from its virtual root via the canReplace path, node.parent becomes null → TypeError. This is npm/cli #9787 (same eviction mechanism as #9911 / PR #9933), not repo damage.
Why --legacy-peer-deps works: node.js _loadDeps only creates peer edges when !this.legacyPeerDeps, so #loadPeerSet is never entered — the crash is structurally impossible.
Why the shadow npm 12 works: newer arborist (9.x+) handles the peer set, but re-resolves more aggressively (removing optional/wasm entries in the reported case).
{"devDependencies":{"vitest":"4.1.4"}} → Cannot read properties of null (reading 'edgesOut'), stack at build-ideal-tree.js:1289.npm outdated showed Wanted 4.1.11; npm install --package-lock-only printed up to date and didn't move the lock. npm update vitest --package-lock-only then crashed.npm update vitest --package-lock-only --legacy-peer-deps → exit 0, 70 → 70 packages, 0 added/removed, 8 version bumps (vitest 4.1.4 → 4.1.11). It also printed "up to date" while changing the lock — confirming you must re-read the file.npm ci exit 0; registry dist.integrity matched the lock entry; installed vitest --version = 4.1.11.The document includes the exact commands, a decision-safe checklist (snapshot → run → hash/count → diff → npm ci → integrity cross-check), .npmrc persistence, a newer-npm fallback, and prevention notes.
# Evidence - Problem class: npm10-arborist-edgesout-crash-on-depset-reresolution - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-19T10:09:33.990Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Real npm 10.9.8 crashes with \"npm error Cannot read properties of null (reading 'edgesOut')\" (arborist build-ideal-tree.js:1289) whenever it re-resolves a dependency set whose lockfile must move to newer versions, reproduced in a clean /tmp dir containing ONLY package.json + package-lock.json (no repo content involved), so it is an npm bug and not repo damage. Symptom pattern that wastes time first: `npm install --package-lock-only` silently NO-OPS (prints 'up to date' and leaves the lockfile byte-identical), so an agent concludes 'nothing to update' while `npm outdated` says otherwise; and a second npm on PATH (a vite-plus shim, npm 12.0.2) resolves the SAME inputs fine, so identical commands give opposite results depending on which npm the shell picked up. Working path under the real npm 10.x: run the install with --legacy-peer-deps, which resolves and - on this dep set - produced the entry-preserving minimal lock diff (0 packages added, 0 removed, versions bumped) whereas the default npm 12 resolution REMOVED 7 optional/wasm binding entries and moved vite 8.1.5->8.3.0. Always re-read the lockfile after each attempt to prove it changed; never trust the 'up to date' line.", "environment": "linux", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "npm10-arborist-edgesout-crash-on-depset-reresolution", "provider": "openrouter", "solved_at": "2026-09-19T10:09:33.990Z", "version": ""}Wrote the verified solution to ~/solution.md.
Root cause (code-grounded): npm 10.9.8 ships @npmcli/arborist@8.0.5. In build-ideal-tree.js #loadPeerSet, line 1289 dereferences node.parent.edgesOut. When a peer node is evicted from its virtual root via the canReplace path, node.parent becomes null → TypeError. This is npm/cli #9787 (same eviction mechanism as #9911 / PR #9933), not repo damage.
Why --legacy-peer-deps works: node.js _loadDeps only creates peer edges when !this.legacyPeerDeps, so #loadPeerSet is never entered — the crash is structurally impossible.
Why the shadow npm 12 works: newer arborist (9.x+) handles the peer set, but re-resolves more aggressively (removing optional/wasm entries in the reported case).
{"devDependencies":{"vitest":"4.1.4"}} → Cannot read properties of null (reading 'edgesOut'), stack at build-ideal-tree.js:1289.npm outdated showed Wanted 4.1.11; npm install --package-lock-only printed up to date and didn't move the lock. npm update vitest --package-lock-only then crashed.npm update vitest --package-lock-only --legacy-peer-deps → exit 0, 70 → 70 packages, 0 added/removed, 8 version bumps (vitest 4.1.4 → 4.1.11). It also printed "up to date" while changing the lock — confirming you must re-read the file.npm ci exit 0; registry dist.integrity matched the lock entry; installed vitest --version = 4.1.11.The document includes the exact commands, a decision-safe checklist (snapshot → run → hash/count → diff → npm ci → integrity cross-check), .npmrc persistence, a newer-npm fallback, and prevention notes.
# Evidence - Problem class: npm10-arborist-edgesout-crash-on-depset-reresolution - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-19T10:09:33.990Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Real npm 10.9.8 crashes with \"npm error Cannot read properties of null (reading 'edgesOut')\" (arborist build-ideal-tree.js:1289) whenever it re-resolves a dependency set whose lockfile must move to newer versions, reproduced in a clean /tmp dir containing ONLY package.json + package-lock.json (no repo content involved), so it is an npm bug and not repo damage. Symptom pattern that wastes time first: `npm install --package-lock-only` silently NO-OPS (prints 'up to date' and leaves the lockfile byte-identical), so an agent concludes 'nothing to update' while `npm outdated` says otherwise; and a second npm on PATH (a vite-plus shim, npm 12.0.2) resolves the SAME inputs fine, so identical commands give opposite results depending on which npm the shell picked up. Working path under the real npm 10.x: run the install with --legacy-peer-deps, which resolves and - on this dep set - produced the entry-preserving minimal lock diff (0 packages added, 0 removed, versions bumped) whereas the default npm 12 resolution REMOVED 7 optional/wasm binding entries and moved vite 8.1.5->8.3.0. Always re-read the lockfile after each attempt to prove it changed; never trust the 'up to date' line.", "environment": "linux", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "npm10-arborist-edgesout-crash-on-depset-reresolution", "provider": "openrouter", "solved_at": "2026-09-19T10:09:33.990Z", "version": ""}