fix(pacquet): per-level preferred-version fold + all-importers hoist rounds#12357
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughResolve tree now seeds per-level preferred-version overlays by resolving direct children first, then walks grandchildren with a layered overlay; adds PreferredVersionsOverlay, threads it into resolver opts and npm pick logic, extends WantedKey caching, refactors node resolution to seed/pending, and updates importer hoist flow and tests. ChangesPer-level preferred-versions overlay mechanism
Sequence Diagram(s)sequenceDiagram
participant TreeWalk as Dependency Tree Walk
participant SeedResolver as resolve_node_seed
participant Cache as WantedKey Cache
participant NpmResolver as NPM Resolver
rect rgba(200,150,255,0.5)
Note over TreeWalk: Phase 1 — resolve all direct children to seed level overlay
TreeWalk->>SeedResolver: resolve_node_seed(child, pick_overlay=None)
SeedResolver->>Cache: lookup WantedKey(overlay_versions=[])
Cache-->>SeedResolver: miss / hit
SeedResolver->>NpmResolver: resolve (no overlay)
NpmResolver-->>SeedResolver: NodeSeed::Done / NodeSeed::Pending
SeedResolver-->>TreeWalk: collect seeds
end
rect rgba(150,200,255,0.5)
Note over TreeWalk: Phase 2 — compute level_versions, layer overlay, walk grandchildren
TreeWalk->>TreeWalk: compute level_versions -> layer overlay
loop per-child's children
TreeWalk->>SeedResolver: resolve_node_seed(grandchild, pick_overlay=layered)
SeedResolver->>Cache: lookup WantedKey(overlay_versions=[...])
Cache-->>SeedResolver: miss / hit
SeedResolver->>NpmResolver: resolve (with merged overlay selectors)
NpmResolver-->>SeedResolver: result respects sibling-preferred version
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
Micro-Benchmark ResultsLinux |
Code Review by Qodo
1. Wrong tuple field check
|
PR Summary by QodoFold per-level resolved versions into preferred versions during tree resolution WalkthroughsDescription• Resolve each sibling level before walking subtrees to match pnpm per-level preference folding. • Thread a per-level preferred-version overlay into npm version picking and dedup cache keys. • Add regression test ensuring children prefer a parent-level sibling’s resolved version. Diagramgraph TD
A["deps-resolver: extend_tree"] --> B["resolve_node_seed"] --> C["level_versions"] --> D["PreferredVersionsOverlay"] --> E["walk_node_children"] --> F["npm resolver pick"] --> G["overlay_merged_selectors"]
High-Level AssessmentThe following are alternative approaches to this PR: 1. Clone and mutate preferred_versions per level
2. Keep overlay outside ResolveOptions (walker-only hint)
3. Do not include overlay view in wanted-dedup cache key
Recommendation: Current approach is the best tradeoff: it matches pnpm semantics via an explicit per-level barrier, keeps layering O(1) with Arc chaining, and preserves correctness by keying memoization on the overlay’s per-name view (while avoiding allocations when overlays don’t apply). File ChangesEnhancement (4)
Bug fix (3)
Tests (1)
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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.
Inline comments:
In `@pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs`:
- Around line 973-974: The new per-level overlay is being created from None so
the caller's ResolveDependencyTreeOptions.base_opts.preferred_versions_overlay
is dropped; change construction of children_overlay (where
PreferredVersionsOverlay::layer(None, level_versions(ctx, &seeds)) is called) to
compose the existing opts.preferred_versions_overlay with the per-level layer
(e.g., layer(existing_overlay, level_versions(...))) and propagate this combined
overlay into the WantedKey creation and into the pick_overlay application (the
code paths around WantedKey and pick_overlay at the earlier noted block). Ensure
the same combined overlay is used both for building the cache key for
resolved_by_wanted and passed into the resolver call so descendant resolution
and caching see the caller's seeded preferences.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 411a7e70-ab1e-4919-ad52-11ad85ab1533
📒 Files selected for processing (8)
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-npm-resolver/src/lib.rspacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/resolving-npm-resolver/src/preferred_overlay.rspacquet/crates/resolving-resolver-base/src/lib.rspacquet/crates/resolving-resolver-base/src/resolve.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). (5)
- GitHub Check: ubuntu-latest / Node.js 24 / Test
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Lint and Test (ubuntu-latest)
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plainStringor&strin Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too viaTryFrom<String>and/orFromStrand do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallibleFrom<String>constructor
If upstream TypeScript occasionally constructs a branded string without validation, exposefrom_str_uncheckedin Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization
Derive simple conversions for branded strings using#[derive(derive_more::From)]and#[derive(derive_more::Into)]instead of handwritingimplblocks; use manualimplonly when conversion needs custom logic
Model TypeScript string literal unions (like'auto' | 'always' | 'never') as Rustenums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like`${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide inCODE_STYLE_GUIDE.md— imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Choose owned vs. borrowed parameters to minimize copies; widen to t...
Files:
pacquet/crates/resolving-npm-resolver/src/lib.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/resolving-npm-resolver/src/preferred_overlay.rspacquet/crates/resolving-resolver-base/src/lib.rspacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rspacquet/crates/resolving-resolver-base/src/resolve.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
🧠 Learnings (6)
📚 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-npm-resolver/src/lib.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/resolving-npm-resolver/src/preferred_overlay.rspacquet/crates/resolving-resolver-base/src/lib.rspacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rspacquet/crates/resolving-resolver-base/src/resolve.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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-npm-resolver/src/lib.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/resolving-npm-resolver/src/preferred_overlay.rspacquet/crates/resolving-resolver-base/src/lib.rspacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rspacquet/crates/resolving-resolver-base/src/resolve.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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-npm-resolver/src/lib.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/resolving-npm-resolver/src/preferred_overlay.rspacquet/crates/resolving-resolver-base/src/lib.rspacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rspacquet/crates/resolving-resolver-base/src/resolve.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.
Applied to files:
pacquet/crates/resolving-npm-resolver/src/lib.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/resolving-npm-resolver/src/preferred_overlay.rspacquet/crates/resolving-resolver-base/src/lib.rspacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rspacquet/crates/resolving-resolver-base/src/resolve.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
🔇 Additional comments (7)
pacquet/crates/resolving-resolver-base/src/resolve.rs (1)
148-195: LGTM!Also applies to: 255-258
pacquet/crates/resolving-resolver-base/src/lib.rs (1)
32-36: LGTM!pacquet/crates/resolving-npm-resolver/src/lib.rs (1)
34-34: LGTM!pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs (1)
12-27: LGTM!pacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rs (1)
202-208: LGTM!pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs (1)
368-374: LGTM!pacquet/crates/resolving-deps-resolver/src/tests.rs (1)
2754-2874: LGTM!
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.97916423128,
"stddev": 0.1502081399506422,
"median": 3.93701308558,
"user": 4.333532,
"system": 2.22290896,
"min": 3.83472565508,
"max": 4.293412545080001,
"times": [
4.293412545080001,
3.9481461480799998,
3.87050708308,
3.8498119590799997,
3.87951061508,
4.09924356508,
3.92588002308,
3.95023870908,
3.83472565508,
4.140166010080001
]
},
{
"command": "pacquet@main",
"mean": 3.9665521024799992,
"stddev": 0.14244583874539535,
"median": 3.97019072108,
"user": 4.2847304,
"system": 2.20418496,
"min": 3.80283432308,
"max": 4.19771516808,
"times": [
4.10118596708,
4.19771516808,
3.80283432308,
3.98701226608,
3.9920993990799998,
3.83796643808,
3.95336917608,
4.1349352470800005,
3.81526223308,
3.8431408070799997
]
},
{
"command": "pnpr@HEAD",
"mean": 1.9388999354799998,
"stddev": 0.13481684432708335,
"median": 1.9264244765800003,
"user": 2.8745751000000004,
"system": 1.84106216,
"min": 1.71854031008,
"max": 2.1592903830799997,
"times": [
1.9314992020800001,
2.1076967130799997,
1.9213497510800002,
2.04840706108,
1.71854031008,
1.8749723690800002,
1.8088949840800002,
1.9469084140800001,
1.87144016708,
2.1592903830799997
]
},
{
"command": "pnpr@main",
"mean": 1.95962570538,
"stddev": 0.12174868733580592,
"median": 1.9397534840800001,
"user": 2.8690433,
"system": 1.8349169599999997,
"min": 1.7692296470800002,
"max": 2.11186912208,
"times": [
2.11186912208,
1.91654845308,
1.8212016340800001,
2.05316573208,
1.7692296470800002,
1.9629585150800002,
1.8768197650800003,
2.07170242208,
2.10739608908,
1.9053656740800002
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.48434549122000004,
"stddev": 0.00846134352934324,
"median": 0.48393622862,
"user": 0.37788739999999993,
"system": 0.7863315,
"min": 0.47115597212,
"max": 0.49430550512,
"times": [
0.47115597212,
0.47753069712,
0.48140215112,
0.47631971012,
0.49391604212,
0.49430550512,
0.49238819412,
0.49169350512,
0.48647030612000003,
0.47827282912
]
},
{
"command": "pacquet@main",
"mean": 0.48947918662,
"stddev": 0.008593936692058638,
"median": 0.48903950562,
"user": 0.38049920000000004,
"system": 0.7849109000000001,
"min": 0.47760471412,
"max": 0.5014342311200001,
"times": [
0.48331872512,
0.49436176112,
0.48448251512,
0.49823021712,
0.49193176912000003,
0.47866755412,
0.48614724212,
0.5014342311200001,
0.47760471412,
0.49861313712
]
},
{
"command": "pnpr@HEAD",
"mean": 0.57238100542,
"stddev": 0.09023135846987788,
"median": 0.54347127062,
"user": 0.38693199999999994,
"system": 0.824664,
"min": 0.51021552012,
"max": 0.80504479912,
"times": [
0.5279133731200001,
0.5413968931200001,
0.5302336441200001,
0.51317275212,
0.54725910712,
0.80504479912,
0.6462922991200001,
0.51021552012,
0.5567360181200001,
0.5455456481200001
]
},
{
"command": "pnpr@main",
"mean": 0.5211374555200001,
"stddev": 0.010994101694611267,
"median": 0.52363790562,
"user": 0.3868346,
"system": 0.8042347000000001,
"min": 0.50546423912,
"max": 0.5407713831200001,
"times": [
0.5407713831200001,
0.5220624611200001,
0.5253848441200001,
0.5173914041200001,
0.52521335012,
0.52992074412,
0.5056019191200001,
0.5263004201200001,
0.51326379012,
0.50546423912
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 3.88194437106,
"stddev": 0.03714039385813992,
"median": 3.8751159823600005,
"user": 3.9134268799999994,
"system": 2.1170238400000008,
"min": 3.8356952363600003,
"max": 3.9625734763600002,
"times": [
3.90283666336,
3.8387328423600002,
3.86693782736,
3.8763702283600003,
3.8738617363600003,
3.9625734763600002,
3.8766822583600002,
3.8718695583600002,
3.8356952363600003,
3.91388388336
]
},
{
"command": "pacquet@main",
"mean": 3.7937561770600006,
"stddev": 0.042539745574638056,
"median": 3.7869669698600004,
"user": 3.97929338,
"system": 2.1377488400000004,
"min": 3.7396007043600004,
"max": 3.86340757636,
"times": [
3.8034868583600003,
3.86340757636,
3.8268638433600004,
3.7655973563600003,
3.7396007043600004,
3.83486156936,
3.8261533553600002,
3.7704470813600004,
3.7554002803600004,
3.7517431453600003
]
},
{
"command": "pnpr@HEAD",
"mean": 1.97249616056,
"stddev": 0.14397507669161522,
"median": 1.9302425838600001,
"user": 2.7116391799999997,
"system": 1.8245726399999995,
"min": 1.82772964436,
"max": 2.23262216636,
"times": [
1.86382860536,
1.8393181923600002,
1.82772964436,
1.9134893513600002,
1.94699581636,
2.0955139433600003,
2.17396111036,
1.88336310236,
2.23262216636,
1.94813967336
]
},
{
"command": "pnpr@main",
"mean": 2.0059942672599997,
"stddev": 0.12502288389101637,
"median": 1.98857260736,
"user": 2.72132838,
"system": 1.79827074,
"min": 1.78366310636,
"max": 2.23639282336,
"times": [
2.23639282336,
2.07637995936,
1.99968695836,
1.92322800136,
1.9451912093600001,
1.97745825636,
2.09376944536,
2.09583502036,
1.92833789236,
1.78366310636
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.9902476382,
"stddev": 0.017129775197592567,
"median": 0.9837633019,
"user": 1.1826534399999997,
"system": 1.0243024799999998,
"min": 0.9754075299,
"max": 1.0328890279,
"times": [
1.0024729369,
0.9829108949,
0.9973169239,
0.9805815809,
1.0328890279,
0.9754075299,
0.9853931609000001,
0.9784247109,
0.9846157089,
0.9824639069000001
]
},
{
"command": "pacquet@main",
"mean": 1.0144845476000002,
"stddev": 0.011690846282158777,
"median": 1.0132940974000002,
"user": 1.1940848399999997,
"system": 1.0307272800000002,
"min": 0.9970449959000001,
"max": 1.0316209689,
"times": [
1.0074226529,
1.0066775849,
0.9970449959000001,
1.0167787749000001,
1.0138102039,
1.0127779909,
1.0030842839,
1.0315236839000002,
1.0316209689,
1.0241043359000002
]
},
{
"command": "pnpr@HEAD",
"mean": 0.5120527571,
"stddev": 0.008375247877170347,
"median": 0.5107963819,
"user": 0.35078444,
"system": 0.76561808,
"min": 0.5032753609,
"max": 0.5321926719,
"times": [
0.5321926719,
0.5115238139,
0.5047208599,
0.5100689499,
0.5063542389,
0.5070373279,
0.5032753609,
0.5163935179,
0.5133814919,
0.5155793379
]
},
{
"command": "pnpr@main",
"mean": 0.5799418606,
"stddev": 0.10764822018191762,
"median": 0.5217134353999999,
"user": 0.34672223999999996,
"system": 0.7796917800000001,
"min": 0.5060296639,
"max": 0.8010415699,
"times": [
0.5152876769,
0.5123191089,
0.5219079899,
0.5060296639,
0.5215188809,
0.5146800769,
0.6585232139,
0.8010415699,
0.7242671869,
0.5238432379
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.73283817042,
"stddev": 0.029191765453916704,
"median": 2.73080179722,
"user": 1.6697424999999995,
"system": 1.1936721399999999,
"min": 2.6947554617200002,
"max": 2.80018786572,
"times": [
2.74865912172,
2.72611377472,
2.6947554617200002,
2.71419865672,
2.70848568572,
2.71681162572,
2.73858241772,
2.74509727472,
2.73548981972,
2.80018786572
]
},
{
"command": "pacquet@main",
"mean": 2.54893734882,
"stddev": 0.016884603512704995,
"median": 2.54757344622,
"user": 1.7138633000000003,
"system": 1.21885464,
"min": 2.52756745572,
"max": 2.57729921572,
"times": [
2.52977887972,
2.5325652817199997,
2.54693073972,
2.57729921572,
2.52756745572,
2.56732276372,
2.54059403272,
2.56356404272,
2.54821615272,
2.55553492372
]
},
{
"command": "pnpr@HEAD",
"mean": 0.54262375062,
"stddev": 0.005748594400440866,
"median": 0.54178283772,
"user": 0.3519746999999999,
"system": 0.8097662400000001,
"min": 0.53598463072,
"max": 0.5527719547200001,
"times": [
0.5447349757200001,
0.54252103072,
0.54104464472,
0.53783161972,
0.5383617367200001,
0.53598463072,
0.5466263027200001,
0.5366372757200001,
0.5497233347200001,
0.5527719547200001
]
},
{
"command": "pnpr@main",
"mean": 0.5244718080200002,
"stddev": 0.007204034874918544,
"median": 0.5255619542200001,
"user": 0.3568652,
"system": 0.7828783399999999,
"min": 0.51108363172,
"max": 0.5330526107200001,
"times": [
0.51108363172,
0.5330526107200001,
0.52580725372,
0.5252088557200001,
0.5330521977200001,
0.52562915572,
0.5238991617200001,
0.52549475272,
0.5280859157200001,
0.5134045447200001
]
}
]
} |
|
| Branch | pr/12357 |
| 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 | 3,881.94 ms(-43.66%)Baseline: 6,890.70 ms | 8,268.84 ms (46.95%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 2,732.84 ms(-30.99%)Baseline: 3,960.01 ms | 4,752.01 ms (57.51%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 990.25 ms(-22.87%)Baseline: 1,283.92 ms | 1,540.70 ms (64.27%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 3,979.16 ms(-47.69%)Baseline: 7,607.44 ms | 9,128.92 ms (43.59%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 484.35 ms(-27.65%)Baseline: 669.48 ms | 803.38 ms (60.29%) |
|
Code review by qodo was updated up to the latest commit c847297 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #12357 +/- ##
==========================================
+ Coverage 88.07% 88.15% +0.08%
==========================================
Files 294 295 +1
Lines 37375 37733 +358
==========================================
+ Hits 32919 33265 +346
- Misses 4456 4468 +12 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Code review by qodo was updated up to the latest commit c66c2e2 |
|
Code review by qodo was updated up to the latest commit fc24d66 |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
pacquet/crates/resolving-deps-resolver/src/tests.rs (1)
2876-2948: ⚡ Quick winAdd a
jsr:overlay regression next to the npm alias case.
overlay_lookup_namesnow has a separatejsr:name-derivation branch inpacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs(Lines 1570-1577), but this module only locks down the plain andnpm:flows. A smalljsr:fixture here would keep that new cache-key / overlay-injection path from drifting unnoticed.As per coding guidelines, "Tests are documentation — do not duplicate test scenarios, edge cases, failure modes, or worked examples in prose when they are already captured by tests."
🤖 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/tests.rs` around lines 2876 - 2948, Add a new test mirroring npm_alias_child_consults_overlay_by_inner_name that verifies the jsr: alias path injects overlays by the inner name: create a test (e.g., jsr_alias_child_consults_overlay_by_inner_name) using the same fake_manifest, OverlayRecordingResolver, and resolve_dependency_tree call, but add an entry in the resolver.table for the aliased dependency key ("renamed", "jsr:pinned@~5.0.0") mapping to the pinned fake_result and assert that resolver.seen_overlay.lock().unwrap().get(&("renamed".to_string(), "jsr:pinned@~5.0.0".to_string())) == Some(&vec!["5.0.0".to_string()]); this ensures the new overlay_lookup_names jsr: branch (in resolve_dependency_tree.rs) is exercised.Source: Coding guidelines
🤖 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/tests.rs`:
- Around line 2876-2948: Add a new test mirroring
npm_alias_child_consults_overlay_by_inner_name that verifies the jsr: alias path
injects overlays by the inner name: create a test (e.g.,
jsr_alias_child_consults_overlay_by_inner_name) using the same fake_manifest,
OverlayRecordingResolver, and resolve_dependency_tree call, but add an entry in
the resolver.table for the aliased dependency key ("renamed",
"jsr:pinned@~5.0.0") mapping to the pinned fake_result and assert that
resolver.seen_overlay.lock().unwrap().get(&("renamed".to_string(),
"jsr:pinned@~5.0.0".to_string())) == Some(&vec!["5.0.0".to_string()]); this
ensures the new overlay_lookup_names jsr: branch (in resolve_dependency_tree.rs)
is exercised.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 14b752d7-8c77-46c8-b24f-32d8668b6e70
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (3)
pacquet/crates/resolving-deps-resolver/Cargo.tomlpacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rspacquet/crates/resolving-deps-resolver/src/tests.rs
✅ Files skipped from review due to trivial changes (1)
- pacquet/crates/resolving-deps-resolver/Cargo.toml
📜 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). (7)
- GitHub Check: Lint and Test (ubuntu-latest)
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Code Coverage
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plainStringor&strin Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too viaTryFrom<String>and/orFromStrand do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallibleFrom<String>constructor
If upstream TypeScript occasionally constructs a branded string without validation, exposefrom_str_uncheckedin Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization
Derive simple conversions for branded strings using#[derive(derive_more::From)]and#[derive(derive_more::Into)]instead of handwritingimplblocks; use manualimplonly when conversion needs custom logic
Model TypeScript string literal unions (like'auto' | 'always' | 'never') as Rustenums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like`${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide inCODE_STYLE_GUIDE.md— imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Choose owned vs. borrowed parameters to minimize copies; widen to t...
Files:
pacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
🧠 Learnings (6)
📚 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/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
🔇 Additional comments (1)
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs (1)
968-979: LGTM!Also applies to: 1017-1033, 1148-1190, 1431-1469, 1540-1580
fc24d66 to
372546a
Compare
|
Code review by qodo was updated up to the latest commit 372546a |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
pacquet/crates/resolving-deps-resolver/src/tests.rs (1)
2876-2949: ⚡ Quick winAdd a
jsr:overlay-parity regression in this module.These new tests cover plain and
npm:alias overlay propagation, but the resolver change also introduced ajsr:name-folding branch in overlay lookup. Adding ajsr:case here would lock that branch and its cache-key/overlay behavior against regressions.🤖 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/tests.rs` around lines 2876 - 2949, Add a parallel regression case for the jsr: alias so the jsr name-folding branch is exercised: either duplicate the npm test (npm_alias_child_consults_overlay_by_inner_name) as a new test named e.g. jsr_alias_child_consults_overlay_by_inner_name or extend the existing test to also insert a table entry for ("renamed", "jsr:pinned@~5.0.0") -> fake_result("pinned","5.0.0") using the same OverlayRecordingResolver/seen_overlay, run resolve_dependency_tree the same way, and assert that seen.get(&("renamed".to_string(), "jsr:pinned@~5.0.0".to_string())) == Some(&vec!["5.0.0".to_string()) so the jsr: overlay propagation is locked in.
🤖 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/tests.rs`:
- Around line 2876-2949: Add a parallel regression case for the jsr: alias so
the jsr name-folding branch is exercised: either duplicate the npm test
(npm_alias_child_consults_overlay_by_inner_name) as a new test named e.g.
jsr_alias_child_consults_overlay_by_inner_name or extend the existing test to
also insert a table entry for ("renamed", "jsr:pinned@~5.0.0") ->
fake_result("pinned","5.0.0") using the same
OverlayRecordingResolver/seen_overlay, run resolve_dependency_tree the same way,
and assert that seen.get(&("renamed".to_string(),
"jsr:pinned@~5.0.0".to_string())) == Some(&vec!["5.0.0".to_string()) so the jsr:
overlay propagation is locked in.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e103ac15-733b-4259-ae07-21546cf28067
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
pacquet/crates/resolving-deps-resolver/Cargo.tomlpacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rspacquet/crates/resolving-deps-resolver/src/resolve_importer.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-npm-resolver/src/lib.rspacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rspacquet/crates/resolving-npm-resolver/src/npm_resolver.rspacquet/crates/resolving-npm-resolver/src/preferred_overlay.rspacquet/crates/resolving-resolver-base/src/lib.rspacquet/crates/resolving-resolver-base/src/resolve.rs
✅ Files skipped from review due to trivial changes (1)
- pacquet/crates/resolving-deps-resolver/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (8)
- pacquet/crates/resolving-npm-resolver/src/lib.rs
- pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs
- pacquet/crates/resolving-resolver-base/src/resolve.rs
- pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs
- pacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rs
- pacquet/crates/resolving-resolver-base/src/lib.rs
- pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
- pacquet/crates/resolving-deps-resolver/src/resolve_importer.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). (6)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Code Coverage
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plainStringor&strin Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too viaTryFrom<String>and/orFromStrand do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallibleFrom<String>constructor
If upstream TypeScript occasionally constructs a branded string without validation, exposefrom_str_uncheckedin Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization
Derive simple conversions for branded strings using#[derive(derive_more::From)]and#[derive(derive_more::Into)]instead of handwritingimplblocks; use manualimplonly when conversion needs custom logic
Model TypeScript string literal unions (like'auto' | 'always' | 'never') as Rustenums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like`${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide inCODE_STYLE_GUIDE.md— imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Choose owned vs. borrowed parameters to minimize copies; widen to t...
Files:
pacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
🧠 Learnings (6)
📚 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/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
🔇 Additional comments (2)
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs (1)
14-16: LGTM!Also applies to: 428-429, 940-997, 999-1035, 1037-1061, 1067-1099, 1123-1125, 1148-1200, 1275-1276, 1374-1385, 1387-1547, 1549-1615, 1776-1779
pacquet/crates/resolving-deps-resolver/src/tests.rs (1)
2754-2875: LGTM!
|
| Branch | pr/12357 |
| 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.50 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 542.62 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 512.05 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 1,938.90 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 572.38 ms |
|
Code review by qodo was updated up to the latest commit e85f6c9 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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.
Inline comments:
In `@pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs`:
- Around line 1574-1597: The warmup path must not run the post-read hooks;
modify the warm children call site (the code that calls resolve_wanted_cached
during the warm phase) to skip warming whenever
ctx.workspace.manifest_hook.is_some() || ctx.workspace.pnpmfile_hook.is_some(),
or alternatively extend resolve_wanted_cached with a boolean flag (e.g.
skip_hooks / prewarm) and pass true from the warm path; ensure
resolve_wanted_cached and the code that currently invokes manifest_hook and
pnpmfile_hook.read_package respect that flag and do not execute manifest_hook or
pnpmfile_hook.read_package when skip_hooks/prewarm is set so warm entries never
trigger logging or hook-side effects.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 4c0ea38f-7052-4937-9cdd-dc8c69c1c59f
📒 Files selected for processing (1)
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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). (9)
- GitHub Check: Code Coverage
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Analyze (javascript)
- GitHub Check: Lint and Test (ubuntu-latest)
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Dylint
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plainStringor&strin Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too viaTryFrom<String>and/orFromStrand do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallibleFrom<String>constructor
If upstream TypeScript occasionally constructs a branded string without validation, exposefrom_str_uncheckedin Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization
Derive simple conversions for branded strings using#[derive(derive_more::From)]and#[derive(derive_more::Into)]instead of handwritingimplblocks; use manualimplonly when conversion needs custom logic
Model TypeScript string literal unions (like'auto' | 'always' | 'never') as Rustenums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like`${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide inCODE_STYLE_GUIDE.md— imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Choose owned vs. borrowed parameters to minimize copies; widen to t...
Files:
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
🧠 Learnings (7)
📚 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_dependency_tree.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_dependency_tree.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_dependency_tree.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_dependency_tree.rs
📚 Learning: 2026-06-12T14:51:20.984Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12361
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:1338-1341
Timestamp: 2026-06-12T14:51:20.984Z
Learning: When reviewing pacquet Rust dependency-tree resolution logic that performs `landed_on_prior_entry`-style comparisons, avoid comparing raw ids that may include a `patch_hash=(...)` suffix and/or peer suffixes. `PkgNameVerPeer`’s peer-suffix handling can already absorb the patch-hash token such that `prior_key.without_peer()` represents bare `nameversion` for registry packages. The id-mismatch risk is on the normalized-id side: `build_pkg_id_with_patch_hash` may add `file:`/`git:`/`tarball:` prefixes before `name@...`. For correctness, ensure both sides are normalized by stripping patch/peer suffixes—specifically compare `prior_key.without_peer()` with `pacquet_deps_path::remove_suffix(&id)` (both stripped), ideally via the `landed_on_prior_entry` helper so the normalization rules stay consistent.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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_dependency_tree.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
|
Code review by qodo was updated up to the latest commit 185c85a |
185c85a to
9a71282
Compare
|
Code review by qodo was updated up to the latest commit 9a71282 |
…dren's preferred versions pnpm extends the preferred-versions map per resolution level: after a package's direct dependencies settle, their (name, version) pairs join the map the children's subtree resolutions pick against (https://github.com/pnpm/pnpm/blob/ce9c096e8e/installing/deps-resolver/src/resolveDependencies.ts#L717-L746), so e.g. signed-varint's varint@~5.0.0 dedupes to the varint@5.0.0 its parent pinned as a sibling. pacquet picked against a static seed only, drifting to highest-in-range (varint 5.0.2, es-abstract, the spdx-expression-parse 3-vs-4 cascade, and the jest/@types/node variants under importers without @types/node). The walk now resolves a whole sibling level before any child subtree starts (upstream's postponed-resolution barrier): resolve_node splits into resolve_node_seed (the per-package half) and walk_node_children (the recursion half), and each level layers its resolved versions onto an O(1) PreferredVersionsOverlay chain consulted by the npm picker as plain version selectors. The overlay's per-name view joins the per-wanted dedup cache key, since the same range can legitimately pick different versions under levels that resolved different siblings. Lockfile-reuse subtrees keep the no-overlay path — their versions are exact pins. Whole-monorepo lockfile diff vs fresh pnpm 11.6.0: 128 → 11 changed lines. Part of #12266.
… initial wave pnpm resolves every importer's initial wave before any peer hoist and then repeats global hoist rounds (per round: each importer's required-peer loop to a fixpoint, then one optional-peer hoist) until no importer hoists (https://github.com/pnpm/pnpm/blob/ce9c096e8e/installing/deps-resolver/src/resolveDependencies.ts#L335-L445). pacquet ran each importer's whole hoist loop before the next importer's initial wave, so an early importer's optional-peer pick could not see versions a later importer's wave resolves — e.g. @cyclonedx/cyclonedx-library's spdx-expression-parse hoisted 3.0.1 where pnpm's barrier-visible map picks 4.0.0. resolve_importer_with_workspace splits into an ImporterHoistState with init / run_required_round / hoist_optional_round steps; resolve_workspace initializes every importer first and then drives the rounds, exactly mirroring upstream's phase order. The single-importer entry composes the same steps, unchanged in behavior. The hoist-missing scope snapshot moves to once per round, and the per-importer final peer pass is dropped in the workspace flow (its result was discarded; the workspace-wide pass recomputes peers). Whole-monorepo lockfile diff vs fresh pnpm 11.6.0: 11 → 5 changed lines. Part of #12266.
…, compose seeded overlays The fold's gate and cache-key view keyed off the outer alias, but the npm picker merges overlay selectors by the resolved spec name — for npm: aliases the inner target and for jsr: specifiers the folded @jsr/... name — so an aliased edge skipped the fold and could reuse an overlay-insensitive cached pick. overlay_lookup_names mirrors the resolver's name derivation and the view now covers every candidate name. Also compose any overlay seeded on base_opts under the level chain (and thread it through resolve_node's no-fold wrapper) so descendant picks and dedup keys keep honoring it. Both from review comments on the PR.
The WantedKey overlay view flattened versions across the candidate lookup names into one union, so two overlays distributing the same versions across different names could collide and reuse a pick made under the other state. The view now pairs each name with its sorted versions. From a review comment on the PR.
…r waits The postponed-resolution barrier serializes a node's grandchildren behind its slowest sibling — the structural cost of level-correct preferred-version picks, and the ~3.5% cold-cache fresh-install regression the integrated benchmark shows. Each freshly-seeded child now speculatively resolves its own children during the barrier wait, through the shared per-wanted dedup cache under the empty-overlay-view key: when the real pick's view is empty (the overwhelmingly common case) it reuses the warm entry outright, otherwise it misses into its own bucket and re-picks from the warm metadata caches. Errors are swallowed — a speculative fetch must never fail the install. The resolve-and-hook pipeline moves into resolve_wanted_cached, shared by the seed phase and the warm. Behavior-neutral: whole-monorepo lockfile byte-diff unchanged and deterministic across runs.
…configured A pnpmfile hook is externally observable per call (readPackage IPC, context.log, custom resolvers), so speculative resolutions must not fire it. The pure in-memory manifest hook keeps the warm path: it is idempotent and cache-deduped, indistinguishable from a first-caller win in the pre-existing concurrent-miss race. From a review comment on the PR.
The rebase onto the currentPkg threading left the overlay-view tuple index stale and the warm key without the prior-key slot.
The deterministic-owner machinery replaced the is_revisit lazy gate; only the occurrence that owns a package's children walks them, so the warm is wasted on non-owners.
9a71282 to
ef28e45
Compare
|
Code review by qodo was updated up to the latest commit ef28e45 |
Summary
Two parity changes for pacquet's resolver, continuing #12266. Measured on the monorepo (fresh state,
install --lockfile-only, back-to-back vs pnpm 11.6.0), the real-lockfile document diff drops from 128 to 5 changed lines (re-measured after rebasing over #12361/#12362: 132 → 11, where 8 of the 11 are a divergence the pacquet side of #12362 itself introduced — see the analysis on #12266 — and 3 are the known cycle-closing-edge gap).1. Per-level preferred-version fold
pnpm extends the preferred-versions map per resolution level: after a package's direct dependencies settle, their
(name, version)pairs join the map the children's subtree resolutions pick against (resolveDependencies.ts#L717-L746). Sosigned-varint'svarint@~5.0.0dedupes to thevarint@5.0.0its parent pinned as a sibling instead of drifting to5.0.2. pacquet picked against a static seed only; besidesvarint/es-abstract, this turned out to drive the remainingjest/@types/nodeduplicate variants too.resolve_nodesplits intoresolve_node_seed+walk_node_children.PreferredVersionsOverlay(O(1)Arc-chained layers inresolver-base); the npm picker folds the per-name view in as plainversionselectors at both registry seams.2. Hoist rounds across all importers (deterministic barrier, same logic as pnpm)
pnpm resolves every importer's initial wave before any peer hoist, then repeats global hoist rounds (per round: each importer's required-peer loop to a fixpoint, then one optional-peer hoist) until no importer hoists (resolveDependencies.ts#L335-L445). pacquet ran each importer's whole hoist loop before the next importer's initial wave, so an early importer's optional-peer pick couldn't see versions a later importer resolves —
@cyclonedx'sspdx-expression-parsehoisted3.0.1where pnpm's barrier-visible map picks4.0.0.resolve_importer_with_workspaceis now anImporterHoistState(init/run_required_round/hoist_optional_round) driven byresolve_workspacein upstream's exact phase order. Both implementations are deterministic here; the rule is identical.Verification
child_resolution_prefers_parent_level_sibling_versions(fails with the fold disabled) + fullresolving-*,package-manager,clisuites: 1,242 tests pass; clippy--deny warnings, rustfmt, typos clean.Remaining gaps (tracked in #12266)
es-abstract: 1.24.2underarraybuffer.prototype.slice@1.0.4although the tree walk breaks the cycle; pacquet drops the edge. Next self-contained fix.@types/ssri→@types/node22 vs 25): in pnpm, a shared package's children context belongs to whichever occurrence resolves first — an async race biased toward the shallowest occurrence; pacquet's claimer is deterministically the first importer in sorted order. Making pnpm deterministic here is a separate project: the claim's outputs are structure-shared (missingPeersOfChildrenByPkgIdpromises already consumed by hoist logic when a better occurrence arrives), and the code itself notes a proper fix "would probably require a big rewrite of the resolution algorithm" (resolveDependencies.ts#L1705-L1707).Written by an agent (Claude Code, claude-fable-5).
Summary by CodeRabbit