Skip to content

fix(pacquet): per-level preferred-version fold + all-importers hoist rounds#12357

Merged
zkochan merged 11 commits into
mainfrom
lockfile-parity4
Jun 12, 2026
Merged

fix(pacquet): per-level preferred-version fold + all-importers hoist rounds#12357
zkochan merged 11 commits into
mainfrom
lockfile-parity4

Conversation

@zkochan

@zkochan zkochan commented Jun 12, 2026

Copy link
Copy Markdown
Member

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). So signed-varint's varint@~5.0.0 dedupes to the varint@5.0.0 its parent pinned as a sibling instead of drifting to 5.0.2. pacquet picked against a static seed only; besides varint/es-abstract, this turned out to drive the remaining jest/@types/node duplicate variants too.

  • The walk resolves a whole sibling level before any child subtree starts (upstream's postponed-resolution barrier): resolve_node splits into resolve_node_seed + walk_node_children.
  • Each level layers its versions onto a new PreferredVersionsOverlay (O(1) Arc-chained layers in resolver-base); the npm picker folds the per-name view in as plain version selectors at both registry seams.
  • The overlay's per-name view joins the per-wanted dedup cache key; lockfile-reuse subtrees keep the no-overlay path (exact pins).

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's spdx-expression-parse hoisted 3.0.1 where pnpm's barrier-visible map picks 4.0.0. resolve_importer_with_workspace is now an ImporterHoistState (init / run_required_round / hoist_optional_round) driven by resolve_workspace in upstream's exact phase order. Both implementations are deterministic here; the rule is identical.

Verification

  • New regression test child_resolution_prefers_parent_level_sibling_versions (fails with the fold disabled) + full resolving-*, package-manager, cli suites: 1,242 tests pass; clippy --deny warnings, rustfmt, typos clean.
  • Whole-monorepo diff vs fresh pnpm 11.6.0: 128 → 5 changed lines; consecutive pacquet runs byte-identical.

Remaining gaps (tracked in #12266)

  • Cycle-closing dependency edges (4 lines): pnpm's snapshots keep e.g. es-abstract: 1.24.2 under arraybuffer.prototype.slice@1.0.4 although the tree walk breaks the cycle; pacquet drops the edge. Next self-contained fix.
  • One claim-race line (@types/ssri@types/node 22 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 (missingPeersOfChildrenByPkgId promises 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

  • Improvements
    • Level-aware preferred-version overlays improve sibling/transitive version selection, reducing surprising downgrades and mismatches.
    • More consistent lockfile reuse and steadier handling of hoisted optional peers across workspace installs for more reliable, repeatable installs.
  • Tests
    • Added tests validating preferred-version propagation across levels, including aliased dependency scenarios.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Resolve 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.

Changes

Per-level preferred-versions overlay mechanism

Layer / File(s) Summary
Overlay data structure and ResolveOptions integration
pacquet/crates/resolving-resolver-base/src/resolve.rs, pacquet/crates/resolving-resolver-base/src/lib.rs
PreferredVersionsOverlay stores per-name preferred versions with optional parent layering and aggregation. ResolveOptions gains an optional preferred_versions_overlay field for per-level overlay passing.
Overlay merging in npm resolvers
pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs, pacquet/crates/resolving-npm-resolver/src/lib.rs, pacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rs, pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs
Adds overlay_merged_selectors to merge overlay-resolved versions into preferred selectors; npm registry pick paths now prefer overlay-derived selectors.
Resolve node refactor and cache-key extension
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
Introduce resolve_node_seed (NodeSeed::Done/Pending), extend WantedKey with overlay_versions: Vec<(String, Vec<String>)>, compute overlay lookup names and per-edge overlay_versions, and conditionally attach the pick-overlay to ResolveOptions on cache misses.
Two-phase child walking and overlay layering
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
extend_tree/walk_node_children now run phase 1 (resolve all children to seeds), compute level_versions, layer a grandchild overlay, then run phase 2 to walk grandchildren; adds level_versions and overlay_lookup_names helpers and preserves lockfile-reuse keying.
Test: overlay recording and behavior verification
pacquet/crates/resolving-deps-resolver/src/tests.rs
Adds OverlayRecordingResolver test stub and async tests asserting importer direct resolves see no overlay while child resolves see parent-level sibling version; verifies npm: alias overlay lookup by inner name.
Importer hoist-state refactor and hoist rounds
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
Introduce ImporterHoistState, move initialization into init, extract required-peers fixed-point loop into run_required_round, implement hoist_optional_round, and provide into_direct/into_result.
Workspace multi-importer hoist loop
pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
Initialize ImporterHoistState for all importers, run shared required/optional hoist rounds until stable, then collect per-importer direct inputs for subsequent workspace peer resolution.
Cargo manifest formatting
pacquet/crates/resolving-deps-resolver/Cargo.toml
Whitespace/line-wrapping reflow for dependency entries only.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pnpm/pnpm#11874: Overlapping changes to caching/memoization in resolve_dependency_tree.rs.
  • pnpm/pnpm#12128: Overlaps in WantedKey cache-key adjustments for resolution reuse.

"I'm a rabbit with a map of hops and codes,
I seed the level where sibling version bodes,
grandchildren follow with layered delight,
versions aligned, every subtree polite.
Hooray for overlays—hops and bytes tonight!"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly addresses the two main changes in the PR: per-level preferred-version folding and all-importers hoist rounds, matching the primary modifications in resolve_dependency_tree.rs and resolve_importer.rs/resolve_workspace.rs.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lockfile-parity4

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.00      7.5±0.20ms   577.0 KB/sec    1.01      7.6±0.53ms   569.9 KB/sec

@zkochan zkochan marked this pull request as ready for review June 12, 2026 12:24
@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 (8) 📘 Rule violations (0) 📎 Requirement gaps (1) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0)

Grey Divider


Action required

1. Wrong tuple field check 🐞 Bug ≡ Correctness
Description
In resolve_wanted_cached, the code checks cache_key.7.is_empty(), but WantedKey element 7 is
prior_key: Option<PkgNameVerPeer>, so this is a type error and prevents compilation. The overlay
guard should check the overlay_versions Vec field (index 8) instead.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[R1662-1670]

+    let overlay_opts;
+    let opts = if cache_key.7.is_empty() {
+        opts
+    } else {
+        let mut owned = opts.clone();
+        owned.preferred_versions_overlay = pick_overlay.map(Arc::clone);
+        overlay_opts = owned;
+        &overlay_opts
+    };
Evidence
WantedKey’s 8th element (index 7) is an Option<PkgNameVerPeer>, while the overlay versions are
stored in the 9th element (index 8), so calling is_empty() on cache_key.7 is a compile-time type
mismatch.

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[456-466]
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1646-1670]

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

## Issue description
`resolve_wanted_cached` uses `cache_key.7.is_empty()` to decide whether to clone `ResolveOptions` to attach `preferred_versions_overlay`, but tuple field 7 is `prior_key: Option<PkgNameVerPeer>`.
### Issue Context
`WantedKey` was extended to include `overlay_versions` as the last element; the guard should inspect that Vec (index 8).
### Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[456-466]
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1662-1670]
### Suggested change
- Replace `cache_key.7.is_empty()` with `cache_key.8.is_empty()` (or, more readably, destructure `cache_key` and check the `overlay_versions` element).

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


2. Warm cache-key arity mismatch 🐞 Bug ≡ Correctness
Description
warm_children_resolutions constructs a WantedKey tuple with only 8 elements even though WantedKey
now has 9 elements (it includes prior_key and overlay_versions). This is a build-breaking type
mismatch in the warmup path.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[R1768-1780]

+                let project_scope = is_project_relative_specifier(wanted.bare_specifier.as_deref())
+                    .then(|| ctx.base_opts.project_dir.clone());
+                let cache_key: WantedKey = (
+                    wanted.alias.clone(),
+                    wanted.bare_specifier.clone(),
+                    wanted.optional,
+                    wanted.injected,
+                    opts.pick_lowest_version,
+                    opts.published_by,
+                    project_scope,
+                    Vec::new(),
+                );
+                let _ = resolve_wanted_cached(ctx, resolver, &wanted, opts, None, cache_key).await;
Evidence
WantedKey is defined with 9 elements, but the warmup path’s tuple literal only supplies 8, omitting
the prior_key field entirely.

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[456-466]
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1751-1785]

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

## Issue description
`warm_children_resolutions` creates a `WantedKey` tuple that no longer matches the `WantedKey` type alias after it was extended to include `(prior_key, overlay_versions)`.
### Issue Context
This code path is not behind a cfg gate; the crate won’t compile until the tuple matches the updated alias.
### Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[456-466]
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1768-1780]
### Suggested change
Update the warmup cache key to include both trailing fields, e.g.:

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


3. Overlay cache-key collision 🐞 Bug ≡ Correctness
Description
resolve_node_seed flattens overlay versions across multiple possible lookup names into a single
sorted Vec that becomes part of the resolved_by_wanted cache key, so different overlays can produce
the same key and incorrectly reuse a prior pick. This is incorrect because the npm resolver consults
preferred_versions_overlay by the resolved package name (spec.name), so the per-name distribution of
overlay versions matters, not just the union.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[R1154-1165]

+    let overlay_versions: Vec<String> = pick_overlay
+        .as_ref()
+        .map(|overlay| {
+            let mut versions: Vec<String> = overlay_lookup_names(&wanted)
+                .iter()
+                .flat_map(|name| overlay.versions_for(name))
+                .map(str::to_string)
+                .collect();
+            versions.sort_unstable();
+            versions.dedup();
+            versions
+        })
Evidence
The cache key currently stores only a sorted union of overlay versions across multiple names, but
the overlay is consumed by the npm picker for a single resolved name (spec.name). Therefore two
overlays with different per-name version sets can collide in the cache and reuse the wrong resolve
result.

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1148-1176]
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1540-1579]
pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs[12-26]
pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs[361-375]

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

## Issue description
`resolve_node_seed` builds `overlay_versions` as a *union* of overlay versions across `overlay_lookup_names(&wanted)` and stores only that union in the `WantedKey`. Because the actual picker consults the overlay under a specific resolved name (`spec.name`), two different overlay states can collapse to the same cache key and cause a resolve result (picked version) computed under the “wrong” overlay to be reused.
## Issue Context
This happens when `overlay_lookup_names()` returns more than one name (alias + npm inner + jsr folded name). The current cache key representation loses which versions came from which name.
Concrete collision shape:
- Overlay A: versions_for("inner") = ["1.0.0"], versions_for("alias") = ["2.0.0"] → union ["1.0.0","2.0.0"]
- Overlay B: versions_for("inner") = ["2.0.0"], versions_for("alias") = ["1.0.0"] → union ["1.0.0","2.0.0"]
If the resolver consults the overlay under "inner" (via `spec.name`), Overlay A and B can produce different picks but collide in the cache.
## Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1148-1190]
Suggested implementation direction:
- Replace `overlay_versions: Vec<String>` with a stable, name-associated structure in the cache key, e.g. `Vec<(String, Vec<String>)>` or `BTreeMap<String, Vec<String>>`, where:
- each lookup name is included explicitly,
- its versions list is sorted/deduped,
- the outer collection is sorted by name.
- Use that same structure both to decide whether the overlay is “empty” (no relevant entries) and to key `resolved_by_wanted`.
- Keep the existing fast path (no overlay in key/options) when the per-name structure is empty.

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


View more (1)
4. Alias misses overlay folding ✓ Resolved 🐞 Bug ≡ Correctness
Description
resolve_node_seed computes the overlay view (and decides whether to pass the overlay into
ResolveOptions) using wanted.alias, but the npm resolver consults the overlay using
RegistryPackageSpec.name which differs from the alias for npm: aliases (and jsr:). This causes
the overlay to be skipped and can reuse an overlay-insensitive cached pick even when the per-level
fold should influence the version choice.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[R1139-1152]

+    // The overlay's view for this alias joins the cache key: the same
+    // range can legitimately pick different versions under levels
+    // that resolved different siblings. Empty for almost every edge,
+    // so the dedup keeps working where it matters.
+    let overlay_versions: Vec<String> = pick_overlay
+        .as_ref()
+        .zip(wanted.alias.as_deref())
+        .map(|(overlay, alias)| {
+            overlay.versions_for(alias).into_iter().map(str::to_string).collect()
+        })
+        .unwrap_or_default();
let cache_key: WantedKey = (
wanted.alias.clone(),
wanted.bare_specifier.clone(),
Evidence
The walker uses wanted.alias to query the overlay and to decide whether to attach the overlay to
ResolveOptions, but the npm resolver parses npm: aliases so the picker name (spec.name)
becomes the *inner* target package (e.g. lodash). Since the picker merges overlay selectors by
spec.name, using wanted.alias prevents overlay hits for alias dependencies and can lead to
different resolution results than intended.

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1139-1173]
pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rs[37-77]
pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier/tests.rs[62-78]
pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs[196-223]
pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs[316-370]
pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs[12-26]

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

## Issue description
Per-level preferred-version folding is currently keyed off `wanted.alias` in `resolve_node_seed`, but the npm resolver’s picker merges overlay selectors by the resolved package name (`spec.name`). For `npm:` alias dependencies (e.g. `"foo": "npm:lodash@^4"`) and the `jsr:` path (alias recorded as JSR name while `spec.name` is the folded npm name), `wanted.alias` != `spec.name`, so the overlay is treated as empty and not passed into `ResolveOptions`, and the overlay-aware cache key is also wrong.
### Issue Context
- The overlay lookup and cache-key component are computed via `wanted.alias`.
- The npm resolver parses `npm:` aliases such that `spec.name` becomes the *inner* target name (e.g. `lodash`), not the outer alias.
- The picker merges overlay selectors using `spec.name`, so skipping the overlay in the walker suppresses the fold.
### Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1139-1173]
- pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rs[37-77]
- pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs[196-223]
- pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs[12-26]
- pacquet/crates/resolving-deps-resolver/src/tests.rs[2754-2873]
### Suggested fix
1. In `resolve_node_seed`, derive the **picker name** used for overlay lookup from `(wanted.alias, wanted.bare_specifier)` using the same rules as the npm resolver:
- For non-`npm:` cases, the name is usually `wanted.alias`.
- For `npm:<innerName>@<range>` aliasing, the name should be the inner target (`innerName`), not the outer alias.
- Consider the `jsr:` flow as well, where the npm resolver picks by the folded npm name (`jsr_spec.spec.name`) while recording an alias.
2. Use that derived picker-name when:
- Building `overlay_versions` for the cache key.
- Deciding whether the overlay is empty / whether to clone `opts` and set `preferred_versions_overlay`.
3. Add a regression test covering an `npm:` alias scenario where a sibling pin should be visible to a child (similar to the new varint-style test, but with `foo: npm:pinned@~5.0.0` and a sibling `pinned@5.0.0`).

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



Remediation recommended

5. Named-registry overlay ignored 🐞 Bug ≡ Correctness
Description
overlay_lookup_names() only derives overlay lookup keys for the manifest alias, npm: aliases, and
jsr: specifiers, so named-registry specifiers like gh:@scope/pkg@^1 won’t include the resolved
package name (@scope/pkg). When that happens, resolve_wanted_cached() treats the overlay as empty
and does not pass preferred_versions_overlay into the resolver, so named-registry version picking
won’t see the per-level fold and may reuse an overlay-insensitive resolved_by_wanted cache entry.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[R1492-1564]

+/// The package names the npm picker may consult the preferred-versions
+/// overlay under for one wanted edge: the alias itself, plus the inner
+/// target of an `npm:` alias and the folded `@jsr/...` name of a
+/// `jsr:` specifier — mirroring the name derivation in the npm
+/// resolver's `parse_bare_specifier`, which keys its overlay merge by
+/// the resolved `spec.name` rather than the outer alias.
+fn overlay_lookup_names(wanted: &WantedDependency) -> Vec<String> {
+    let mut names: Vec<String> = Vec::new();
+    if let Some(alias) = wanted.alias.as_deref()
+        && !alias.is_empty()
+    {
+        names.push(alias.to_string());
+    }
+    let Some(bare) = wanted.bare_specifier.as_deref() else { return names };
+    if let Some(rest) = bare.strip_prefix("npm:") {
+        let alias_keeps_name = wanted
+            .alias
+            .as_deref()
+            .is_some_and(|alias| !alias.is_empty() && rest.parse::<node_semver::Range>().is_ok());
+        if !alias_keeps_name {
+            let last_at =
+                rest.bytes().enumerate().rev().find_map(|(i, b)| (b == b'@').then_some(i));
+            let inner = match last_at {
+                Some(idx) if idx >= 1 => &rest[..idx],
+                _ => rest,
+            };
+            if !inner.is_empty() && !names.iter().any(|name| name == inner) {
+                names.push(inner.to_string());
+            }
+        }
+    } else if bare.starts_with("jsr:")
+        && let Ok(Some(spec)) = pacquet_resolving_jsr_specifier_parser::parse_jsr_specifier(
+            bare,
+            wanted.alias.as_deref(),
+        )
+        && !names.contains(&spec.npm_pkg_name)
+    {
+        names.push(spec.npm_pkg_name);
+    }
+    names
+}
+
+/// Look the wanted edge up in the per-wanted dedup cache or run the
+/// resolver chain and the manifest-hook pipeline, caching the
+/// `Arc<ResolveResult>` under `cache_key`. Concurrent first-callers
+/// can both miss and resolve in parallel — the resolver's own
+/// per-cache-key fetch locker coalesces the network work, and the
+/// second `or_insert` loses the race harmlessly.
+async fn resolve_wanted_cached<Chain>(
+    ctx: &TreeCtx,
+    resolver: &Chain,
+    wanted: &WantedDependency,
+    opts: &ResolveOptions,
+    pick_overlay: Option<&Arc<PreferredVersionsOverlay>>,
+    cache_key: WantedKey,
+) -> Result<Arc<pacquet_resolving_resolver_base::ResolveResult>, ResolveDependencyTreeError>
+where
+    Chain: Resolver + ?Sized,
+{
+    let cached =
+        lock_recoverable(&ctx.workspace.resolved_by_wanted).get(&cache_key).map(Arc::clone);
+    if let Some(result) = cached {
+        return Ok(result);
+    }
+    let overlay_opts;
+    let opts = if cache_key.7.is_empty() {
+        opts
+    } else {
+        let mut owned = opts.clone();
+        owned.preferred_versions_overlay = pick_overlay.map(Arc::clone);
+        overlay_opts = owned;
+        &overlay_opts
+    };
Evidence
The deps-resolver derives overlay lookup names from the wanted edge, and only enables overlay
passing when that derived overlay-view is non-empty; however, named-registry parsing can produce a
resolved package name from the specifier body that differs from the manifest alias, and the
named-registry resolver consults overlays under that resolved name, so the overlay can be skipped
entirely.

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1492-1564]
pacquet/crates/resolving-npm-resolver/src/parse_bare_specifier.rs[201-286]
pacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rs[195-216]

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

## Issue description
`overlay_lookup_names()` does not include the resolved package name for named-registry specifiers (`<registryAlias>:<pkgName>@<selector>`). Because `resolve_wanted_cached()` only passes `preferred_versions_overlay` to the resolver when the overlay-view portion of the cache key is non-empty, named-registry edges can incorrectly resolve without the per-level preferred-version overlay.
### Issue Context
Named-registry resolution consults the overlay using the *resolved* `spec.name` (not the manifest alias). Therefore, if the overlay name-derivation misses that `spec.name`, the overlay won’t be applied and the cache key won’t distinguish overlay-sensitive vs overlay-insensitive resolutions.
### Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1492-1564]
Suggested implementation direction:
- Extend `overlay_lookup_names()` to also derive the inner package name for `:<body>` specifiers (named registry), similar to the logic in `parse_named_registry_specifier_to_registry_package_spec`:
- If `bare` contains `:` and is not a known protocol that shouldn’t use npm picking (`link:`, `file:`, `workspace:`, `npm:`, `jsr:`, `http(s)://`), parse the body to extract the package name portion (scoped/unscoped) and include it in the returned names.
- Ensure the extracted name is de-duplicated against existing entries.
- Add a small regression test analogous to the `npm_alias_child_consults_overlay_by_inner_name` test, but for a named-registry dependency where alias != resolved name, asserting the overlay is visible under the resolved name.

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


6. Warmup blocks phase barrier 🐞 Bug ➹ Performance
Description
extend_tree/walk_node_children await warm_children_resolutions, and warm_children_resolutions awaits
join_all over child resolves, so the phase-1 sibling barrier can be extended by warmup work rather
than being purely opportunistic overlap. This can increase install critical-path latency (and
network/CPU load) on large graphs where warmups outlast the slowest sibling seed resolution.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[R968-973]

+                let base_overlay = ctx.base_opts.preferred_versions_overlay.clone();
+                let seed =
+                    resolve_node_seed(ctx, resolver, wanted, &[], 0, false, reuse, base_overlay)
+                        .await?;
+                warm_children_resolutions(ctx, resolver, &seed).await;
+                Ok::<NodeSeed, ResolveDependencyTreeError>(seed)
Evidence
The seed phase awaits warm_children_resolutions before returning each seed future, and the warm
helper itself awaits join_all of child resolves; the doc comment explicitly states the goal is
overlap while the barrier waits, which isn’t true when the warm futures extend the barrier.

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[940-1000]
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1618-1670]

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

## Issue description
`warm_children_resolutions` is intended as speculative overlap, but it is awaited from the seed phase and itself awaits `join_all`, so it can become part of the phase barrier and extend critical-path latency.
## Issue Context
Because `extend_tree`/`walk_node_children` run a two-phase “postponed-resolution barrier” (resolve all seeds, then walk), any extra awaited work inside the seed futures delays the barrier completion.
## Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[940-999]
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1618-1670]
## Suggested fix
- If the intent is truly speculative warming: refactor so warmups are not awaited on the phase-1 critical path (e.g., enqueue/spawn warm tasks and let them run concurrently while phase 2 begins). This likely requires making the warm tasks own/clone what they need (or routing through a shared/background fetch queue) so they can outlive the stack frame.
- If you intentionally want to await warmups to maximize cache hits: update the doc comment to reflect that warmups are part of the barrier, and consider bounding the warmup work (limit concurrency and/or apply a timeout) so it can’t dominate phase-1 runtime.

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


7. Overlay clones selector map 🐞 Bug ➹ Performance
Description
overlay_merged_selectors clones the full per-package VersionSelectors map whenever the overlay
contributes versions for a name, allocating and copying even when only a few overlay versions need
to be added. This happens in the npm resolver pick hot path and can add noticeable allocation/CPU
overhead on large graphs with frequent overlay hits.
Code

pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs[R12-26]

+pub(crate) fn overlay_merged_selectors(
+    opts: &ResolveOptions,
+    name: &str,
+) -> Option<VersionSelectors> {
+    let versions = opts.preferred_versions_overlay.as_ref()?.versions_for(name);
+    if versions.is_empty() {
+        return None;
+    }
+    let mut selectors = opts.preferred_versions.get(name).cloned().unwrap_or_default();
+    for version in versions {
+        selectors
+            .entry(version.to_string())
+            .or_insert(VersionSelectorEntry::Plain(VersionSelectorType::Version));
+    }
+    Some(selectors)
Evidence
The overlay helper clones the selector map on overlay hits, and both the named-registry and npm
resolvers call it per registry pick and then pass the merged selectors into PickPackageOptions.

pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs[1-27]
pacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rs[195-216]
pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs[361-382]

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

## Issue description
When an overlay hit occurs, `overlay_merged_selectors()` clones `opts.preferred_versions.get(name)` into a new `VersionSelectors` map and inserts overlay versions into it. This creates per-resolve allocations and copies on a hot path.
## Issue Context
The callers invoke this for each registry pick and then pass the owned merged map into the picker.
## Fix Focus Areas
- pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs[1-27]
- pacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rs[195-216]
- pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs[361-382]
## Suggested fix
- Avoid materializing a merged `VersionSelectors` map. Instead, pass overlay versions separately down into the picker (e.g., extend `PickPackageOptions` / `PickPackageFromMetaOptions` with `preferred_overlay_versions: Option<Vec<&str>>` or an iterator) and have `prioritize_preferred_versions()` add overlay versions directly to the prioritizer with weight=1.
- If you must materialize: consider caching merged selectors per `(overlay_layer_id, name)` (where `overlay_layer_id` is derived from `Arc::as_ptr`) to amortize repeated clones within the same subtree level.

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


View more (2)
8. WantedKey overlay names lost 📎 Requirement gap ≡ Correctness
Description
The per-level overlay contribution to the dedup cache key in resolve_node_seed is computed as a
name-less union of versions, so distinct overlays with different name → versions mappings can
collide and incorrectly reuse a cached resolution. This can yield wrong package picks (e.g., swapped
preferred versions across alias vs inner npm:/folded jsr: names) and cause lockfile output drift
versus pnpm.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[R1148-1169]

+    // The overlay's view for this edge joins the cache key: the same
+    // range can legitimately pick different versions under levels
+    // that resolved different siblings. The view covers every name
+    // the picker may merge the overlay under (alias, `npm:` inner
+    // target, folded `jsr:` name). Empty for almost every edge, so
+    // the dedup keeps working where it matters.
+    let overlay_versions: Vec<String> = pick_overlay
+        .as_ref()
+        .map(|overlay| {
+            let mut versions: Vec<String> = overlay_lookup_names(&wanted)
+                .iter()
+                .flat_map(|name| overlay.versions_for(name))
+                .map(str::to_string)
+                .collect();
+            versions.sort_unstable();
+            versions.dedup();
+            versions
+        })
+        .unwrap_or_default();
let cache_key: WantedKey = (
wanted.alias.clone(),
wanted.bare_specifier.clone(),
Evidence
PR Compliance ID 1 requires pacquet’s lockfile output to match pnpm, but the current cache key logic
stores overlay influence as only a flattened Vec of versions produced by aggregating
versions_for(name) across all names returned by overlay_lookup_names (which may include alias,
npm: inner target, or folded jsr: name). Because this representation discards the association
between each lookup name and its versions, two different overlay states can produce the same
flattened version set and therefore the same WantedKey, allowing the resolver to reuse a
ResolveResult computed under a different name→version mapping and thus break parity via incorrect
resolution reuse.

Lockfile parity: pacquet install --lockfile-only reproduces pnpm 11.5.2 lockfile content (doc-2 equivalent)
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1148-1176]
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[420-429]
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1540-1579]

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

## Issue description
`resolve_node_seed`/`WantedKey` currently records overlay influence as a flattened `Vec<String>` of versions (a union across all lookup names returned by `overlay_lookup_names`), which drops the per-name association of versions. This allows different overlays with different `name → versions` mappings to collide on the same cache key and incorrectly reuse cached `ResolveResult`, potentially resolving the wrong version and causing pnpm parity/lockfile drift.
## Issue Context
- Compliance goal: pacquet must match pnpm lockfile output (PR Compliance ID 1); incorrect cached picks due to overlay-key collisions can create lockfile differences.
- `overlay_lookup_names` can return multiple names for a single wanted edge (alias + `npm:` inner target + folded `jsr:` npm name).
- The resolver consults overlay versions by a specific package name (`spec.name`), so the overlay is effectively a `name -> versions` mapping; flattening to a version set loses that mapping.
- Example collision class: overlays where alias vs inner target have swapped preferred versions (e.g., renamed→1.0.0,pinned→2.0.0 vs renamed→2.0.0,pinned→1.0.0) can hash to the same flattened version vector.
- Fix should preserve the existing “empty overlay fast-path” behavior (avoid extra allocations/key material when overlay contributes nothing).
## Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[420-429]
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1148-1176]
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1540-1579]

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


9. Overlay lookup de-dupe cost 🐞 Bug ➹ Performance
Description
PreferredVersionsOverlay::versions_for walks the parent chain and de-dupes via repeated
Vec::contains, and resolve_node_seed calls it to build a Vec for every overlay-aware resolve.
This can become a measurable hot-path cost on large graphs with deep overlay chains or many
preferred versions per name.
Code

pacquet/crates/resolving-resolver-base/src/resolve.rs[R180-193]

+    pub fn versions_for(&self, name: &str) -> Vec<&str> {
+        let mut versions: Vec<&str> = Vec::new();
+        let mut layer = Some(self);
+        while let Some(current) = layer {
+            if let Some(found) = current.entries.get(name) {
+                for version in found {
+                    if !versions.contains(&version.as_str()) {
+                        versions.push(version);
+                    }
+                }
+            }
+            layer = current.parent.as_deref();
+        }
+        versions
Evidence
The overlay lookup currently walks each layer and uses a linear membership check for de-duping, and
the walker calls it to build the cache-key’s overlay component. This is a direct, repeatable cost in
the resolver hot path when overlays are enabled.

pacquet/crates/resolving-resolver-base/src/resolve.rs[177-195]
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1139-1149]

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

## Issue description
`PreferredVersionsOverlay::versions_for` builds an owned `Vec<&str>` and de-dupes by scanning the accumulating vector (`versions.contains(...)`) while walking each overlay layer. This is called from the tree walker to build `overlay_versions` (a `Vec<String>`) for the per-wanted cache key, adding avoidable overhead.
### Issue Context
- The overlay is queried frequently during resolution.
- The current de-dupe is linear per insert, which can become costly when the overlay chain is deep or when many levels contribute versions for the same name.
### Fix Focus Areas
- pacquet/crates/resolving-resolver-base/src/resolve.rs[177-195]
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1139-1149]
### Suggested fix
- Keep output order (nearest level first), but track seen versions with a set:
- Maintain `Vec<&str>` for order + `HashSet<&str>` (or `BTreeSet<String>` / `HashSet<String>` depending on lifetimes) for O(1) membership.
- Alternatively, add a lightweight `has_versions_for(name)`/`versions_for_nonempty(name)` API that can early-exit once a hit is found, to avoid walking the full chain when callers only need emptiness.
- Consider caching flattened per-name results in the overlay (e.g., `OnceLock<HashMap<String, Arc<Vec<String>>>>`) if profiling shows repeated queries for the same names.

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



Informational

10. Stale preferred-versions comment 🐞 Bug ⚙ Maintainability
Description
The ResolveImporterOptions::all_preferred_versions field docs say optional-peer hoisting reads a
snapshot taken before run-resolved versions are folded in, but ImporterHoistState updates
all_preferred_versions before hoist_optional_round consults it. This mismatch is misleading for
future maintenance/tests of hoist behavior.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[R399-408]

+        // Both hoists read the run-extended preferred-versions map:
+        // upstream folds every resolved package's version into
+        // `ctx.allPreferredVersions`
+        // ([`resolveDependencies.ts#L1483-L1488`](https://github.com/pnpm/pnpm/blob/01b3d45ddb/installing/deps-resolver/src/resolveDependencies.ts#L1483-L1488))
+        // and consults it for the optional hoist after each wave.
+        // Refreshed per round so it carries every importer's versions,
+        // not just this importer's.
+        update_preferred_versions_with_ctx(&self.ctx, &mut self.all_preferred_versions);
+        // The hoist input must not see missing peers declared inside a
+        // subtree first resolved under an earlier importer — upstream
Evidence
The docs explicitly state optional hoisting reads a snapshot before run-resolved versions are folded
in, but the implementation updates all_preferred_versions in run_required_round and then uses
that same map in hoist_optional_round.

pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[78-92]
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[399-406]
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[493-508]

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

## Issue description
`ResolveImporterOptions::all_preferred_versions` documentation claims optional-peer hoisting uses a pre-fold snapshot, but the current control flow updates `all_preferred_versions` (via `update_preferred_versions_with_ctx`) before `get_hoistable_optional_peers` is called.
## Issue Context
This is a docs/maintainability issue: the code is internally consistent, but the comment now contradicts the behavior after the hoist-round refactor.
## Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[78-91]
- pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[399-408]
- pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[505-508]

ⓘ 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

Fold per-level resolved versions into preferred versions during tree resolution
🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• 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.
Diagram
graph 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"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Clone and mutate preferred_versions per level
  • ➕ Simpler mental model (a single map passed down)
  • ➕ Avoids adding a new overlay type to resolver-base
  • ➖ Higher allocation/copy cost (maps can be large)
  • ➖ Harder to share across async tasks without more cloning/locking
2. Keep overlay outside ResolveOptions (walker-only hint)
  • ➕ Avoids expanding the public-ish ResolveOptions surface area
  • ➕ Keeps overlay concerns localized to deps-resolver
  • ➖ Requires threading extra parameters through all resolver layers anyway
  • ➖ Harder to reuse the same mechanism across different resolvers/pickers
3. Do not include overlay view in wanted-dedup cache key
  • ➕ Maximizes cache hits and reduces resolver calls
  • ➖ Incorrect: same (name, range) can validly pick different versions per level
  • ➖ Would reintroduce the parity bug via cross-level memoization

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).

Grey Divider

File Changes

Enhancement (4)
lib.rs Wire in preferred overlay helper module +1/-0

Wire in preferred overlay helper module

• Registers the new preferred_overlay module so npm resolver components can merge overlay selectors into the picker inputs.

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


preferred_overlay.rs Merge overlay versions into preferred selectors +27/-0

Merge overlay versions into preferred selectors

• Introduces overlay_merged_selectors(opts, name) that converts overlay versions into plain 'version' selectors and merges them with any existing preferred selectors. Avoids allocations when the overlay has no entries for the name.

pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs


lib.rs Re-export PreferredVersionsOverlay from resolver-base +5/-5

Re-export PreferredVersionsOverlay from resolver-base

• Exports the new PreferredVersionsOverlay type so downstream crates (deps-resolver, npm-resolver) can share a single definition.

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


resolve.rs Add PreferredVersionsOverlay type and ResolveOptions field +53/-0

Add PreferredVersionsOverlay type and ResolveOptions field

• Defines an Arc-chained PreferredVersionsOverlay with O(1) layering and per-name version lookup across the chain. Extends ResolveOptions with an optional preferred_versions_overlay used during tree walking (direct deps remain overlay-free).

pacquet/crates/resolving-resolver-base/src/resolve.rs


Bug fix (3)
resolve_dependency_tree.rs Two-phase level walk and per-level preferred-version overlay +230/-13

Two-phase level walk and per-level preferred-version overlay

• Splits node processing into a seed phase (resolve + memoize + dedup) and a child-walk phase to enforce a sibling-level barrier before recursing. Builds a PreferredVersionsOverlay from each fully-resolved level and passes it to child picks; also adds overlay-derived data to the WantedKey to keep memoization correct across different overlays.

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


named_registry_resolver.rs Consult overlay-derived selectors during registry picks +5/-1

Consult overlay-derived selectors during registry picks

• Augments preferred_version_selectors to first consider overlay_merged_selectors(name), falling back to the static preferred_versions map when no overlay applies.

pacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rs


npm_resolver.rs Consult overlay-derived selectors during npm picks +5/-1

Consult overlay-derived selectors during npm picks

• Mirrors the named_registry_resolver change: fold per-level overlay versions into the preferred selectors used by pick_package, with fallback to the static map.

pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs


Tests (1)
tests.rs Regression test for per-level preferred version folding +122/-0

Regression test for per-level preferred version folding

• Adds a resolver stub that records the preferred_versions_overlay view at resolve time. Verifies importer-level direct deps see no overlay, while a child dependency sees a sibling-resolved pinned version in its overlay.

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


Grey Divider

Qodo Logo

Comment thread pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs Outdated

@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_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

📥 Commits

Reviewing files that changed from the base of the PR and between ce9c096 and 2f998fc.

📒 Files selected for processing (8)
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/resolving-npm-resolver/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rs
  • pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs
  • pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs
  • pacquet/crates/resolving-resolver-base/src/lib.rs
  • pacquet/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 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-npm-resolver/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs
  • pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs
  • pacquet/crates/resolving-resolver-base/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rs
  • pacquet/crates/resolving-resolver-base/src/resolve.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/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.rs
  • pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs
  • pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs
  • pacquet/crates/resolving-resolver-base/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rs
  • pacquet/crates/resolving-resolver-base/src/resolve.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.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-npm-resolver/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs
  • pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs
  • pacquet/crates/resolving-resolver-base/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rs
  • pacquet/crates/resolving-resolver-base/src/resolve.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.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-npm-resolver/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs
  • pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs
  • pacquet/crates/resolving-resolver-base/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rs
  • pacquet/crates/resolving-resolver-base/src/resolve.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.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-npm-resolver/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs
  • pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs
  • pacquet/crates/resolving-resolver-base/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rs
  • pacquet/crates/resolving-resolver-base/src/resolve.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.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/tests.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/tests.rs
  • pacquet/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!

Comment thread pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs Outdated
@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.979 ± 0.150 3.835 4.293 2.05 ± 0.16
pacquet@main 3.967 ± 0.142 3.803 4.198 2.05 ± 0.16
pnpr@HEAD 1.939 ± 0.135 1.719 2.159 1.00
pnpr@main 1.960 ± 0.122 1.769 2.112 1.01 ± 0.09
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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 484.3 ± 8.5 471.2 494.3 1.00
pacquet@main 489.5 ± 8.6 477.6 501.4 1.01 ± 0.03
pnpr@HEAD 572.4 ± 90.2 510.2 805.0 1.18 ± 0.19
pnpr@main 521.1 ± 11.0 505.5 540.8 1.08 ± 0.03
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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.882 ± 0.037 3.836 3.963 1.97 ± 0.14
pacquet@main 3.794 ± 0.043 3.740 3.863 1.92 ± 0.14
pnpr@HEAD 1.972 ± 0.144 1.828 2.233 1.00
pnpr@main 2.006 ± 0.125 1.784 2.236 1.02 ± 0.10
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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 990.2 ± 17.1 975.4 1032.9 1.93 ± 0.05
pacquet@main 1014.5 ± 11.7 997.0 1031.6 1.98 ± 0.04
pnpr@HEAD 512.1 ± 8.4 503.3 532.2 1.00
pnpr@main 579.9 ± 107.6 506.0 801.0 1.13 ± 0.21
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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.733 ± 0.029 2.695 2.800 5.21 ± 0.09
pacquet@main 2.549 ± 0.017 2.528 2.577 4.86 ± 0.07
pnpr@HEAD 0.543 ± 0.006 0.536 0.553 1.03 ± 0.02
pnpr@main 0.524 ± 0.007 0.511 0.533 1.00
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
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12357
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,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%)
🐰 View full continuous benchmarking report in Bencher

@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 c847297

@zkochan zkochan changed the title fix(pacquet): fold each level's resolved versions into the children's preferred versions fix(pacquet): per-level preferred-version fold + all-importers hoist rounds Jun 12, 2026
@codecov-commenter

codecov-commenter commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.76876% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.15%. Comparing base (9b35a60) to head (ef28e45).

Files with missing lines Patch % Lines
...lving-deps-resolver/src/resolve_dependency_tree.rs 96.28% 10 Missing ⚠️
...es/resolving-npm-resolver/src/preferred_overlay.rs 93.75% 1 Missing ⚠️
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.
📢 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.

@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 c66c2e2

@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 fc24d66

Comment thread pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs Outdated

@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.

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

2876-2948: ⚡ Quick win

Add a jsr: overlay regression next to the npm alias case.

overlay_lookup_names now has a separate jsr: name-derivation branch in pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs (Lines 1570-1577), but this module only locks down the plain and npm: flows. A small jsr: 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

📥 Commits

Reviewing files that changed from the base of the PR and between c847297 and fc24d66.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • pacquet/crates/resolving-deps-resolver/Cargo.toml
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/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 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/tests.rs
  • pacquet/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.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/tests.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/tests.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/tests.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/tests.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/tests.rs
  • pacquet/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

@zkochan zkochan force-pushed the lockfile-parity4 branch from fc24d66 to 372546a Compare June 12, 2026 14:02
@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 372546a

@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.

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

2876-2949: ⚡ Quick win

Add a jsr: overlay-parity regression in this module.

These new tests cover plain and npm: alias overlay propagation, but the resolver change also introduced a jsr: name-folding branch in overlay lookup. Adding a jsr: 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

📥 Commits

Reviewing files that changed from the base of the PR and between fc24d66 and 372546a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • pacquet/crates/resolving-deps-resolver/Cargo.toml
  • 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_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/resolving-npm-resolver/src/lib.rs
  • pacquet/crates/resolving-npm-resolver/src/named_registry_resolver.rs
  • pacquet/crates/resolving-npm-resolver/src/npm_resolver.rs
  • pacquet/crates/resolving-npm-resolver/src/preferred_overlay.rs
  • pacquet/crates/resolving-resolver-base/src/lib.rs
  • pacquet/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 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/tests.rs
  • pacquet/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.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/tests.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/tests.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/tests.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/tests.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/tests.rs
  • pacquet/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!

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12357
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
1,972.50 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
542.62 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
512.05 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
1,938.90 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
572.38 ms
🐰 View full continuous benchmarking report in Bencher

@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 e85f6c9

@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_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

📥 Commits

Reviewing files that changed from the base of the PR and between 372546a and e85f6c9.

📒 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 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_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

@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 185c85a

@zkochan zkochan force-pushed the lockfile-parity4 branch from 185c85a to 9a71282 Compare June 12, 2026 19:27
@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 9a71282

zkochan added 11 commits June 12, 2026 21:33
…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.
@zkochan zkochan force-pushed the lockfile-parity4 branch from 9a71282 to ef28e45 Compare June 12, 2026 19:47
@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 ef28e45

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