Conversation
…they surface When two peers form a mutual cycle (e.g. vite ↔ @vitejs/devtools) and both hit `findHit` cache instead of running their own `calculateDepPath`, the cycle detected at the parent level had no participant to short-circuit it. Sibling resolutions at that level now drop their cyclic peers into the `name@version` form so the cached path promises can release. Closes #11999.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📜 Recent review details⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
📝 WalkthroughWalkthroughFixes a hang in peer-dependency resolution when aliased installs create transitive mutual peer cycles by expanding cycle detection to include pending peer aliases; adds Jest and Rust regression tests and a changeset documenting the patch release. ChangesCyclic Aliased Peer Resolution
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…er install Pacquet's `resolve_peers` walks synchronously with an `in_progress` set and `find_hit` returns already-realized `DepPath` values, so the cross- cache cycle from #11999 doesn't deadlock here. Add a regression test using the same shape — aliased root `a@npm:a-real`, two siblings that each pull half of an `x` ↔ `y` mutual-peer pair, peer-back to the aliased root — so the parity is locked in.
Micro-Benchmark ResultsLinux |
Review Summary by QodoFix hang on cyclic aliased peer dependency resolution
WalkthroughsDescription• Fix hang in peer resolution for aliased installs with transitive mutual-peer cycles • Expand cycle detection to include pending peers, not just current alias • Add regression tests in pnpm and pacquet to prevent future regressions • Cycles detected at parent level now properly short-circuit via sibling resolutions Diagramflowchart LR
A["Aliased Install<br/>a@npm:a-real"] -->|depends on| B["Sibling b-real"]
A -->|depends on| C["Sibling c-real"]
B -->|depends on| X["Package x"]
C -->|depends on| Y["Package y"]
X -->|peer cycle| Y
Y -->|peer cycle| X
B -->|peer-depends on| A
C -->|peer-depends on| A
X -->|auto-installed| Root["Root Level"]
Y -->|auto-installed| Root
Root -->|cycle detection<br/>now includes pending peers| Fix["Cycle Short-circuit"]
File Changes1. installing/deps-resolver/src/resolvePeers.ts
|
Integrated-Benchmark Report (Linux)Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.11435775132,
"stddev": 0.14856382552826278,
"median": 2.10603139152,
"user": 2.7016877599999995,
"system": 3.2587299599999993,
"min": 1.9633633340200003,
"max": 2.5076455810200002,
"times": [
2.11214681902,
2.02489671902,
2.10934864202,
2.12564033102,
2.0438145370200003,
2.1027141410200003,
2.12751677502,
2.5076455810200002,
2.02649063402,
1.9633633340200003
]
},
{
"command": "pacquet@main",
"mean": 2.04187280572,
"stddev": 0.0628812740769117,
"median": 2.0476228705199997,
"user": 2.71750186,
"system": 3.2550897599999997,
"min": 1.9541448220200002,
"max": 2.13569589002,
"times": [
2.13569589002,
1.9541448220200002,
1.99689717002,
2.05790255302,
2.10346437402,
1.9753709080200001,
1.9786397210200002,
2.08735473602,
2.0919146950200003,
2.03734318802
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.68309063694,
"stddev": 0.01364711768224537,
"median": 0.6780006111400001,
"user": 0.36889757999999995,
"system": 1.35358026,
"min": 0.6720011611400001,
"max": 0.7136484751400001,
"times": [
0.7136484751400001,
0.6872831921400001,
0.67895859014,
0.68128746514,
0.67704263214,
0.67231011214,
0.67277063914,
0.67601565214,
0.6720011611400001,
0.69958845014
]
},
{
"command": "pacquet@main",
"mean": 0.7052314787400001,
"stddev": 0.031239755478996597,
"median": 0.69468873314,
"user": 0.36832107999999997,
"system": 1.3581152600000002,
"min": 0.6686941591400001,
"max": 0.7594529001400001,
"times": [
0.7517586891400001,
0.7594529001400001,
0.6686941591400001,
0.6836533921400001,
0.6875939281400001,
0.71114044214,
0.68804991914,
0.7245429081400001,
0.7013275471400001,
0.6761009021400001
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.3676020200800005,
"stddev": 0.03053077079885649,
"median": 2.36630651268,
"user": 3.90672468,
"system": 3.01635742,
"min": 2.31083567118,
"max": 2.40491830418,
"times": [
2.40491830418,
2.3326913411800003,
2.3955082131800003,
2.31083567118,
2.35544186018,
2.3923060511800003,
2.39681082918,
2.36684341618,
2.36576960918,
2.35489490518
]
},
{
"command": "pacquet@main",
"mean": 2.36658683928,
"stddev": 0.03187844118965516,
"median": 2.3639317121800003,
"user": 3.9154443799999994,
"system": 3.03868112,
"min": 2.30228466718,
"max": 2.42205585018,
"times": [
2.30228466718,
2.42205585018,
2.39894894118,
2.37767615518,
2.36008164918,
2.36778177518,
2.35962829218,
2.3772496591800003,
2.35267766218,
2.34748374118
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.51112777728,
"stddev": 0.016467763991670065,
"median": 1.5059646888800002,
"user": 1.7220167599999996,
"system": 1.79260488,
"min": 1.49601282638,
"max": 1.5486514463800003,
"times": [
1.5295626313800001,
1.5118939013800001,
1.51275446238,
1.5486514463800003,
1.4987482793800002,
1.49601282638,
1.50111406938,
1.51063760738,
1.5006107783800002,
1.5012917703800002
]
},
{
"command": "pacquet@main",
"mean": 1.55209041458,
"stddev": 0.04898651921074414,
"median": 1.5385126908800002,
"user": 1.7279414599999998,
"system": 1.8254855799999998,
"min": 1.50908131638,
"max": 1.6695398703800002,
"times": [
1.55878838138,
1.5484929173800002,
1.6695398703800002,
1.50908131638,
1.51352954338,
1.5247082743800002,
1.52014144138,
1.5976956393800001,
1.5285324643800002,
1.5503942973800002
]
}
]
} |
|
| Branch | pr/12018 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 2,367.60 ms(+1.12%)Baseline: 2,341.38 ms | 2,809.66 ms (84.27%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,511.13 ms(+2.84%)Baseline: 1,469.43 ms | 1,763.32 ms (85.70%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 2,114.36 ms(+3.24%)Baseline: 2,048.01 ms | 2,457.61 ms (86.03%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 683.09 ms(+1.59%)Baseline: 672.43 ms | 806.91 ms (84.65%) |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs (1)
833-865: ⚡ Quick winTrim prose that re-narrates the test flow.
The new doc/inline comments mostly restate setup and assertions already visible in the test body. Please keep only the minimal non-obvious rationale to reduce doc drift.
Proposed simplification
-/// Regression test for <https://github.com/pnpm/pnpm/issues/11999>. -/// -/// The TypeScript fix (`installing/deps-resolver/src/resolvePeers.ts`) -/// broadens which cycles `calculateDepPath` short-circuits. Pacquet's -/// `resolve_peers` walks synchronously with an `in_progress` set, so -/// the deadlock that hit pnpm does not occur here — but the scenario -/// has to keep terminating with a graph entry for the aliased root and -/// for each pair of mutually-peer-depending leaves. -/// -/// Layout (from the upstream bug): an aliased install `a@npm:a-real` -/// pulls in `b-real` and `c-real`. Each of those depends on one half -/// of a mutual-peer pair (`x` ↔ `y`) and peer-depends on the aliased -/// root (`a@npm:a-real`). The hoist loop auto-installs `x` and `y` at -/// the importer level, where their cycle surfaces. +/// Regression guard for <https://github.com/pnpm/pnpm/issues/11999>: +/// aliased root + transitive mutual-peer cycle must terminate and keep expected graph entries. #[tokio::test] async fn aliased_install_with_transitive_mutual_peer_cycle_terminates() { let mut table = HashMap::new(); - // Root install: `a@npm:a-real@1.0.0`.As per coding guidelines: “Tests are documentation. Do not duplicate them in prose.”
Also applies to: 868-917
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs` around lines 833 - 865, Condense the long doc comment that precedes the #[tokio::test] regression test for issue 11999 (the block that describes the aliased install scenario) so it no longer restates the test setup and assertions; keep a single short sentence stating the test's purpose (regression for `#11999`) and, if needed, one brief non-obvious rationale or the minimal layout summary (1–2 lines). Apply the same trimming to the subsequent comment lines that cover the test body (the section noted as also applying to 868–917). Edit the comment block surrounding the test (the prose before the test attribute) rather than the test code or helper functions like aliased_fake_result; remove redundant narrative while preserving the issue reference and any essential context.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs`:
- Around line 833-865: Condense the long doc comment that precedes the
#[tokio::test] regression test for issue 11999 (the block that describes the
aliased install scenario) so it no longer restates the test setup and
assertions; keep a single short sentence stating the test's purpose (regression
for `#11999`) and, if needed, one brief non-obvious rationale or the minimal
layout summary (1–2 lines). Apply the same trimming to the subsequent comment
lines that cover the test body (the section noted as also applying to 868–917).
Edit the comment block surrounding the test (the prose before the test
attribute) rather than the test code or helper functions like
aliased_fake_result; remove redundant narrative while preserving the issue
reference and any essential context.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: cc30ec62-8f8f-465f-bd37-544e86f335c0
📒 Files selected for processing (4)
.changeset/fix-cyclic-aliased-peer-hang.mdinstalling/deps-installer/test/install/cyclicPeerDeterminism.tsinstalling/deps-resolver/src/resolvePeers.tspacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: ubuntu-latest / Node.js 24 / Test
- GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Follow Standard Style with trailing commas, preferring functions over classes, and declaring functions after they are used (relying on hoisting)
Use a single options object instead of multiple parameters when a function needs more than two or three arguments
Follow Import Order: Standard libraries first, then external dependencies (alphabetically), then relative imports
Write self-documenting code where function names, parameters, and types explain what a function does without requiring prose comments
Do not write comments that restate what the code already says; refactor via renaming, splitting helpers, or restructuring instead
Do not repeat documentation at call sites that already exists in JSDoc on the callee; update JSDoc once for all call sites to benefit
Use JSDoc only for a function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the body
Do not record past implementation shape, refactor history, or 'the previous code did X' framing in code; use git log and git blame instead
Write comments only when: the reason for code is non-obvious (hidden invariant, workaround for known bug, deliberate exception), or the right name doesn't fit (temporary technical constraint)
Files:
installing/deps-resolver/src/resolvePeers.tsinstalling/deps-installer/test/install/cyclicPeerDeterminism.ts
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: When porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(), orstreamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plainStringor&str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only viaTryFrom<String>and/orFromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallibleFrom<String>(andFrom<&str>when convenient).
If upstream occasionally constructs without validation, exposefrom_str_uncheckedas an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization.
Use#[derive(derive_more::From)]and#[derive(derive_more::Into)]for mechanical conversion impls. Fall back to manualimplonly when conversion needs custom logic.
String-literal unions should becomeenums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Pathover&PathBuf,&strover&String) when it doesn't force extra copies.
PreferArc::clone(&x)/Rc::clone(&x)overx.clone()for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;. Two forms stay allowed: external-crate preludes likeuse rayon::prelude::*;and root-of-module re-...
Files:
pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
🧠 Learnings (7)
📚 Learning: 2026-05-26T21:01:06.666Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11966
File: .changeset/require-tarball-integrity.md:6-6
Timestamp: 2026-05-26T21:01:06.666Z
Learning: In pnpm lockfile-related release notes/docs (especially changeset markdown), preserve URL hostnames exactly as they appear in pnpm-lock.yaml tarball resolution entries—keep hosts like `codeload.github.com`, `bitbucket.org`, and `gitlab.com` in lowercase. Do not “correct” them to title-case/preserve brand capitalization (e.g., LanguageTool rules like `GITHUB` capitalization) because these are literal URL fragments, not platform brand names.
Applied to files:
.changeset/fix-cyclic-aliased-peer-hang.md
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.
Applied to files:
installing/deps-resolver/src/resolvePeers.tsinstalling/deps-installer/test/install/cyclicPeerDeterminism.ts
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
📚 Learning: 2026-05-24T21:10:50.292Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:10:50.292Z
Learning: When implementing dependency “revisit”/optional-folding in pacquet, mirror pnpm’s `resolveDependencies.ts` behavior: on revisit, update the `optional` flag only for the directly visited package, and do not automatically change `optional` for transitive descendants. pnpm corrects stale optional flags later via `copyDependencySubGraph` (a BFS in `lockfile/pruner/src/index.ts`/`copyDependencySubGraph`). Until pacquet has an equivalent pruner/copy-subgraph step, be aware that the raw `node.optional` can flow through to the lockfile/virtual store via `dependencies_graph_to_lockfile.rs` → `create_virtual_store.rs` → `installability.rs`, so review changes that affect optional flag propagation for this mismatch.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
📚 Learning: 2026-05-24T21:10:50.292Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:10:50.292Z
Learning: In pacquet’s Rust resolver layer, the behavior of `ResolvedPackage.optional` “AND-folding”/revisit updates is intentionally aligned with pnpm: when a package is directly revisited, only that package’s `optional` flag is updated; transitive descendants are not re-walked/rescored in this stage. pnpm later corrects any stale optional flags downstream via its pruner/BFS logic (`copyDependencySubGraph` + `nonOptional` reachability stamping in the lockfile pruner). Since pacquet does not yet have that pruner equivalent, review should NOT flag the downstream/stale optional propagation as a bug in pacquet PRs; treat the resolver-layer optional propagation gap as expected parity until the pruning/stamping step is ported.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs
🔇 Additional comments (3)
.changeset/fix-cyclic-aliased-peer-hang.md (1)
1-7: LGTM!installing/deps-installer/test/install/cyclicPeerDeterminism.ts (1)
120-221: LGTM!installing/deps-resolver/src/resolvePeers.ts (1)
558-567: LGTM!
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #12018 +/- ##
==========================================
+ Coverage 88.21% 88.29% +0.08%
==========================================
Files 228 228
Lines 28777 28942 +165
==========================================
+ Hits 25385 25555 +170
+ Misses 3392 3387 -5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Actionable comments posted: 0 |
Benchmark Results
Run 26570835797 · 10 runs per scenario · triggered by @zkochan |
|
Thanks! 🙏 |
Summary
pnpm i nuxt@npm:nuxt-nightly@5x(and similar aliased installs) hung at 0% CPU during peer resolution afterresolved N, reused 0, downloaded N, added 0.resolvePeers.calculateDepPathonly short-circuited cycles whose members includedcurrentAlias. When two peers form a mutual cycle (e.g.vite↔@vitejs/devtools) and both hit thefindHitcache instead of running their owncalculateDepPath, the cycle surfaced at a level where no participant could break it — a sibling'scalculateDepPathsaw the cycle in thecyclesargument but kept awaitingpathsByNodeIdPromiseson cyclic peer node IDs.cyclicPeerAliasesto also include any cycle that intersects the current call's pending peers, so awaiting siblings emit thename@versionpeer id and the cached promise gets released.resolve_peerswalks synchronously with anin_progressset and returns already-realizedDepPathvalues fromfind_hit, so the deadlock does not occur there. A pacquet regression test locks in that the aliased-install + transitive-mutual-peer scenario terminates with the expected graph entries.Closes #11999.
Test plan
aliased install with a transitive mutual-peer cycle should not hangincyclicPeerDeterminism.ts— verified to fail with a 15 s timeout without the pnpm-side fix and to pass with it.aliased_install_with_transitive_mutual_peer_cycle_terminatesinpacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs— exercises the same shape againstresolve_importerand asserts the aliased root + auto-installed peers + leaf graph entries.pnpm i nuxt@npm:nuxt-nightly@5xcompletes in ~2 s instead of hanging.pnpm i nuxt-nightly@5x(the un-aliased baseline) still completes in ~2 s.pacquet installof the samenuxt@npm:nuxt-nightly@5xpackage.json completes successfully.@pnpm/installing.deps-resolverpass.pacquet-resolving-deps-resolverpass (incl. the new regression test).installing/deps-installer/test/install/peerDependencies.tspass.cyclicPeerDeterminism.ts,deepRecursive.ts, andinstall should not hang on circular peer dependenciesinmisc.tsall pass.Written by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit
#11999).