fix(resolver): make shared-children missing-peer reuse independent of resolution order#12514
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthrough
ChangesDeterministic Peer Resolution
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches🧪 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 |
PR Summary by QodoFix non-deterministic transitive peer suffix by removing timing-based missing-peers reuse Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
Context used 1. Test-driven internal export
|
|
Code review by qodo was updated up to the latest commit 824c3da |
824c3da to
eebfd99
Compare
|
Code review by qodo was updated up to the latest commit eebfd99 |
Benchmark Results
Run 27821711939 · 30 runs per scenario · triggered by @zkochan |
eebfd99 to
4c6dc24
Compare
|
Code review by qodo was updated up to the latest commit 4c6dc24 |
1 similar comment
|
Code review by qodo was updated up to the latest commit 4c6dc24 |
|
See the benchmark results. Looks like this makes the "Isolated linker: fresh add new dep, hot cache + hot store" scenario significantly slower. We probably still need to do the change, if it improves correctness and determinism, but we should think about maybe ways to make it faster. |
|
I tried to reproduce the Running the same harness CI usesUsing
Both run-orders, so it isn't warmup drift. A separate full Why HEAD can't be doing more workThe dropped clause only affects the non-owner reuse branch in Instrumenting
So HEAD consistently does equal-or-slightly-less resolution work for an identical result. What I think the CI number wasThe benchmark runs on SuggestionSince the regression doesn't reproduce and the resolver provably does less work, I don't think a perf change is warranted here. Could we re-trigger the benchmark workflow for a second data point? If it still shows a regression on Written by an agent (Claude Code, claude-opus-4-8). |
… resolution order
`claimChildrenResolution` lets a non-owner occurrence of a shared package
reuse the owner's `missingPeersOfChildren`. The reuse condition was
existing.owner.depth >= opts.currentDepth || existing.missingPeersOfChildren.resolved
The second clause made reuse depend on whether the owner's resolution had
already settled by the time this node was claimed. Under concurrent
resolution that settling time varies run to run, so a deeper consumer would
inherit the owner's missing peers on some runs but not others. For a package
like `next`, whose dependency styled-jsx declares an optional `@babel/core`
peer, that flipped `@babel/core` in and out of the resolved peer suffix
between otherwise identical installs, churning the lockfile and producing
intermittent `pnpm dedupe --check` failures.
Drop the `.resolved` clause: reuse the owner's missing peers only when the
owner is at the same or a deeper depth, which is a function of the dependency
graph rather than completion order.
The `.resolved` flag was introduced in #5467 (closing #5454)
to avoid a deadlock: with auto-install-peers in a workspace, a shared package
reached through multiple importers could await its own not-yet-settled
`missingPeersOfChildren` promise and hang. The flag let the code reuse that
promise only once settled, otherwise re-resolve. Removing the clause is
strictly more conservative — a shallower owner's promise is now never reused,
settled or not, so no unsettled promise is ever awaited and the deadlock
cannot return. The #5454 regression test ("installation on a
workspace with many complex circular dependencies does not fail when auto
install peers is on") still passes, as do the peer, dedupe, and cyclic
suites. The deterministic owner selection added in #12362 made
shared-package ownership order-independent but had moved this timing branch
in verbatim without neutralizing it.
pacquet needs no matching change: it resolves a non-owner's children lazily
and per-occurrence during peer resolution (`realize_children`), with no
equivalent settled-promise timing gate, so it is deterministic by
construction here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Is it possible to cover this with a test? And if yes, also add the same test to pacquet, even if it works as expected now |
4c6dc24 to
09b5433
Compare
|
Code review by qodo was updated up to the latest commit 09b5433 |
1 similar comment
|
Code review by qodo was updated up to the latest commit 09b5433 |
Add regression coverage for the determinism fix in #12514. pnpm: a focused unit test on `claimChildrenResolution`. The mock-registry e2e harness serializes resolution through the depth barrier (`waitForPackageResolutionTurn`), so it cannot reproduce the completion-order race end to end. The changed line is testable directly, though: a deeper consumer of a shared package must never inherit a strictly shallower owner's `missingPeersOfChildren`, even when that owner's promise has already settled (`resolved = true`) — the only run-to-run-variable input the dropped clause keyed on. The test fails on the pre-fix condition and passes after. A second case pins the retained same-depth structural sharing. `claimChildrenResolution` is exported so the test can drive it. The function's only external inputs are fields on a minimal `ResolutionContext`. pacquet: a parity test that builds the same shape — a shared `styled-jsx` occurrence whose optional `@babel/core` peer is provided by a sibling at a shallow depth but absent at a deeper one. pacquet resolves the tree in a single deterministic walk, so the deeper occurrence keeps the bare suffix while the shallow one carries `(@babel/core@7.0.0)`. Rebuilding the tree (fresh `HashMap`s) on each of 16 iterations guards against hashing order leaking into the graph the way completion order did upstream. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Yes — done in b19c3f5. pnpm. The end-to-end mock-registry harness still can't reproduce the flake: it serializes resolution through the depth barrier ( pacquet. Same scenario, expressed at the level pacquet resolves at: a shared Written by an agent (Claude Code, claude-opus-4-8). |
The perfectionist::bare-url dylint flagged the parity-check comment's bare PR URL under -D warnings. Wrap it in angle brackets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 similar comment
|
@coderabbitai resume |
✅ Action performedReviews resumed. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #12514 +/- ##
==========================================
- Coverage 88.14% 88.14% -0.01%
==========================================
Files 314 314
Lines 43559 43559
==========================================
- Hits 38397 38395 -2
- Misses 5162 5164 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Code review by qodo was updated up to the latest commit 7202435 |
Integrated-Benchmark Report (Linux)Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 3.58121869738,
"stddev": 0.18538156775491088,
"median": 3.50062788498,
"user": 3.8750497599999996,
"system": 2.0790732,
"min": 3.39853485098,
"max": 3.90737059798,
"times": [
3.5125954359800002,
3.42880965798,
3.8573191169800003,
3.44021715698,
3.4886603339800004,
3.90737059798,
3.71554672398,
3.4480315369800003,
3.6151015619800004,
3.39853485098
]
},
{
"command": "pacquet@main",
"mean": 3.594245937880001,
"stddev": 0.22877024655356243,
"median": 3.54689953898,
"user": 3.8658455599999995,
"system": 2.0543262,
"min": 3.2890915719800002,
"max": 3.9865354389800003,
"times": [
3.9865354389800003,
3.2890915719800002,
3.4052262309800003,
3.4299107599800003,
3.8136801029800003,
3.47657651498,
3.8554145609800003,
3.4262796039800003,
3.6425220309800004,
3.6172225629800003
]
},
{
"command": "pnpr@HEAD",
"mean": 1.9149450253800002,
"stddev": 0.15370446564739537,
"median": 1.88496811148,
"user": 2.8831070599999995,
"system": 1.8137563999999997,
"min": 1.75068719498,
"max": 2.1550885609800003,
"times": [
2.1550885609800003,
2.13222377698,
1.75068719498,
1.93205998198,
1.84559599498,
1.7692384079799999,
1.7949870669799999,
1.92434022798,
1.78148071098,
2.06374832998
]
},
{
"command": "pnpr@main",
"mean": 1.96362618508,
"stddev": 0.160475793466554,
"median": 1.96260997648,
"user": 2.8985471599999997,
"system": 1.8034993,
"min": 1.71602321298,
"max": 2.21546259098,
"times": [
2.2023569599800004,
1.83991807898,
1.82371768198,
2.03249047598,
1.9996217319799998,
1.99710837198,
2.21546259098,
1.71602321298,
1.8814511649799999,
1.92811158098
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.46362338674000003,
"stddev": 0.010296231818236506,
"median": 0.46018548064000003,
"user": 0.37434978,
"system": 0.78483088,
"min": 0.45294506064,
"max": 0.48368352164,
"times": [
0.48368352164,
0.47907050164000003,
0.45601803564000004,
0.45767189064,
0.45837585264,
0.46199510864000004,
0.46290458364000003,
0.46762992664,
0.45593938564000003,
0.45294506064
]
},
{
"command": "pacquet@main",
"mean": 0.4733073533399999,
"stddev": 0.011321123999557996,
"median": 0.47116971014000003,
"user": 0.3798210799999999,
"system": 0.7803454799999999,
"min": 0.46141337864000004,
"max": 0.48706826464,
"times": [
0.48361558164,
0.46279223564,
0.48515957764,
0.46225726164000003,
0.47855669364000003,
0.46364611264,
0.46378272664000003,
0.48706826464,
0.48478170064000004,
0.46141337864000004
]
},
{
"command": "pnpr@HEAD",
"mean": 0.57006381644,
"stddev": 0.09192466908287077,
"median": 0.51512165264,
"user": 0.39104387999999995,
"system": 0.80295668,
"min": 0.48240875864000005,
"max": 0.72374137664,
"times": [
0.51685340964,
0.51338989564,
0.5108038546400001,
0.48240875864000005,
0.49095836264000003,
0.6849745236400001,
0.72374137664,
0.6500372376400001,
0.6317724496400001,
0.49569829564
]
},
{
"command": "pnpr@main",
"mean": 0.49880939253999995,
"stddev": 0.012423963787321668,
"median": 0.49449740714,
"user": 0.3878591799999999,
"system": 0.7962367799999999,
"min": 0.48369726164000004,
"max": 0.5152148496400001,
"times": [
0.51449576364,
0.49728614164,
0.5095902096400001,
0.5152148496400001,
0.49013322364,
0.49170867264,
0.48483659264,
0.49056645564,
0.5105647546400001,
0.48369726164000004
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.01794396254,
"stddev": 0.044013015560698865,
"median": 4.006837190840001,
"user": 4.0284491000000004,
"system": 2.0972571799999997,
"min": 3.9531614093400003,
"max": 4.094106147340001,
"times": [
3.9997182213399998,
3.9531614093400003,
4.01831827034,
4.094106147340001,
3.98386556834,
3.99025732734,
4.0436211473400006,
4.012962674340001,
4.082717152340001,
4.000711707340001
]
},
{
"command": "pacquet@main",
"mean": 4.00775326564,
"stddev": 0.04367261038319079,
"median": 4.01834257084,
"user": 4.050701699999999,
"system": 2.0700961799999997,
"min": 3.93566261334,
"max": 4.05327183134,
"times": [
4.01515257334,
3.93566261334,
3.94160186634,
4.0215325683400005,
3.98582693534,
4.0463230903400005,
4.04933684834,
4.04170476434,
4.05327183134,
3.98711956534
]
},
{
"command": "pnpr@HEAD",
"mean": 1.97277772724,
"stddev": 0.15353561573366584,
"median": 1.9598801403400001,
"user": 2.7226472999999998,
"system": 1.76079838,
"min": 1.75930076734,
"max": 2.2511325373399997,
"times": [
2.00091895734,
2.1004654343399998,
1.94914227834,
2.1204937733399998,
1.81614732134,
1.75930076734,
1.9706180023400002,
2.2511325373399997,
1.81734964734,
1.94220855334
]
},
{
"command": "pnpr@main",
"mean": 2.11025618804,
"stddev": 0.10574271800963318,
"median": 2.1044396178399998,
"user": 2.6989381999999997,
"system": 1.7700796799999998,
"min": 1.91582453334,
"max": 2.25490784534,
"times": [
1.9997978273400001,
2.23106148234,
2.12849984534,
2.21899483434,
2.07094438034,
2.25490784534,
2.10370435734,
2.07365189634,
1.91582453334,
2.10517487834
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.1550678856799999,
"stddev": 0.013229961869302172,
"median": 1.15157244038,
"user": 1.34085074,
"system": 1.02538278,
"min": 1.14271937688,
"max": 1.18336185288,
"times": [
1.16034997288,
1.15475992288,
1.14507664488,
1.14883670688,
1.17106376688,
1.14559657288,
1.18336185288,
1.14460586588,
1.15430817388,
1.14271937688
]
},
{
"command": "pacquet@main",
"mean": 1.17518811658,
"stddev": 0.09125266625947039,
"median": 1.14595709188,
"user": 1.33441444,
"system": 1.03705988,
"min": 1.12860541688,
"max": 1.43330017788,
"times": [
1.14613185088,
1.14604733688,
1.14586684688,
1.16306015288,
1.12860541688,
1.16224877188,
1.13932205488,
1.43330017788,
1.14162638888,
1.14567216788
]
},
{
"command": "pnpr@HEAD",
"mean": 0.48962150038,
"stddev": 0.005080341198382143,
"median": 0.48745687388000003,
"user": 0.34125054,
"system": 0.7545125799999999,
"min": 0.48501781488,
"max": 0.50056076488,
"times": [
0.49485673388,
0.48566462188000004,
0.48501781488,
0.48741744988,
0.49258969488000004,
0.50056076488,
0.48658790688000003,
0.48749629788000004,
0.48520172288,
0.49082199588000003
]
},
{
"command": "pnpr@main",
"mean": 0.5018740772799999,
"stddev": 0.010339440300965173,
"median": 0.49812853888,
"user": 0.34582373999999994,
"system": 0.7600728799999998,
"min": 0.49299771788,
"max": 0.5207951938800001,
"times": [
0.50252948688,
0.49885141088,
0.52069468188,
0.5207951938800001,
0.49523705188,
0.49299771788,
0.49650991588000004,
0.49740566688000004,
0.49382007988000004,
0.49989956688000003
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.86124332228,
"stddev": 0.037317750181058595,
"median": 2.8458546570800003,
"user": 1.8305473799999998,
"system": 1.2101905,
"min": 2.81283014758,
"max": 2.94320154058,
"times": [
2.90503433358,
2.8439843605800004,
2.8462884445800003,
2.81283014758,
2.84143102158,
2.8454208695800003,
2.86981845758,
2.86098224458,
2.84344180258,
2.94320154058
]
},
{
"command": "pacquet@main",
"mean": 2.88384369098,
"stddev": 0.029890202591214235,
"median": 2.87455981658,
"user": 1.8349770799999998,
"system": 1.2262219999999997,
"min": 2.8403672825800004,
"max": 2.93610762058,
"times": [
2.85901764358,
2.86248014558,
2.8403672825800004,
2.86923416758,
2.91792431458,
2.87988546558,
2.8685837595800003,
2.90647280658,
2.8983637035800003,
2.93610762058
]
},
{
"command": "pnpr@HEAD",
"mean": 0.5038358027800001,
"stddev": 0.009842756483349289,
"median": 0.50177373258,
"user": 0.34095668,
"system": 0.7795753,
"min": 0.49417674158,
"max": 0.5300725685800001,
"times": [
0.49992659658,
0.5072658215800001,
0.49820565258000005,
0.49932365658,
0.5024271025800001,
0.5031564755800001,
0.50112036258,
0.5300725685800001,
0.5026830495800001,
0.49417674158
]
},
{
"command": "pnpr@main",
"mean": 0.4929381340800001,
"stddev": 0.00536334706160918,
"median": 0.49445192108,
"user": 0.34238778000000003,
"system": 0.7530076000000001,
"min": 0.48408037958,
"max": 0.49986889058,
"times": [
0.49986889058,
0.48664718758000003,
0.49806744158000005,
0.49477093858000004,
0.49261832658000004,
0.49533363258,
0.49711983358,
0.49413290358,
0.48408037958,
0.48674180658000005
]
}
]
} |
|
| Branch | pr/12514 |
| 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 | 4,017.94 ms(-4.89%)Baseline: 4,224.53 ms | 5,069.44 ms (79.26%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 2,861.24 ms(-4.62%)Baseline: 2,999.99 ms | 3,599.99 ms (79.48%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,155.07 ms(-13.11%)Baseline: 1,329.33 ms | 1,595.20 ms (72.41%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 3,581.22 ms(-13.19%)Baseline: 4,125.34 ms | 4,950.41 ms (72.34%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 463.62 ms(-25.43%)Baseline: 621.76 ms | 746.12 ms (62.14%) |
|
| Branch | pr/12514 |
| Testbed | pnpr |
⚠️ WARNING: No Threshold found!Without a Threshold, no Alerts will ever be generated.
Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the--ci-only-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 1,972.78 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 503.84 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 489.62 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 1,914.95 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 570.06 ms |
|
Good overall but the comments don't adhere the styleguide we have in this repo. See the latest styleguide on main branch and instructions related to docs in code |
…ior code The comments on the determinism fix narrated the removed `.resolved` clause and the bug it caused, which belongs in the commit history rather than the source. Rewrite the source and test comments to state the current invariant — reuse of a shared package's missing-peers promise is gated on the owner's depth alone — and the failure mode each test guards, per the repository comment style guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed in 812db5f. The comments violated the "don't record past implementation shape / refactor history / 'the previous code did X' framing" rule in the comment style guide — they narrated the removed
The full historical rationale (the Written by an agent (Claude Code, claude-opus-4-8). |
|
Code review by qodo was updated up to the latest commit 812db5f |
1 similar comment
|
Code review by qodo was updated up to the latest commit 812db5f |
|
Code review by qodo was updated up to the latest commit 2cbfb73 |
1 similar comment
|
Code review by qodo was updated up to the latest commit 2cbfb73 |
Summary
The problem
Peer resolution is non-deterministic in one case: an optional transitive peer can be pulled into — or left out of — a package's resolved peer-dependency suffix depending on resolution-completion order, not on the dependency graph. The result is lockfile churn and intermittent
pnpm dedupe --checkfailures in CI, even when runningpnpm dedupelocally reports nothing to change.A concrete, public instance of the shape:
nextdepends onstyled-jsx, andstyled-jsxdeclares@babel/coreas an optional peer (@babel/coreunderpeerDependenciesMetaas optional — verifiable vianpm view styled-jsx peerDependenciesMeta).@babel/coreis present elsewhere in a workspace (e.g. pulled by a Babel/Storybook/React-Native toolchain in another project), whether a givennextoccurrence resolves it as a transitive peer — i.e. whether its dep path becomesnext@x(@babel/core@y)…or justnext@x(…)— varies between otherwise identical installs.Root cause
The non-determinism is in
claimChildrenResolution. When a shared package's children are resolved by one occurrence (the "owner") and a deeper occurrence reuses them, the reuse condition was:The second clause keys the decision on whether the owner's
missingPeersOfChildrenpromise has settled yet at the moment this node is claimed. Under concurrent resolution that settling time varies run to run, so a deeper consumer inherits the owner's missing peers on some runs and not others. That is exactly what flips an optional transitive peer like@babel/corein and out of the suffix.The deterministic owner selection added in #12362 (via
compareChildrenResolutionOwners) made which occurrence owns a shared package order-independent — but it did not neutralize this.resolvedtiming branch, which predates it.Why the clause existed
The
.resolvedflag was introduced in #5467 (closing #5454, "don't crash when auto-install-peers is true in a workspace"). Withauto-install-peersin a workspace, a shared package reached through multiple importers could end up awaiting its own not-yet-settledmissingPeersOfChildrenpromise — a deadlock. The flag let the resolver reuse that promise only once it had settled, and otherwise re-resolve children instead of awaiting. #12362 later moved this guard verbatim intoclaimChildrenResolutionwithout neutralizing its timing-dependence.The fix
Drop the
.resolvedclause: reuse the owner's missing peers only when the owner is at the same or a deeper depth — a function of the dependency-graph structure rather than completion order. This is strictly more conservative than the original guard: a shallower owner's promise is now never reused (settled or not), so no unsettled promise is ever awaited and the #5454 deadlock cannot return. The #5454 regression test (installation on a workspace with many complex circular dependencies does not fail when auto install peers is on) still passes.How to verify the analysis
claimChildrenResolution: the only run-to-run-variable input to the branch isexisting.missingPeersOfChildren.resolved; everything else (owner.depth,currentDepth,parentIds) is structural..resolvedis set when the owner's children finish resolving (see where it is assigned), so its value at claim time depends on concurrent ordering.resolvePeers,cyclicPeerDeterminism,peerDependencies, and thepeers/hoist/dedupe/transitive/autoInstallPeersinstall tests), including the regressions from #12286 and #11999.Testing note
I was not able to add a failing-then-passing unit test. The race only manifests under real concurrent network I/O; the mock-registry test harness serializes resolution through the depth barrier (
waitForPackageResolutionTurn) and produces deterministic output regardless of response timing, so it does not reproduce the flake. The fix was validated against a large real workspace: before,dedupe --checkflipped between clean and "changes" across cold runs; after, 30/30 cold runs produced a byte-identical result. Guidance on an acceptable deterministic test (e.g. a scheduler-injection seam) would be welcome.Update: test coverage added in b19c3f5. The mock-registry harness still can't reproduce the flake, but the changed line is testable directly. A unit test on
claimChildrenResolutionasserts a deeper consumer never reuses a strictly shallower owner'smissingPeersOfChildreneven when that promise has already settled — the run-to-run-variable input the dropped clause keyed on. It fails with the old clause restored and passes after. pacquet gets the parity version of the same scenario (sharedstyled-jsxwhose optional@babel/corepeer is provided by a sibling at a shallow depth but absent deeper), asserted stable across 16 fresh-HashMaprebuilds.Squash Commit Body
Checklist
pacquet/port, or the description notes what still needs porting. — pnpm-only. pacquet needs no change: it resolves a non-owner's children lazily and per-occurrence during peer resolution (realize_childreninpacquet/crates/resolving-deps-resolver/src/resolve_peers.rs), with no settled-promise timing gate, so it is deterministic by construction here.Written by an agent (Claude Code, claude-opus-4-8).
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests