fix: Exclude peer dependencies from workspace external dep resolution (#12050)
## Summary
Fixes `turbo prune` producing invalid npm lockfiles when workspace
packages have peer dependencies satisfied by different versions across
apps.
Closes #10985
## Problem
When two apps depend on different versions of a package (e.g. `next@14`
and `next@15`), npm nests one under the app's `node_modules/` while
hoisting the other. After `turbo prune` removes the app that needed the
hoisted version, the pruned lockfile still has the old tree structure —
a nested version that should now be hoisted. `npm ci` rejects this as
inconsistent.
## Fix
`NpmLockfile::subgraph` now runs a `rehoist_packages` pass after
building the pruned package set. It detects packages nested under a
workspace's `node_modules/` whose hoisted counterpart was not explicitly
requested by any workspace's transitive closure, and promotes them to
the hoisted position with sub-dependencies relocated.
The condition `!requested.contains(hoisted_key)` is what distinguishes
the issue-10985 case (hoisted v14 pulled in only by a peer dep from the
wrong path context — not in any workspace's real transitive closure)
from the npm-lock case (hoisted `@types/react` v18 genuinely needed by
`web`'s transitive closure).
One file changed: `crates/turborepo-lockfiles/src/npm.rs` (+86 lines).
## Testing
Verified locally against all relevant fixtures:
- `issue-10985`: all 3 targets PASS (app-one, app-two, @repo/components)
- `npm-peer-dep`: all 3 PASS
- `pnpm-peer-dep`: all 3 PASS
- `npm-lock → web`: `@types/react` stays at v18.0.17 (no incorrect
rehoisting)