Skip to content

perf(pacquet): lazy children realization in dependency tree#11915

Merged
zkochan merged 6 commits into
mainfrom
perf/lazy-children
May 24, 2026
Merged

perf(pacquet): lazy children realization in dependency tree#11915
zkochan merged 6 commits into
mainfrom
perf/lazy-children

Conversation

@zkochan

@zkochan zkochan commented May 24, 2026

Copy link
Copy Markdown
Member

Summary

Ports upstream pnpm's lazy children thunk on DependenciesTreeNode to pacquet. Picks up the third optimization from #11907 — the one explicitly deferred from #11906 (purePkgs + peersCache).

Upstream's DependenciesTreeNode.children is a (() => ChildrenMap) | ChildrenMap sum type. The first walk of a pkgIdWithPatchHash records its children in childrenByParentId and seeds the lazy thunk; every revisit reuses that entry and defers the per-occurrence subtree expansion to buildTree during peer resolution.

Pacquet was eagerly fanning out every revisit, producing one fresh NodeId for every reachable occurrence even though peer resolution only ever descends into the ones it can't short-circuit through purePkgs. On deep trees this dominated resolve_importer.

Approach

  • TreeChildren enum with Realized(BTreeMap<String, NodeId>) and Lazy { parent_ids: Arc<Vec<String>> } arms, mirroring upstream's union.
  • New children_by_id: HashMap<String, Arc<Vec<ChildEdge>>> on ResolvedTree (upstream's childrenByParentId), populated by the first walk and immutable thereafter. ChildEdge carries the install alias, resolved child pkgIdWithPatchHash, and the optional bit so per-occurrence realization keeps ResolvedPackage::optional AND-folded correctly.
  • resolve_node writes TreeChildren::Lazy { parent_ids: ancestor chain } on revisits and short-circuits the recursive descent.
  • Peer-resolver Walker gains realize_children(&mut self, NodeId) -> &BTreeMap<String, NodeId> which expands a lazy node on demand, allocating per-occurrence NodeIds (NodeId::leaf for pure leaves, NodeId::next otherwise) and applying upstream's parentIdsContainSequence cycle break against the stored parent_ids chain.
  • resolve_peers takes &mut ResolvedTree so realization can flip Lazy → Realized in place. Pure subtrees that the peer resolver short-circuits via purePkgs are never realized.

Bench (astro deep tree, cold store)

before after speedup
tree nodes 74,940 4,069 ~18× smaller
resolve_importer phase 11.6s 8.2s ~1.42×

Wall-clock total is in the noise — create_virtual_store is network/IO-bound and varies more than the resolver win. The optimization targets the resolver phase specifically, which is what #11907 calls out as the remaining hotspot.

Tests

All 54 tests in pacquet-resolving-deps-resolver pass, including the peer-resolution coverage added in #11906 (revisit_resolves_peer_in_one_occurrence_misses_in_other, cyclic_peer_dependencies_resolve_cleanly, etc.) which exercise both Realized and Lazy descent paths.

Refs #11907.

Test plan

  • cargo nextest run -p pacquet-resolving-deps-resolver
  • just check
  • just lint
  • cargo fmt --check
  • typos pacquet registry
  • Astro fixture install end-to-end (verified locally; resolver phase 1.42× faster, tree 18× smaller)

Written by an agent (Claude Code, claude-opus-4-7).

Summary by CodeRabbit

  • Performance Improvements

    • Per-package child caching and on-demand (lazy) child realization improve revisit speed and large-graph walks.
  • Refactor

    • Traversal updated to support in-place realization, consistent leaf classification across visits, and more robust peer resolution.
  • New Features

    • Added a reusable child-edge descriptor type exposed by the library for consumers.
  • Tests

    • Updated and added tests to reflect new traversal, lazy-realization behavior, and mutable-resolution calling conventions.

Review Change Stack

… until peer resolution

Mirrors upstream pnpm's lazy `children` thunk on `DependenciesTreeNode`:
revisits of a `pkgIdWithPatchHash` no longer recurse to fan out a fresh
NodeId subtree eagerly. Instead the tree node records
`TreeChildren::Lazy { parent_ids }` and the peer resolver allocates
per-occurrence child NodeIds on first descent via `realize_children`,
applying the same `parentIdsContainSequence` cycle break upstream uses
in `buildTree`.

Pure subtrees that the peer resolver already short-circuits through
`purePkgs` (ported in #11906) now skip realization entirely — the tree
never gets walked past the first occurrence of those packages.

Bench (astro deep tree, cold store, single resolver phase):
- tree nodes: 74,940 → 4,069 (~18× smaller)
- `resolve_importer`: 11.6s → 8.2s (~1.42× faster)

Refs #11907.
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown

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

The resolver now caches per-package ChildEdge vectors and emits TreeChildren::Lazy on revisits; resolve_peers accepts a mutable ResolvedTree and realizes lazy children on-demand during traversal, updating the tree in-place and preserving deterministic NodeId/leaf behavior.

Changes

Lazy child realization for dependency resolution

Layer / File(s) Summary
TreeChildren and ChildEdge type definitions
pacquet/crates/resolving-deps-resolver/src/resolved_tree.rs
TreeChildren enum added (Realized / Lazy with parent_ids); ChildEdge struct added; DependenciesTreeNode.children updated to use TreeChildren and ResolvedPackage.is_leaf added.
Children cache infrastructure in TreeCtx and ResolvedTree
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs, pacquet/crates/resolving-deps-resolver/src/resolved_tree.rs
Adds children_by_id cache (Mutex<HashMap<...>> in TreeCtx / HashMap<String, Arc<Vec>> in ResolvedTree); initialized in TreeCtx::new and carried into ResolvedTree via into_resolved_tree/snapshot.
Revisit detection and lazy child population in resolve_node
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
resolve_node detects revisits (is_revisit); on first visit it resolves manifest children and populates children_by_id[id]; on revisits it emits TreeChildren::Lazy { parent_ids } instead of re-resolving.
Mutable ResolvedTree threading through resolve_importer
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
Intermediate snapshot and final resolved_tree are now passed as &mut into resolve_peers, enabling in-walk mutation for lazy realization.
resolve_peers API and Walker wiring
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
resolve_peers signature changed to accept &mut ResolvedTree; Walker now holds a mutable reference to the tree to permit in-place realization; imports updated and direct deps cloned before recursion to avoid borrow conflicts.
On-demand child realization in resolve_peers walker
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
Added Walker::realize_children that expands TreeChildren::Lazy using children_by_id, filters cycles using parent_ids, allocates deterministic NodeId for leaves vs non-leaves, updates dependencies_tree entries with lazy children, and flips nodes to TreeChildren::Realized; walk logic iterates realized children_map.
Test updates for mutable API and realized() accessor
pacquet/crates/resolving-deps-resolver/src/tests.rs
Dependency-tree and peer-resolution tests updated to bind trees as mut, call resolve_peers(&mut tree, ...), and use children.realized() when inspecting materialized child maps; adds two regression tests validating revisit/realization invariants.
Public API export
pacquet/crates/resolving-deps-resolver/src/lib.rs
Re-exports ChildEdge from resolved_tree.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pnpm#11874: Modifies resolve_dependency_tree to memoize per-package child extraction and revisit behavior; strongly related.
  • pnpm/pnpm#11906: Overlaps on resolve_peers Walker/traversal and peer-resolution changes.
  • pnpm/pnpm#11791: Related work touching pkgIdWithPatchHash identity and caching layers.

Poem

🐰 I cached the children in a snug little heap,
Revisit comes softly, the tree wakes from sleep,
Walker stretches branches and realizes a few,
Leaves stay light where they ought — yet occurrences true,
A happy hop for reuse, and tests pass in a sweep.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: implementing lazy children realization in the dependency tree for performance optimization.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/lazy-children

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 May 24, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.03      8.5±0.41ms   513.0 KB/sec    1.00      8.2±0.05ms   528.6 KB/sec

@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: 2

🤖 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 543-607: The revisit branch returns TreeChildren::Lazy which stops
walking the cached subtree and thus never re-applies ChildEdge.optional to
descendants; fix by recording and reusing the per-package children_by_id
(Vec<crate::resolved_tree::ChildEdge>) when creating the lazy entry and, in the
revisit path (where TreeChildren::Lazy is constructed/consumed), iterate the
cached children_by_id for this package and propagate each ChildEdge.optional
combined with current_is_optional into the cached subtree (updating
ResolvedPackage.optional for descendant pkg ids) before returning so transitive
optional flags reflect the revisiting path; key symbols: TreeChildren::Lazy,
children_by_id, ChildEdge.optional, child_specs_by_id, resolve_node,
ResolvedPackage.optional.

In `@pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs`:
- Around line 861-867: The lazy-realization branch currently recomputes
leaf-ness using children_by_id and peer_dependencies (edge.pkg_id,
tree.children_by_id, tree.packages) which is weaker than the eager walk's
pkg_is_leaf logic and causes inconsistent NodeId assignment (NodeId::next vs
NodeId::leaf). Change this branch to reuse the first-walk leaf classification
instead of recomputing: call or consult the same
resolve_dependency_tree::pkg_is_leaf result used during the eager walk (or read
the persisted leaf-flag recorded during the first walk) and base the
child_node_id decision on that value so NodeId::leaf / NodeId::next remain
consistent.
🪄 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: 22b24531-e170-4c97-bfde-a487ceec604e

📥 Commits

Reviewing files that changed from the base of the PR and between b9de85d and ae4eecd.

📒 Files selected for processing (5)
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolved_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: When porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(), or streamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plain String or &str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only via TryFrom<String> and/or FromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallible From<String> (and From<&str> when convenient).
If upstream occasionally constructs without validation, expose from_str_unchecked as an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization.
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls. Fall back to manual impl only when conversion needs custom logic.
String-literal unions should become enums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies.
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;. Two forms stay allowed: external-crate preludes like use rayon::prelude::*; and root-of-module re-...

Files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolved_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
🧠 Learnings (3)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

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

Applied to files:

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

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolved_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
🔇 Additional comments (2)
pacquet/crates/resolving-deps-resolver/src/resolved_tree.rs (1)

52-78: LGTM!

Also applies to: 169-170, 182-232

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

175-177: LGTM!

Also applies to: 256-257

Comment thread pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs Outdated
- Re-export `TreeChildren` and `ChildEdge` from the crate root so the
  intra-doc links from public docs (`resolve_peers`, `ResolvedTree`)
  resolve. They were `pub` on the enum/struct but unreachable because
  `resolved_tree` is a private module.
- Drop the `[Walker::realize_children]` / `[Walker::pure_pkgs]`
  intra-doc references from `resolve_peers`'s public doc — `Walker`
  is private, so the links failed under `--document-private-items`
  with `-D rustdoc::private_intra_doc_links`. The prose still names
  the items in plain backticks.
- Rename closure params `p` and let binding `v` in `realize_children`
  to satisfy `perfectionist::single-letter-{closure-param,let-binding}`.
@codecov-commenter

codecov-commenter commented May 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.49593% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.66%. Comparing base (f5d7723) to head (8e127a5).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...rates/resolving-deps-resolver/src/resolved_tree.rs 50.00% 4 Missing ⚠️
...rates/resolving-deps-resolver/src/resolve_peers.rs 94.33% 3 Missing ⚠️
...lving-deps-resolver/src/resolve_dependency_tree.rs 98.27% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #11915   +/-   ##
=======================================
  Coverage   87.66%   87.66%           
=======================================
  Files         214      215    +1     
  Lines       25390    25458   +68     
=======================================
+ Hits        22258    22319   +61     
- Misses       3132     3139    +7     

☔ View full report in Codecov by Sentry.
📢 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.

…lization

Mirrors upstream's
[`ResolvedPackage.isLeaf`](https://github.com/pnpm/pnpm/blob/b9de85dcb6/installing/deps-resolver/src/resolveDependencies.ts#L250)
field: `pkg_is_leaf(&result)` is computed once on the first walk and
stored on `ResolvedPackage::is_leaf`; the peer-resolver's
`realize_children` reads it back instead of inferring leaf-ness from
`children_by_id.is_empty() + peer_dependencies.is_empty()`.

The inferred check was a weaker approximation — a package with a
missing manifest (e.g. a git/tarball/local resolution where `result.
manifest` is None) lands on `pkg_is_leaf == false` in the eager walk
but on `is_leaf == true` in the realize path, which would collapse
distinct per-occurrence `NodeId`s onto a shared `NodeId::leaf` and
break the peer resolver's per-call-site state.

Matches upstream's
[`buildTree` consumption](https://github.com/pnpm/pnpm/blob/b9de85dcb6/installing/deps-resolver/src/resolveDependencyTree.ts#L381):
`ctx.resolvedPkgsById[child.id].isLeaf` is the source of truth, not
recomputed per realization.
@github-actions

github-actions Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.072 ± 0.087 1.973 2.277 1.00 ± 0.05
pacquet@main 2.067 ± 0.066 1.979 2.171 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.0724993834800003,
      "stddev": 0.08733909898777982,
      "median": 2.04032864608,
      "user": 2.81616024,
      "system": 3.4754287400000003,
      "min": 1.9727952250799998,
      "max": 2.27714572908,
      "times": [
        2.12439873208,
        1.9727952250799998,
        2.02690741808,
        2.05940645108,
        2.02747512008,
        2.0167917500800003,
        2.03007836008,
        2.27714572908,
        2.05057893208,
        2.13941611708
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.06675633228,
      "stddev": 0.06554897301297306,
      "median": 2.05729976258,
      "user": 2.78829304,
      "system": 3.44529774,
      "min": 1.97921768108,
      "max": 2.17138608608,
      "times": [
        2.13321695608,
        2.13195336808,
        2.01292955808,
        2.10430668108,
        2.05149099108,
        2.02394829208,
        1.9960051750799999,
        2.17138608608,
        2.06310853408,
        1.97921768108
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 686.6 ± 22.6 657.7 738.1 1.01 ± 0.04
pacquet@main 678.9 ± 18.9 659.6 727.5 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6866358929599999,
      "stddev": 0.022617507807309097,
      "median": 0.67882440156,
      "user": 0.38959242000000005,
      "system": 1.47196304,
      "min": 0.65768104906,
      "max": 0.73808318306,
      "times": [
        0.73808318306,
        0.70413134306,
        0.69404325406,
        0.6823931280600001,
        0.69507332306,
        0.67304573306,
        0.6735556300600001,
        0.65768104906,
        0.6730966110600001,
        0.6752556750600001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6789324803600001,
      "stddev": 0.018924720516200633,
      "median": 0.67245956756,
      "user": 0.3810710199999999,
      "system": 1.47232374,
      "min": 0.65960619406,
      "max": 0.72749298206,
      "times": [
        0.72749298206,
        0.67247185606,
        0.67815497106,
        0.66716788106,
        0.67244727906,
        0.67223493006,
        0.66832146106,
        0.65960619406,
        0.6888322130600001,
        0.68259503606
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.542 ± 0.037 2.489 2.601 1.00
pacquet@main 3.323 ± 0.075 3.239 3.502 1.31 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.54167332082,
      "stddev": 0.03692783265335108,
      "median": 2.5402220891200002,
      "user": 4.43551684,
      "system": 3.4118865,
      "min": 2.4893130811199997,
      "max": 2.60099777812,
      "times": [
        2.60099777812,
        2.53544211112,
        2.54500206712,
        2.5966539481199997,
        2.55468830912,
        2.5098279781199997,
        2.55609349512,
        2.51962142312,
        2.4893130811199997,
        2.5090930171199997
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.3233058410200003,
      "stddev": 0.07472949593106847,
      "median": 3.31704954912,
      "user": 5.198449039999999,
      "system": 3.4780583,
      "min": 3.2387801501199998,
      "max": 3.50248396312,
      "times": [
        3.31627152112,
        3.25879454312,
        3.26159121612,
        3.2387801501199998,
        3.31782757712,
        3.35927914912,
        3.2940062491199997,
        3.34938777112,
        3.33463627012,
        3.50248396312
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.765 ± 0.027 1.724 1.798 1.00
pacquet@main 2.545 ± 0.132 2.400 2.731 1.44 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.7650360763800002,
      "stddev": 0.026859128791892582,
      "median": 1.76569994678,
      "user": 2.1080864199999994,
      "system": 2.19720212,
      "min": 1.7240956897800002,
      "max": 1.7979722017800002,
      "times": [
        1.74456809278,
        1.7240956897800002,
        1.72961782278,
        1.75818211678,
        1.7582205077800002,
        1.77461872478,
        1.79785080578,
        1.77317938578,
        1.7979722017800002,
        1.79205541578
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.54494658798,
      "stddev": 0.13164197808309358,
      "median": 2.54518480128,
      "user": 2.8288161199999995,
      "system": 2.25516972,
      "min": 2.3999916687800003,
      "max": 2.73059210178,
      "times": [
        2.72264589078,
        2.55845184078,
        2.53191776178,
        2.6806241607800003,
        2.4074811467800004,
        2.3999916687800003,
        2.4173258097800003,
        2.57427033678,
        2.73059210178,
        2.42616516178
      ]
    }
  ]
}

@github-actions

github-actions Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/11915
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
2,541.67 ms
(-46.76%)Baseline: 4,773.69 ms
5,728.43 ms
(44.37%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,765.04 ms
(-52.80%)Baseline: 3,739.58 ms
4,487.50 ms
(39.33%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
2,072.50 ms
(-11.44%)Baseline: 2,340.20 ms
2,808.24 ms
(73.80%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
686.64 ms
(+4.46%)Baseline: 657.30 ms
788.76 ms
(87.05%)
🐰 View full continuous benchmarking report in Bencher

@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/resolve_dependency_tree.rs (1)

553-618: 💤 Low value

Consider importing TreeChildren and ChildEdge to reduce qualified-path verbosity.

Lines 554, 603, 609, 617 use fully qualified crate::resolved_tree::TreeChildren and crate::resolved_tree::ChildEdge while nearby types (DependenciesTreeNode, DirectDep, etc.) are imported at line 32. Adding these to the existing import would be consistent.

♻️ Suggested import consolidation
 use crate::{
     node_id::NodeId,
-    resolved_tree::{DependenciesTreeNode, DirectDep, PeerDep, ResolvedPackage, ResolvedTree},
+    resolved_tree::{ChildEdge, DependenciesTreeNode, DirectDep, PeerDep, ResolvedPackage, ResolvedTree, TreeChildren},
 };

Then simplify usages:

-        crate::resolved_tree::TreeChildren::Lazy { parent_ids: Arc::new(next_ancestors.clone()) }
+        TreeChildren::Lazy { parent_ids: Arc::new(next_ancestors.clone()) }
-        let mut by_id: Vec<crate::resolved_tree::ChildEdge> = Vec::new();
+        let mut by_id: Vec<ChildEdge> = Vec::new();
-            by_id.push(crate::resolved_tree::ChildEdge {
+            by_id.push(ChildEdge {
-        crate::resolved_tree::TreeChildren::Realized(realized)
+        TreeChildren::Realized(realized)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs` around
lines 553 - 618, The code uses fully qualified paths for
crate::resolved_tree::TreeChildren and crate::resolved_tree::ChildEdge several
times (e.g., the children assignment and by_id construction) while other
resolved_tree types are imported; add TreeChildren and ChildEdge to the existing
imports from resolved_tree and then replace occurrences of
crate::resolved_tree::TreeChildren and crate::resolved_tree::ChildEdge with the
shorter TreeChildren and ChildEdge identifiers (affecting the let children = ...
assignment and the by_id.push(...) construction, and the final
TreeChildren::Realized(...) return), leaving the surrounding logic
(children_specs_by_id, children_by_id, extract_children, resolve_node)
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs`:
- Around line 553-618: The code uses fully qualified paths for
crate::resolved_tree::TreeChildren and crate::resolved_tree::ChildEdge several
times (e.g., the children assignment and by_id construction) while other
resolved_tree types are imported; add TreeChildren and ChildEdge to the existing
imports from resolved_tree and then replace occurrences of
crate::resolved_tree::TreeChildren and crate::resolved_tree::ChildEdge with the
shorter TreeChildren and ChildEdge identifiers (affecting the let children = ...
assignment and the by_id.push(...) construction, and the final
TreeChildren::Realized(...) return), leaving the surrounding logic
(children_specs_by_id, children_by_id, extract_children, resolve_node)
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b0152077-1a02-4c12-a1ba-68aa459972b9

📥 Commits

Reviewing files that changed from the base of the PR and between def48e7 and f4ca999.

📒 Files selected for processing (3)
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolved_tree.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: When porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(), or streamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plain String or &str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only via TryFrom<String> and/or FromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallible From<String> (and From<&str> when convenient).
If upstream occasionally constructs without validation, expose from_str_unchecked as an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization.
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls. Fall back to manual impl only when conversion needs custom logic.
String-literal unions should become enums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies.
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;. Two forms stay allowed: external-crate preludes like use rayon::prelude::*; and root-of-module re-...

Files:

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

Applied to files:

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

Applied to files:

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

Applied to files:

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

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolved_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
🔇 Additional comments (10)
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs (2)

857-865: LGTM!


833-893: LGTM!

pacquet/crates/resolving-deps-resolver/src/resolved_tree.rs (5)

52-62: LGTM!


65-79: LGTM!


146-159: LGTM!


183-184: LGTM!


196-246: LGTM!

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs (3)

237-245: LGTM!

Also applies to: 262-262


306-306: LGTM!

Also applies to: 322-322


486-500: LGTM!

Also applies to: 527-527

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs (1)

865-868: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Append the current package id to the lazy ancestor chain.

child_parent_ids should extend parent_ids with pkg_id, not edge.pkg_id. As written, a lazy B -> C realization records [..., C], so when C is expanded later a back-edge to B is no longer recognized as a cycle and pacquet materializes an extra occurrence that upstream skips.

Suggested fix
             let child_parent_ids = {
                 let mut next_ids = (*parent_ids).clone();
-                next_ids.push(edge.pkg_id.clone());
+                next_ids.push(pkg_id.clone());
                 Arc::new(next_ids)
             };

Based on learnings: only match pnpm’s behavior exactly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs` around lines 865
- 868, The lazy ancestor chain is built from the wrong id: replace the use of
edge.pkg_id when extending parent_ids so child_parent_ids appends the current
pkg_id instead; locate the block creating child_parent_ids (the let
child_parent_ids = { ... } expression) and change the push of edge.pkg_id to
push(pkg_id.clone()) so cycles are detected correctly during lazy realization.
🤖 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.

Outside diff comments:
In `@pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs`:
- Around line 865-868: The lazy ancestor chain is built from the wrong id:
replace the use of edge.pkg_id when extending parent_ids so child_parent_ids
appends the current pkg_id instead; locate the block creating child_parent_ids
(the let child_parent_ids = { ... } expression) and change the push of
edge.pkg_id to push(pkg_id.clone()) so cycles are detected correctly during lazy
realization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a4c20d1d-9a11-4910-b35b-8b0465e0caff

📥 Commits

Reviewing files that changed from the base of the PR and between f4ca999 and 7a1cd4a.

📒 Files selected for processing (1)
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.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). (7)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • 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: When porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(), or streamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plain String or &str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only via TryFrom<String> and/or FromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallible From<String> (and From<&str> when convenient).
If upstream occasionally constructs without validation, expose from_str_unchecked as an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization.
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls. Fall back to manual impl only when conversion needs custom logic.
String-literal unions should become enums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies.
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;. Two forms stay allowed: external-crate preludes like use rayon::prelude::*; and root-of-module re-...

Files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
🧠 Learnings (5)
📚 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_peers.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_peers.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_peers.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_peers.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_peers.rs

Adds two regression tests for the lazy-children mechanism introduced
in #11915 that the existing coverage didn't hit:

1. `revisit_with_no_manifest_child_keeps_per_occurrence_node_id` —
   a child whose first walk produced `result.manifest == None`
   (the shape git / tarball / local resolvers return) must keep the
   non-leaf classification on every lazy realisation. Without the
   `ResolvedPackage::is_leaf` persistence the realizer would mis-
   classify it as a leaf and collapse distinct occurrences onto a
   shared `NodeId::Leaf`, breaking per-call-site state.

2. `pure_revisit_leaves_lazy_children_unrealized` — a pure pkg
   reached through multiple parents only realises its children for
   the occurrence the peer resolver walks first. Subsequent
   occurrences hit the `purePkgs` short-circuit before
   `realize_children` runs, so their `TreeChildren::Lazy` stays
   Lazy. Regression guard against accidentally moving the realise
   call above the short-circuit.

Both tests were validated by breaking the relevant subject (swapping
`is_leaf` back to the inferred check; moving `realize_children`
above the `purePkgs` gate) and confirming they fail cleanly.

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

1053-1177: 💤 Low value

Well-structured regression test for manifest-less package handling.

The test correctly validates that packages resolved without a manifest body (as returned by git/tarball resolvers) preserve their non-leaf classification across lazy revisits. The setup forces the peer resolver to descend into every occurrence via the peer dependency, ensuring the lazy realization path is exercised.

Minor: The spell checker flags "mis-classify" (line 1110) and "mis-classified" (line 1158). While the hyphenated form is acceptable, "misclassify" (no hyphen) is the more common modern spelling.

🤖 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 1053 -
1177, Update the spelling in the test function
revisit_with_no_manifest_child_keeps_per_occurrence_node_id: replace the
hyphenated words "mis-classify" and "mis-classified" with the modern
unhyphenated forms "misclassify" and "misclassified" in the comment text so the
test's documentation uses consistent spelling.
🤖 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 1053-1177: Update the spelling in the test function
revisit_with_no_manifest_child_keeps_per_occurrence_node_id: replace the
hyphenated words "mis-classify" and "mis-classified" with the modern
unhyphenated forms "misclassify" and "misclassified" in the comment text so the
test's documentation uses consistent spelling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a46d5f91-5802-419f-ae64-7cc74e35f8b8

📥 Commits

Reviewing files that changed from the base of the PR and between 7a1cd4a and 4331562.

📒 Files selected for processing (1)
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • 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: Lint and Test (ubuntu-latest)
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: When porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(), or streamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plain String or &str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only via TryFrom<String> and/or FromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallible From<String> (and From<&str> when convenient).
If upstream occasionally constructs without validation, expose from_str_unchecked as an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization.
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls. Fall back to manual impl only when conversion needs custom logic.
String-literal unions should become enums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies.
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;. Two forms stay allowed: external-crate preludes like use rayon::prelude::*; and root-of-module re-...

Files:

  • pacquet/crates/resolving-deps-resolver/src/tests.rs
🧠 Learnings (5)
📚 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
📚 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
📚 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
📚 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
📚 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
🪛 GitHub Check: Spell Check
pacquet/crates/resolving-deps-resolver/src/tests.rs

[warning] 1158-1158:
"mis" should be "miss" or "mist".


[warning] 1110-1110:
"mis" should be "miss" or "mist".

🔇 Additional comments (3)
pacquet/crates/resolving-deps-resolver/src/tests.rs (3)

131-133: LGTM!

Also applies to: 202-203


458-470: LGTM!

Also applies to: 508-521, 556-568, 607-619, 666-678, 761-781, 854-866, 959-971, 1026-1038, 1333-1350


1179-1286: LGTM!

CI's typos pass at .typos.toml flags `mis-` (suggests "miss" /
"mist"). Use "misclassify" instead of "mis-classify" — same
word, no hyphen, no typo hit.
@zkochan

zkochan commented May 24, 2026

Copy link
Copy Markdown
Member Author

The outside-diff comment on resolve_peers.rs:865-868 is incorrect — B is in C's stored chain in the current implementation, so a back-edge C -> B is detected.

The misreading is in CodeRabbit's claim that "a lazy B -> C realization records [..., C]". The actual chain stored on C's lazy entry is [..., B, C], because parent_ids at the point realize_children(B) runs already ends with B's own id:

  • Top-level Lazy (created in resolve_dependency_tree.rs:546): parent_ids = next_ancestors = ancestor_ids + [id]. The node's own id is appended.
  • Nested Lazy (created in resolve_peers.rs::realize_children): child's parent_ids = parent_ids_of_current + [edge.pkg_id]. parent_ids_of_current already ends with the current node's id (per the top-level construction), so the child's chain ends with [..., current_id, child_id].

By induction, every stored parent_ids ends with the node's own id. The cycle check parent_ids.contains(edge.pkg_id) then correctly catches edges back to self or to any ancestor.

Walking the claimed scenario explicitly. Say realize_children(N) is called and N's child is B, B's child is C, and C has a back-edge to B:

  1. parent_ids_of_N = [..., N_id] (ends in N_id).
  2. realize_children(N) adds B with child_parent_ids = parent_ids_of_N + [B_id] = [..., N_id, B_id].
  3. realize_children(B) later: parent_ids_of_B = [..., N_id, B_id] (ends in B_id). Adds C with child_parent_ids = parent_ids_of_B + [C_id] = [..., N_id, B_id, C_id].
  4. realize_children(C) later: parent_ids_of_C = [..., N_id, B_id, C_id] (ends in C_id). For back-edge C -> B: parent_ids.contains(B_id)true, cycle caught.

The suggested fix (push(pkg_id.clone()) instead of push(edge.pkg_id.clone())) would push N_id again (duplicating it) and drop B_id from the chain stored on B, producing [..., N_id, N_id]. That diverges from:

  • pacquet's own eager walker — let next_ancestors = ancestor_ids.iter().cloned().chain(std::iter::once(id.clone())).collect(); at resolve_dependency_tree.rs:527-528 — which appends the current node's id and passes that to children (so children see a chain ending with their parent's id, then append their own id when computing their own next_ancestors).
  • pnpm's buildTree recursive call — buildTree(child.id, [...parentIds, child.id], ...) at resolveDependencyTree.ts:389-395 — which also appends child.id, not the current parent's id.

Empirically I tried CodeRabbit's swap locally; the 56-test suite still passes because every cycle that the misaligned chain would miss is one that the eager walker already pruned out of children_by_id on the first walk (so realize_children never sees the back-edge). But it's still the wrong chain shape — diverges from pnpm and from pacquet's eager walker — so I'm keeping edge.pkg_id.

Skipping this finding. The cspell + mis- typo from the same review run is fixed in 8e127a5.


Written by an agent (Claude Code, claude-opus-4-7).

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