Skip to content

fix(pacquet): share a subtree's peer context across importers + record array-form engines#12349

Merged
zkochan merged 3 commits into
mainfrom
lockfile-parity3
Jun 12, 2026
Merged

fix(pacquet): share a subtree's peer context across importers + record array-form engines#12349
zkochan merged 3 commits into
mainfrom
lockfile-parity3

Conversation

@zkochan

@zkochan zkochan commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

Two more lockfile-parity fixes for pacquet, 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 445 to 128 changed lines, and the divergent importer entries drop from ~30 to 2.

1. Shared subtrees keep the first importer's peer context (fix(deps-resolver))

The dominant remaining divergence was which @types/node a hoisted optional peer binds to. Root cause (established with a three-importer repro on @pnpm/meta-updater, behavior verified against pnpm 11.6.0 in four configurations): pnpm computes a package subtree's missing-peer report once, under the importer whose walk first resolves it (missingPeersOfChildrenByPkgId). An importer revisiting the package reuses that report, so:

  • a peer the first context satisfied (root's direct @types/node@22.x) never reaches a later importer's optional-peer hoist — the later importer's occurrence ends up sharing the first context's suffixed variant; while
  • a peer the first context could not satisfy either (clipanion's typanion under a root that only hoists it later) stays visible to every importer, and each hoists its own copy.

pacquet re-walked each importer's tree from scratch, so an importer without @types/node hoisted the max-satisfying 25.x for a subtree the root had already resolved against 22.x, forking snapshots into duplicate suffix variants (two @pnpm/cli-utils@… entries, etc.).

The walk now records per package the claiming importer and the missing-peer names of that first peer walk; the per-importer hoist input drops a miss declared under a foreign-claimed ancestor only when the claiming walk did not report it missing. The final workspace-wide peer pass stays unscoped, so per-project missing-peer warnings are unchanged. Two regression tests cover both directions (suppressed satisfied-miss; visible unsatisfied-miss), each verified to fail without the fix.

2. Array-form engines (fix(lockfile))

jsonparse@1.3.1 declares "engines": ["node >= 0.2.0"]; pnpm records engines: {'0': node >= 0.2.0} (the Object.entries shape for an array). pacquet read the field via as_object only and dropped it.

Verification

  • New tests in pacquet-resolving-deps-resolver (111 total) + full pacquet-package-manager suite (462); clippy --deny warnings, rustfmt, typos clean.
  • Whole-monorepo diff vs fresh pnpm 11.6.0: 445 → 128 changed lines. The earlier minimal repros (@pnpm/meta-updater shared across importers; eslint+cosmiconfig-typescript-loader; debug+concurrently) all match pnpm byte-for-byte.

Remaining gaps (tracked in #12266)

  • jest's @types/node under __utils__/eslint-config and @cyclonedx's spdx-expression-parse under deps/compliance/sbom: pacquet's sequential, lexically-ordered importer processing makes the peer-less importer the first claimer of the shared subtree, where pnpm's concurrent importer resolution lets a peer-satisfying importer win the race. Needs a look at pnpm's project ordering.
  • Per-level preferred-version folding in the picker (resolveDependencies.ts#L717-L733): pnpm biases child resolutions toward versions already resolved at the parent level (varint ~5.0.0 → sibling-pinned 5.0.0; es-abstract; the spdx-expression-parse 3-vs-4 pick). pacquet's picker seed is static.
  • Env-lockfile document: missing packageManagerDependencies block and libc: fields on platform packages.

Written by an agent (Claude Code, claude-fable-5).

Summary by CodeRabbit

  • Refactor

    • Extend engine constraint parsing to accept both object and array formats
    • Improve peer-dependency resolution tracking and hoisting behavior across workspace importers, including suppression of certain missing-peer diagnostics
  • Tests

    • Added tests covering optional peer-dependency hoisting for shared subtrees across multiple workspace importers
  • Style

    • Adjusted code formatting in module imports

zkochan added 2 commits June 12, 2026 12:00
A package.json with `"engines": ["node >= 0.2.0"]` (e.g.
jsonparse@1.3.1) records `engines: {'0': node >= 0.2.0}` in pnpm's
lockfile — the shape Object.entries() yields for an array. pacquet
read engines via as_object only and dropped the field.

Part of #12266.
…s importers

A package first resolved under one importer keeps that walk's
missing-peer report in pnpm (missingPeersOfChildrenByPkgId,
https://github.com/pnpm/pnpm/blob/a751c7f27d/installing/deps-resolver/src/resolveDependencies.ts#L193):
an importer revisiting the package neither recomputes its subtree's
missing peers nor re-hoists optional peers the first context already
satisfied — its occurrence then shares the first context's
peer-suffixed variant. pacquet re-walked every importer's tree from
scratch, so an importer without e.g. @types/node hoisted the highest
satisfying version (25.x) for a subtree the root importer had already
resolved against its own @types/node (22.x), forking the snapshot into
duplicate suffix variants (verified against pnpm 11.6.0 with a
three-importer repro on @pnpm/meta-updater).

The walk now records, per package, the importer whose tree first
resolved it and the missing-peer names of that first peer walk. The
per-importer hoist input drops a missing peer declared under a
foreign-claimed ancestor when the claiming walk did not report it
missing — misses the first context could not satisfy either (e.g.
clipanion's typanion under a root that only hoists it later) stay
visible to every importer, which then hoists its own copy exactly like
pnpm. The final workspace-wide pass stays unscoped so peer warnings
still cover every importer.

Whole-monorepo lockfile diff vs fresh pnpm 11.6.0: 445 → 128 changed
lines; the remaining divergence is two importer entries (jest's
@types/node under __utils__/eslint-config and @CycloneDX's
spdx-expression-parse) plus per-level preferred-version picks (varint,
es-abstract), tracked in the issue.

Part of #12266.
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 11fa2860-8952-4c43-9de3-218ae089ba56

📥 Commits

Reviewing files that changed from the base of the PR and between d8d1141 and 6645acd.

📒 Files selected for processing (2)
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (ubuntu-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 fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plain String or &str in Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too via TryFrom<String> and/or FromStr and 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 infallible From<String> constructor
If upstream TypeScript occasionally constructs a branded string without validation, expose from_str_unchecked in 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 handwriting impl blocks; use manual impl only when conversion needs custom logic
Model TypeScript string literal unions (like 'auto' | 'always' | 'never') as Rust enums 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 in CODE_STYLE_GUIDE.md — imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Choose owned vs. borrowed parameters to minimize copies; widen to t...

Files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.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/resolve_importer.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
📚 Learning: 2026-05-24T21:10:50.292Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:10:50.292Z
Learning: When implementing dependency “revisit”/optional-folding in pacquet, mirror pnpm’s `resolveDependencies.ts` behavior: on revisit, update the `optional` flag only for the directly visited package, and do not automatically change `optional` for transitive descendants. pnpm corrects stale optional flags later via `copyDependencySubGraph` (a BFS in `lockfile/pruner/src/index.ts`/`copyDependencySubGraph`). Until pacquet has an equivalent pruner/copy-subgraph step, be aware that the raw `node.optional` can flow through to the lockfile/virtual store via `dependencies_graph_to_lockfile.rs` → `create_virtual_store.rs` → `installability.rs`, so review changes that affect optional flag propagation for this mismatch.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
📚 Learning: 2026-05-24T21:10:50.292Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:10:50.292Z
Learning: In pacquet’s Rust resolver layer, the behavior of `ResolvedPackage.optional` “AND-folding”/revisit updates is intentionally aligned with pnpm: when a package is directly revisited, only that package’s `optional` flag is updated; transitive descendants are not re-walked/rescored in this stage. pnpm later corrects any stale optional flags downstream via its pruner/BFS logic (`copyDependencySubGraph` + `nonOptional` reachability stamping in the lockfile pruner). Since pacquet does not yet have that pruner equivalent, review should NOT flag the downstream/stale optional propagation as a bug in pacquet PRs; treat the resolver-layer optional propagation gap as expected parity until the pruning/stamping step is ported.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.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_importer.rs
🔇 Additional comments (1)
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs (1)

323-334: LGTM!


📝 Walkthrough

Walkthrough

Tracks which importer first resolved each package and records first-walk missing-peer names; introduces HoistMissingScope and threads it into peer-resolution hoist passes so missing-peer diagnostics can be suppressed per-importer in shared subtrees.

Changes

Hoist-scoped peer suppression with first-importer tracking

Layer / File(s) Summary
Hoist scope option type and result fields
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
Introduce HoistMissingScope with suppresses(...); add hoist_missing_scope: Option<Arc<HoistMissingScope>> to ResolvePeersOptions (default None); add missing_names_by_pkg to ResolvePeersResult.
Dependency-tree context for importer and missing-peer tracking
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
WorkspaceTreeCtx adds first_importer_by_pkg and first_walk_missing_by_pkg mutex maps plus snapshot/record APIs; TreeCtx adds importer_id and with_importer_id.
Recording first importers during package resolution
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
Fresh (resolve_node) and reused (resolve_reused_node) package insertion paths record the package's first importer into first_importer_by_pkg if not already present.
Multi-pass hoisting with scoped missing-peer visibility
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
Baseline peers_opts uses hoist_missing_scope: None; hoist_peers_opts snapshots an importer-scoped HoistMissingScope (from workspace trackers) and is used for inner hoisting passes to avoid observing earlier-importer missing peers.
Missing-peer suppression in walker and resolve_one_peer
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
Walker::walk() computes missing_names_by_pkg; peers-cache re-emission and resolve_one_peer are guarded by missing_issue_suppressed using ancestor package id chains and HoistMissingScope to skip suppressed missing-peer issue insertions.
Workspace wiring, tests, and engines parsing
pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs, pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs, pacquet/crates/resolving-deps-resolver/src/tests.rs, pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs, pacquet/crates/resolving-deps-resolver/src/lib.rs
resolve_workspace and test fixtures set hoist_missing_scope: None where appropriate; added two tokio tests validating shared-subtree hoisting behavior; build_package_metadata parsing of engines now accepts object and array forms; minor reformatting of re-export list.

Sequence Diagram

sequenceDiagram
  participant Importer
  participant resolve_importer
  participant resolve_peers
  participant WorkspaceTreeCtx
  Importer->>resolve_importer: start importer walk (with importer_id)
  resolve_importer->>resolve_peers: call resolve_peers (hoist_peers_opts)
  resolve_peers->>WorkspaceTreeCtx: read first_importer_by_pkg / first_walk_missing_by_pkg
  resolve_peers->>WorkspaceTreeCtx: return missing_names_by_pkg
  resolve_importer->>WorkspaceTreeCtx: record_first_walk_missing(...)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pnpm#11784: Related work on the hoist/auto-install-peers loop that this PR scopes per-importer.
  • pnpm/pnpm#11906: Overlaps in resolve_peers.rs around missing-peer computation and cached issue emission.
  • pnpm/pnpm#12022: Also extends ResolvePeersOptions and touches hoist/peer id handling in related code paths.

Poem

🐰 I hopped through shared subtrees, leaving tiny marks,
First importers signed the maps, quiet in the dark.
When peers went missing near, I hushed the second cries—
No echoes of the same old call beneath those skies.
🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: sharing a subtree's peer context across importers and recording array-form engines, matching the primary objectives and file modifications throughout the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lockfile-parity3

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.01      7.7±0.27ms   566.2 KB/sec    1.00      7.6±0.05ms   569.2 KB/sec

@zkochan zkochan marked this pull request as ready for review June 12, 2026 10:21
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0)

Grey Divider


Action required

1. Missing-peers union mis-scopes 🐞 Bug ≡ Correctness
Description
ResolvePeersResult::missing_names_by_pkg is built as a union of missing peer names across all
occurrences of a package in the walk, but it is persisted as the package’s “first-walk” missing set
and later used to decide hoist suppression. This can make later importers incorrectly *not* suppress
a peer miss (because some later occurrence in the first importer missed it), diverging peer-hoist
behavior from the intended “first occurrence wins” semantics.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[R613-621]

+        let mut missing_names_by_pkg: HashMap<String, std::collections::HashSet<String>> =
+            HashMap::new();
+        for (node_id, missing) in &self.node_missing_peers {
+            let Some(tree_node) = self.tree.dependencies_tree.get(node_id) else { continue };
+            missing_names_by_pkg
+                .entry(tree_node.resolved_package_id.clone())
+                .or_default()
+                .extend(missing.keys().cloned());
+        }
Evidence
missing_names_by_pkg is populated by extending the set for a pkg_id for every node occurrence,
which creates union semantics; this map is then persisted as the “first walk” record via
record_first_walk_missing (first write wins).

pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[613-621]
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[604-612]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`missing_names_by_pkg` is currently computed by unioning missing peer names across *all* occurrences of a resolved package within a single `resolve_peers` walk, and `WorkspaceTreeCtx::record_first_walk_missing` persists that union as the package’s “first walk” missing set.
This defeats the intended “first occurrence / first walker wins” behavior: if the first importer sees the same `pkg_id` multiple times under different peer contexts, a peer that was satisfied in the first occurrence but missing in a later occurrence will still end up recorded as “missing in the first walk”, preventing suppression for later importers.
## Issue Context
- `missing_names_by_pkg` is fed into `WorkspaceTreeCtx::record_first_walk_missing(...)` and then used by `HoistMissingScope::suppresses(...)` to decide whether to suppress a missing-peer issue under a foreign-claimed ancestor.
- The current implementation builds `missing_names_by_pkg` from `self.node_missing_peers` at the end of the walk, which inherently loses traversal order and forces union semantics.
## Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[578-628]
- pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[701-1002]
### Implementation direction
- Track “first occurrence” missing-peer names per `pkg_id` during the DFS walk (in deterministic visit order), e.g.:
- Add a `first_missing_names_by_pkg: HashMap<String, HashSet<String>>` field to `Walker`.
- When a node is resolved (both cache-hit and non-cache-hit), if `first_missing_names_by_pkg` has no entry for `pkg.id`, insert a `HashSet` of the current occurrence’s missing peer names (may be empty).
- Return that map as `ResolvePeersResult::missing_names_by_pkg`.
- Avoid building this from iterating a `HashMap` after the walk, because `HashMap` iteration order is not tied to first-visit order and forces union behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Always builds missing-by-pkg 🐞 Bug ➹ Performance
Description
resolve_peers now always allocates and populates ResolvePeersResult::missing_names_by_pkg by
iterating node_missing_peers, even in call paths where it is not consumed (e.g. final per-importer
and workspace-wide peer passes). This adds avoidable CPU/memory overhead to every resolve_peers
invocation and can regress performance on large graphs/workspaces.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[R617-625]

+        let mut missing_names_by_pkg: HashMap<String, std::collections::HashSet<String>> =
+            HashMap::new();
+        for (node_id, missing) in &self.node_missing_peers {
+            let Some(tree_node) = self.tree.dependencies_tree.get(node_id) else { continue };
+            missing_names_by_pkg
+                .entry(tree_node.resolved_package_id.clone())
+                .or_default()
+                .extend(missing.keys().cloned());
+        }
Evidence
The PR adds an unconditional pass that allocates/fills missing_names_by_pkg for every
resolve_peers result. But both the final per-importer peer pass and the workspace-wide peer pass
explicitly run with hoist_missing_scope: None, meaning this new map is typically unused in those
paths.

pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[617-631]
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[287-294]
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[426-428]
pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs[242-252]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Walker::walk()` always computes `missing_names_by_pkg`, but most call sites do not need it. This adds extra iteration and allocation to all `resolve_peers` calls.
## Issue Context
The field is needed for the importer hoist loop to persist the first walk’s missing-peer names, but the final per-importer pass and workspace-wide pass set `hoist_missing_scope: None` and do not record the per-package missing set.
## Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[617-631]
### Suggested approach
- Make `missing_names_by_pkg` computation conditional (e.g., behind a new option like `ResolvePeersOptions::collect_missing_names_by_pkg: bool`), or change the result field to `Option<...>` and only populate when requested.
- Update the hoist loop call site to request/populate it, leaving other call sites to skip the work.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Hoist scope clones large maps ✓ Resolved 🐞 Bug ➹ Performance
Description
The hoist loop constructs HoistMissingScope by cloning the full first_importer_by_pkg and
first_walk_missing_by_pkg maps on every resolve_peers(...) call. In large workspaces and/or
multiple hoist iterations, this adds avoidable O(packages) cloning overhead per iteration.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[R323-330]

+    let hoist_peers_opts = || {
+        let mut opts = peers_opts();
+        opts.hoist_missing_scope = Some(crate::resolve_peers::HoistMissingScope {
+            importer_id: importer_id.to_string(),
+            first_importer_by_pkg: ctx.workspace().first_importer_by_pkg(),
+            first_walk_missing_by_pkg: ctx.workspace().first_walk_missing_by_pkg(),
+        });
+        opts
Evidence
The hoist options closure pulls first_importer_by_pkg() and first_walk_missing_by_pkg() (both
cloning) for every peer-resolution attempt in the hoist loop.

pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[318-336]
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[597-619]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Inside the nested hoist loops, each `resolve_peers(...)` invocation recreates `HoistMissingScope` by calling `ctx.workspace().first_importer_by_pkg()` and `ctx.workspace().first_walk_missing_by_pkg()`, both of which clone entire `HashMap`s (and nested `HashSet`s). This can become a noticeable overhead on large dependency graphs.
## Issue Context
- For a given importer, the *ownership* map (`first_importer_by_pkg`) is stable.
- For non-first importers, `first_walk_missing_by_pkg` is typically stable as well (it was populated by earlier importers).
- The current design still clones both maps per hoist iteration.
## Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[318-336]
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[597-619]
- pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[101-148]
### Implementation direction
- Prefer making `ResolvePeersOptions::hoist_missing_scope` cheap to clone:
- Change it to `Option<Arc<HoistMissingScope>>` (or equivalent), and update `missing_issue_suppressed` to use `as_deref()`.
- Build the `Arc<HoistMissingScope>` once per importer (or once per outer hoist loop) instead of re-cloning the maps.
- If you want to keep the current type, at minimum snapshot the maps once per importer and reuse them, refreshing only when `record_first_walk_missing` could have changed the relevant data.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Cache-hit chain cloning 🐞 Bug ➹ Performance
Description
On peersCache hits, the walker deep-clones parent_pkg_ids_chain into a new Vec even when
hoist_missing_scope is None (the common case outside the hoist loop). This introduces avoidable
per-cache-hit allocations and string clones on a hot path.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[R891-903]

+                let mut chain_with_self = parent_pkg_ids_chain.to_vec();
+                chain_with_self.push(pkg.id.clone());
+                for (peer_name, info) in &missing {
+                    if self.missing_issue_suppressed(&chain_with_self, peer_name) {
+                        continue;
+                    }
+                    self.issues.missing.entry(peer_name.clone()).or_default().push(MissingPeer {
+                        wanted_range: info.range.clone(),
+                        optional: info.optional,
+                        meta_only: info.meta_only,
+                        parents: parents_from_chain(parent_chain_names, &pkg_name),
+                    });
+                }
Evidence
The cache-hit path clones the chain unconditionally, while the suppression helper immediately
returns false when hoist_missing_scope is absent—so the clone work is unnecessary whenever
suppression is disabled (which is explicitly the case in common call sites).

pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[878-904]
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[1139-1142]
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[287-294]
pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs[242-252]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In the cache-hit branch, `parent_pkg_ids_chain.to_vec()` clones the entire chain (including all `String`s) before calling `missing_issue_suppressed`. When `hoist_missing_scope` is `None`, `missing_issue_suppressed` returns `false` immediately, so the clone is wasted.
## Issue Context
Most `resolve_peers` executions run with `hoist_missing_scope: None` (final per-importer pass and workspace pass). The hoist loop is the only path that needs suppression.
## Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[878-904]
- pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[1139-1142]
### Suggested approach
- In the cache-hit branch, branch on `self.opts.hoist_missing_scope.is_some()`:
- If `None`: emit missing issues without building `chain_with_self`.
- If `Some`: build `chain_with_self` and apply suppression.
- This preserves behavior while removing the clone/allocation from unscoped runs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix pacquet parity: stable peer context for shared subtrees + lockfile array engines
🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Preserve first-importer peer context for shared package subtrees to match pnpm hoisting.
• Scope hoist-loop missing-peer issues using first-walk missing-peer snapshots (warnings unchanged).
• Record array-form engines as index-keyed entries in generated lockfile metadata.
Diagram
graph TD
ws["resolve_workspace"] --> imp["resolve_importer"] --> tree["resolve_dependency_tree"] --> ctx[("WorkspaceTreeCtx")]
ctx --> peers["resolve_peers (scoped hoist)"] --> peersws["resolve_peers_workspace"] --> lock["lockfile metadata"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Recompute subtree missing peers per importer occurrence
  • ➕ Conceptually simpler: no shared ownership/missing-peer snapshots to maintain
  • ➕ Avoids relying on pnpm-specific caching semantics
  • ➖ Diverges from pnpm behavior (the goal of lockfile parity)
  • ➖ Would reintroduce peer-suffix forking for shared subtrees across importers
  • ➖ More compute (re-walking shared subtrees repeatedly)
2. Key missing-peer reports by (pkg_id, importer_id) instead of first-owner
  • ➕ More explicit modeling of per-importer peer contexts
  • ➕ Could support future features that intentionally differ per importer
  • ➖ Does not match pnpm’s effective behavior for shared subtrees
  • ➖ More memory and bookkeeping; harder to reason about suppression rules
3. Change importer processing order to approximate pnpm concurrency
  • ➕ May address remaining parity gaps caused by different first-claimer ordering
  • ➕ Avoids suppressing issues based on foreign ownership
  • ➖ Non-deterministic or complex ordering rules; may regress reproducibility
  • ➖ Still needs a definition of how shared-subtree peer context is stabilized

Recommendation: Current approach is best for parity: explicitly capturing first-importer ownership and first-walk missing-peer names matches pnpm’s shared-subtree caching behavior while keeping the final workspace-wide peer pass unscoped so diagnostics remain complete.

Grey Divider

File Changes

Enhancement (1)
lib.rs Export 'HoistMissingScope' from resolving-deps-resolver +2/-2

Export 'HoistMissingScope' from resolving-deps-resolver

• Updates public re-exports to include 'HoistMissingScope', enabling scoped hoist behavior to be configured from callers.

pacquet/crates/resolving-deps-resolver/src/lib.rs


Bug fix (5)
dependencies_graph_to_lockfile.rs Preserve array-form 'engines' when writing lockfile metadata +23/-10

Preserve array-form 'engines' when writing lockfile metadata

• Extends 'engines' extraction to handle both object and array forms. Array entries are serialized as index-keyed map entries (matching 'Object.entries()' behavior) and '"*"' ranges are filtered out.

pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs


resolve_dependency_tree.rs Track first importer and first-walk missing peers for shared packages +60/-0

Track first importer and first-walk missing peers for shared packages

• Adds workspace-level maps to record which importer first resolved each package and the first peer-walk’s missing-peer names per package. Persists the first importer on first resolution (including reused-node paths) and exposes snapshot APIs consumed by peer-hoist scoping.

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs


resolve_importer.rs Scope hoist-loop peer resolution using first-importer subtree context +18/-1

Scope hoist-loop peer resolution using first-importer subtree context

• Threads the importer id into the tree context and constructs scoped 'ResolvePeersOptions' for the hoist loop. Records the first-walk missing-peer names from each 'resolve_peers' call so later importers can suppress satisfied peers under foreign-owned subtrees, matching pnpm’s behavior.

pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs


resolve_peers.rs Suppress foreign-owned subtree missing peers during hoist input computation +107/-14

Suppress foreign-owned subtree missing peers during hoist input computation

• Introduces 'hoist_missing_scope' and 'HoistMissingScope' to conditionally suppress missing-peer issues under subtrees first resolved by other importers when the first walk did not report that peer missing. Also returns 'missing_names_by_pkg' so the first walk’s missing-peer set can be persisted for later importers.

pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs


resolve_workspace.rs Initialize new peer-resolution option for workspace peer pass +1/-0

Initialize new peer-resolution option for workspace peer pass

• Sets 'hoist_missing_scope: None' for the final workspace-wide peer pass to keep missing-peer warnings complete across importers.

pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs


Tests (2)
tests.rs Add regression tests for shared-subtree peer context stability +194/-0

Add regression tests for shared-subtree peer context stability

• Adds two workspace-level tests covering both directions: (1) later importers must not hoist optional peers satisfied in the first importer’s context for shared subtrees, and (2) misses unsatisfied by the first importer remain visible and hoisted per importer.

pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs


tests.rs Update peer-resolution tests to include 'hoist_missing_scope' +1/-0

Update peer-resolution tests to include 'hoist_missing_scope'

• Adjusts test construction of 'ResolvePeersOptions' to populate the new 'hoist_missing_scope' field (set to 'None') to keep tests compiling and behavior unchanged.

pacquet/crates/resolving-deps-resolver/src/tests.rs


Grey Divider

Qodo Logo

@codecov-commenter

codecov-commenter commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.65421% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.01%. Comparing base (a751c7f) to head (6645acd).

Files with missing lines Patch % Lines
...rates/resolving-deps-resolver/src/resolve_peers.rs 81.63% 9 Missing ⚠️
...kage-manager/src/dependencies_graph_to_lockfile.rs 94.44% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12349      +/-   ##
==========================================
+ Coverage   87.97%   88.01%   +0.03%     
==========================================
  Files         292      292              
  Lines       36752    36831      +79     
==========================================
+ Hits        32332    32415      +83     
+ Misses       4420     4416       -4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_peers.rs`:
- Around line 613-620: The current construction of missing_names_by_pkg uses
self.node_missing_peers directly and therefore includes names that were
suppressed by missing_issue_suppressed(); change this so the exported map only
contains emitted (non-suppressed) misses: iterate the same nodes in
self.node_missing_peers but, for each candidate name, call
missing_issue_suppressed(...) (or use the existing post-suppression collection
if one exists) and only extend missing_names_by_pkg with names that are NOT
suppressed; keep the original raw map intact for cache compatibility if needed,
and ensure record_first_walk_missing() uses this filtered missing_names_by_pkg
so only actually emitted misses are recorded.
🪄 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: 0afd02d3-d0ee-429a-b0d8-f3e046d02bba

📥 Commits

Reviewing files that changed from the base of the PR and between a751c7f and d8d1141.

📒 Files selected for processing (8)
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
  • pacquet/crates/resolving-deps-resolver/src/lib.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: windows-latest / Node.js 22.13.0 / Test
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-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 fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plain String or &str in Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too via TryFrom<String> and/or FromStr and 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 infallible From<String> constructor
If upstream TypeScript occasionally constructs a branded string without validation, expose from_str_unchecked in 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 handwriting impl blocks; use manual impl only when conversion needs custom logic
Model TypeScript string literal unions (like 'auto' | 'always' | 'never') as Rust enums 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 in CODE_STYLE_GUIDE.md — imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Choose owned vs. borrowed parameters to minimize copies; widen to t...

Files:

  • pacquet/crates/resolving-deps-resolver/src/lib.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • 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/lib.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • 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/lib.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • 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/lib.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • 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/lib.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • 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/lib.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • 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/lib.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-06-12T07:04:03.703Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12227
File: pacquet/crates/package-manager/src/install.rs:708-719
Timestamp: 2026-06-12T07:04:03.703Z
Learning: In this pnpm-based repo’s package-manager Rust code, `ThrottledClient` uses a two-class semaphore scheduling policy (“reserved half” for tarball/download work and a higher-priority “latency class” for lockfile-verification metadata fetches). Therefore, when reviewing, do NOT flag reuse of the same `Arc<ThrottledClient>` for both downloads and verification as a budget/contention issue—this reuse is intentional and handled by the internal two-class “reserved permits” design. Creating a separate client instance would break the EMFILE guard behavior that relies on shared throttling.

Applied to files:

  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
🔇 Additional comments (4)
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs (1)

523-550: LGTM!

pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs (2)

294-397: LGTM!


399-487: LGTM!

pacquet/crates/resolving-deps-resolver/src/tests.rs (1)

2058-2058: LGTM!

Comment thread pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.854 ± 0.166 3.603 4.070 1.80 ± 0.11
pacquet@main 3.867 ± 0.160 3.646 4.085 1.80 ± 0.11
pnpr@HEAD 2.155 ± 0.111 1.955 2.317 1.00 ± 0.07
pnpr@main 2.145 ± 0.098 2.001 2.321 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.8542937427600004,
      "stddev": 0.1663006823820761,
      "median": 3.83097209646,
      "user": 3.6700447600000006,
      "system": 3.3939011999999997,
      "min": 3.6034235969600004,
      "max": 4.06960697696,
      "times": [
        4.05562806196,
        3.75916640496,
        3.6034235969600004,
        4.00087604696,
        4.06960697696,
        3.72044742896,
        3.85707251996,
        3.80487167296,
        3.68181780096,
        3.9900269169600002
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.8665537872600004,
      "stddev": 0.1599513087798402,
      "median": 3.8303776449600004,
      "user": 3.6318252599999994,
      "system": 3.353546299999999,
      "min": 3.6456865149600004,
      "max": 4.08526466296,
      "times": [
        4.08526466296,
        4.0422339779600005,
        3.9610131059600002,
        4.04923222996,
        3.7464899699600003,
        3.8995330639600003,
        3.6456865149600004,
        3.76122222596,
        3.72096233396,
        3.7538997869600004
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.15465338716,
      "stddev": 0.11139932981774285,
      "median": 2.15546691246,
      "user": 2.6744039599999994,
      "system": 2.9692128999999996,
      "min": 1.9550332169600002,
      "max": 2.31747437896,
      "times": [
        1.9550332169600002,
        2.0896727839600002,
        2.2281290189600003,
        2.0766136909600004,
        2.17252124196,
        2.3108007169600002,
        2.14794269296,
        2.16299113196,
        2.31747437896,
        2.08535499796
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.14489457266,
      "stddev": 0.09794585706557442,
      "median": 2.1392585259600003,
      "user": 2.61497376,
      "system": 2.9643091,
      "min": 2.00063144896,
      "max": 2.3212766419600004,
      "times": [
        2.3212766419600004,
        2.1431194979600003,
        2.00063144896,
        2.27225353696,
        2.1630682789600004,
        2.08437240696,
        2.1901501909600003,
        2.0513723929600003,
        2.1353975539600003,
        2.0873037769600002
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 685.8 ± 15.0 657.2 705.3 1.00
pacquet@main 710.9 ± 119.2 634.7 1047.0 1.04 ± 0.18
pnpr@HEAD 737.8 ± 16.3 713.8 766.7 1.08 ± 0.03
pnpr@main 725.1 ± 24.8 697.6 773.0 1.06 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.68582980652,
      "stddev": 0.014964070413959621,
      "median": 0.68604673222,
      "user": 0.40932401999999996,
      "system": 1.3349343599999999,
      "min": 0.65716534672,
      "max": 0.7053291137200001,
      "times": [
        0.68811548572,
        0.6754998267200001,
        0.68397797872,
        0.69847901372,
        0.6817756177200001,
        0.7053291137200001,
        0.67159353972,
        0.70081324272,
        0.69554889972,
        0.65716534672
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.7108750403200002,
      "stddev": 0.1192041250188281,
      "median": 0.6771219152200001,
      "user": 0.41367291999999994,
      "system": 1.3241659600000002,
      "min": 0.63471989572,
      "max": 1.04703364872,
      "times": [
        0.69149005372,
        0.66946065872,
        0.67726038672,
        0.6744386197200001,
        0.69262376272,
        0.6790362067200001,
        0.6769834437200001,
        0.66570372672,
        0.63471989572,
        1.04703364872
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.73782556852,
      "stddev": 0.016328708438087456,
      "median": 0.7378256347200001,
      "user": 0.41770791999999995,
      "system": 1.3645822599999997,
      "min": 0.71378478572,
      "max": 0.76671671072,
      "times": [
        0.7531320087200001,
        0.73039171972,
        0.74452486472,
        0.7426925617200001,
        0.73295870772,
        0.71378478572,
        0.7213753367200001,
        0.72349129572,
        0.76671671072,
        0.74918769372
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.72509847912,
      "stddev": 0.02477451131829827,
      "median": 0.7242726422200001,
      "user": 0.42159932,
      "system": 1.3524536600000001,
      "min": 0.6975949827200001,
      "max": 0.77297859672,
      "times": [
        0.7400168007200001,
        0.72461528172,
        0.7093076367200001,
        0.6992609457200001,
        0.72746160072,
        0.7239300027200001,
        0.7535275127200001,
        0.77297859672,
        0.70229143072,
        0.6975949827200001
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.967 ± 0.054 3.893 4.059 1.85 ± 0.10
pacquet@main 3.954 ± 0.056 3.877 4.055 1.84 ± 0.10
pnpr@HEAD 2.214 ± 0.134 2.031 2.418 1.03 ± 0.08
pnpr@main 2.150 ± 0.116 2.054 2.359 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.9673235434199996,
      "stddev": 0.05354998358504826,
      "median": 3.9643127378200003,
      "user": 3.6867505,
      "system": 3.3372533399999993,
      "min": 3.8927737218200003,
      "max": 4.05946073082,
      "times": [
        3.9852059838200002,
        3.91231102482,
        4.05946073082,
        4.02443331582,
        3.8927737218200003,
        3.95191841782,
        3.92786246782,
        4.01153002882,
        3.97670705782,
        3.93103268482
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.95416821702,
      "stddev": 0.055997129835163494,
      "median": 3.94423642982,
      "user": 3.6406913999999992,
      "system": 3.33638244,
      "min": 3.8773227388200002,
      "max": 4.05523308982,
      "times": [
        4.05523308982,
        3.94271391382,
        3.91039171182,
        3.89287000082,
        3.98329323982,
        3.8773227388200002,
        3.93121577982,
        4.01525108282,
        3.9457589458199998,
        3.98763166682
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.2139786959200007,
      "stddev": 0.1341124672700319,
      "median": 2.20826265232,
      "user": 2.4973158999999994,
      "system": 2.85523834,
      "min": 2.03069827582,
      "max": 2.41790598182,
      "times": [
        2.10841755882,
        2.2815025908199997,
        2.41790598182,
        2.14778418682,
        2.13695321482,
        2.27559641782,
        2.07344964982,
        2.26874111782,
        2.39873796482,
        2.03069827582
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.14987852512,
      "stddev": 0.11572345988356393,
      "median": 2.11821186232,
      "user": 2.4897672,
      "system": 2.87266304,
      "min": 2.0536368718199998,
      "max": 2.35887606082,
      "times": [
        2.05717861382,
        2.11849905082,
        2.11792467382,
        2.06924907182,
        2.06908161682,
        2.35887606082,
        2.1314284898199998,
        2.0536368718199998,
        2.35819577982,
        2.16471502182
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.213 ± 0.009 1.198 1.231 1.77 ± 0.09
pacquet@main 1.217 ± 0.026 1.185 1.258 1.78 ± 0.10
pnpr@HEAD 0.684 ± 0.034 0.663 0.775 1.00
pnpr@main 0.703 ± 0.067 0.668 0.890 1.03 ± 0.11
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.2132541139800002,
      "stddev": 0.008648309140393332,
      "median": 1.21331492058,
      "user": 1.24878058,
      "system": 1.6955334199999996,
      "min": 1.19761574408,
      "max": 1.2305783350800001,
      "times": [
        1.21914952008,
        1.21521877508,
        1.21467844908,
        1.2305783350800001,
        1.19761574408,
        1.20612703508,
        1.21195139208,
        1.21679610608,
        1.20875147808,
        1.21167430508
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.2166991921799999,
      "stddev": 0.0256538288944032,
      "median": 1.2122228540800002,
      "user": 1.22444948,
      "system": 1.71334542,
      "min": 1.18519198508,
      "max": 1.25828613508,
      "times": [
        1.2194337260800001,
        1.19037497108,
        1.25828613508,
        1.19408377508,
        1.18519198508,
        1.25803358608,
        1.22870432808,
        1.20843770708,
        1.21596520508,
        1.20848050308
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6841405261800002,
      "stddev": 0.033758684945402705,
      "median": 0.67505321808,
      "user": 0.36189208000000006,
      "system": 1.2812436200000001,
      "min": 0.66302294608,
      "max": 0.77544508708,
      "times": [
        0.68079969608,
        0.66302294608,
        0.6639444550800001,
        0.66305199808,
        0.6662648840800001,
        0.68093372908,
        0.77544508708,
        0.6874306920800001,
        0.69120503408,
        0.66930674008
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.70299622618,
      "stddev": 0.06675711282810422,
      "median": 0.68090260258,
      "user": 0.37289748000000006,
      "system": 1.2839881199999998,
      "min": 0.6684887500800001,
      "max": 0.89020248708,
      "times": [
        0.67976111208,
        0.6733283630800001,
        0.6701680720800001,
        0.6684887500800001,
        0.6779858440800001,
        0.70639539608,
        0.6909785740800001,
        0.69060957008,
        0.68204409308,
        0.89020248708
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.776 ± 0.044 2.711 2.858 4.07 ± 0.10
pacquet@main 2.737 ± 0.049 2.679 2.862 4.01 ± 0.11
pnpr@HEAD 0.683 ± 0.014 0.668 0.710 1.00
pnpr@main 0.706 ± 0.103 0.664 0.998 1.03 ± 0.15
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.7755422317600003,
      "stddev": 0.04430421004820169,
      "median": 2.7766755197600004,
      "user": 1.7283163399999997,
      "system": 2.0281468999999994,
      "min": 2.71138052676,
      "max": 2.85820950576,
      "times": [
        2.74207007176,
        2.71138052676,
        2.7420210877600004,
        2.77379272676,
        2.7885811027600003,
        2.7795583127600003,
        2.82597958876,
        2.7951025897600004,
        2.73872680476,
        2.85820950576
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.73712328766,
      "stddev": 0.048766958486508576,
      "median": 2.7295739397600003,
      "user": 1.7311760399999998,
      "system": 1.9540101,
      "min": 2.67942917076,
      "max": 2.86175891476,
      "times": [
        2.7021937627600003,
        2.7215449097600004,
        2.7265911427600003,
        2.71361375676,
        2.74024823976,
        2.67942917076,
        2.73657711876,
        2.7325567367600003,
        2.86175891476,
        2.75671912376
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6825864001599999,
      "stddev": 0.013849486006840763,
      "median": 0.67894553326,
      "user": 0.35816114000000004,
      "system": 1.2866195,
      "min": 0.6677972947599999,
      "max": 0.70989733076,
      "times": [
        0.67159060176,
        0.70096010376,
        0.70989733076,
        0.67421082676,
        0.6677972947599999,
        0.68286789476,
        0.69021946876,
        0.67851432476,
        0.67937674176,
        0.67042941376
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7064149409599999,
      "stddev": 0.10273173215320594,
      "median": 0.67387360326,
      "user": 0.36548094000000003,
      "system": 1.2794259,
      "min": 0.66373892876,
      "max": 0.99794614276,
      "times": [
        0.68379069576,
        0.66373892876,
        0.66867326476,
        0.66385274676,
        0.67285546076,
        0.67489174576,
        0.68255192776,
        0.67044322476,
        0.99794614276,
        0.68540527176
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12349
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
3,967.32 ms
(-46.55%)Baseline: 7,422.00 ms
8,906.40 ms
(44.54%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
2,775.54 ms
(-33.90%)Baseline: 4,199.17 ms
5,039.01 ms
(55.08%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,213.25 ms
(-7.18%)Baseline: 1,307.04 ms
1,568.45 ms
(77.35%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
3,854.29 ms
(-52.96%)Baseline: 8,193.23 ms
9,831.87 ms
(39.20%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
685.83 ms
(+2.75%)Baseline: 667.46 ms
800.95 ms
(85.63%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12349
Testbedpnpr

⚠️ 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-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,213.98 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
682.59 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
684.14 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,154.65 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
737.83 ms
🐰 View full continuous benchmarking report in Bencher

The hoist loop cloned the claim and first-walk-missing maps on every
iteration; suppression only consults entries recorded by earlier
importers (an importer's own loop additions are self-claimed and
exempt), so one Arc-shared snapshot per importer is equivalent.
Addresses a review comment on the PR.
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 12, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6645acd

@zkochan

zkochan commented Jun 12, 2026

Copy link
Copy Markdown
Member Author

Addressed the two findings from the Qodo review:

  1. Missing-peers union scoping — replied inline (thread on resolve_peers.rs): upstream's "first occurrence" is a concurrency race with no deterministic order to mirror; the union is the deterministic approximation that errs toward keeping peers hoistable, which is the safer divergence and matches the upstream outcome whenever the missing-context occurrence wins the race. The monorepo measurement shows no divergence attributable to it. Keeping as is.
  2. Hoist scope clones large maps — fixed in 6645acd: the scope is now snapshotted once per importer and shared across hoist iterations via Arc. This is equivalent because suppression only consults entries recorded by earlier importers — everything an importer's own loop adds is self-claimed and exempt from suppression.

Written by an agent (Claude Code, claude-fable-5).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants