Skip to content

perf(pacquet): reuse lockfile resolutions on re-resolution#12113

Merged
zkochan merged 11 commits into
mainfrom
pacquet-lockfile-resolution-reuse
Jun 1, 2026
Merged

perf(pacquet): reuse lockfile resolutions on re-resolution#12113
zkochan merged 11 commits into
mainfrom
pacquet-lockfile-resolution-reuse

Conversation

@zkochan

@zkochan zkochan commented Jun 1, 2026

Copy link
Copy Markdown
Member

What

Port pnpm's lockfile-resolution reuse to pacquet: during a non-frozen install, reuse the prior pnpm-lock.yaml's resolution + transitive subtree for dependencies that are still satisfied and not being updated, instead of re-resolving everything from manifests (pacquet previously only fed the lockfile into preferred-versions seeding). Follow-up to #12096, which closed the same gap only for remote tarballs.

Design notes: pacquet/plans/LOCKFILE_RESOLUTION_REUSE.md.

Approach

Faithful port of pnpm's getInfoFromLockfile / resolvedDependencies / parentPkg.updated machinery (installing/deps-resolver/src/resolveDependencies.ts), adapted to pacquet's resolver as a hybrid resolve: snapshot-walk unchanged subtrees (reusing install_frozen_lockfile's logic — a package version's dependency set is immutable, so the snapshot child-refs are authoritative) and fresh-resolve new/changed/update-targeted deps, with an updated flag propagated down to break the reuse chain. The reuse gate uses semver-satisfies (pnpm parity), not string-equality.

Conservative by construction — anything not provably safe falls through to a fresh resolve:

  • Registry resolutions only (incl. registry Tarball with integrity); remote-tarball / git / directory / binary / non-semver / link:, and any subtree containing one, re-resolve.
  • subtree_fully_reusable requires the entire transitive subtree to be synthesizable before reusing.
  • pacquet update targets (and their subtrees, at any depth) re-resolve via UpdateReuseScope.
  • A changed packageExtensions config invalidates reuse (the prior lockfile is withheld when packageExtensionsChecksum no longer matches).
  • Dependency cycles conservatively re-resolve.

Tests

  • Unit: the semver-satisfies gate + ResolveResult synthesis (registry reuse, peer-metadata carry, non-registry / absent / non-semver rejection).
  • Integration (lockfile_resolution_reuse.rs): a dead-registry test that succeeds only via reuse (verified to fail without the change), and a reuse-vs-fresh structural-equivalence test asserting a reused tree matches a fresh resolve (parsed-lockfile compare, so it's robust to pacquet: pnpm-lock.yaml map ordering is non-canonical (HashMap iteration order) #12117).
  • Full pacquet-resolving-deps-resolver / pacquet-package-manager / pacquet-cli suites green.

Follow-ups (tracked, non-blocking)


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

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements lockfile-resolution reuse in pacquet: a feature that avoids re-resolving unchanged dependency trees during non-frozen installs by reusing the prior lockfile's recorded resolution and snapshot graph when the locked version satisfies the current manifest specifier and update targets are not involved.

Changes

Lockfile-Resolution Reuse

Layer / File(s) Summary
Lockfile reuse gate and synthesis
pacquet/crates/resolving-deps-resolver/src/lockfile_reuse.rs
reusable_importer_dep validates semver satisfaction; importer_dep locates lockfile entries; synthesize_reused_result reconstructs minimal ResolveResult from lockfile metadata with conservative acceptance rules; synthesize_manifest rebuilds JSON peer/platform metadata fragments.
Lockfile reuse unit tests
pacquet/crates/resolving-deps-resolver/src/lockfile_reuse/tests.rs
Tests for semver ranges, absence checks, registry/tarball/missing scenarios, peer metadata synthesis, and version parsing edge cases.
Resolver state for lockfile reuse
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs, pacquet/crates/resolving-deps-resolver/src/lib.rs
Adds UpdateReuseScope enum and ReuseSource tracking; extends WorkspaceTreeCtx with wanted_lockfile, update_reuse_scope, and subtree_reusable memoization.
Tree extension with reuse awareness
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
extend_tree computes ReuseSource per dependency edge based on wanted_lockfile presence, passing reuse mode into resolve_node.
Node-level reuse attempt and fallback
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
resolve_node accepts reuse parameter, attempts try_reuse_node before fresh resolution, routes reusable nodes through resolve_reused_node, and passes ReuseSource::Off to fresh-resolution children.
Subtree reusability checks & reused-node builder
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
try_reuse_node, subtree_fully_reusable, subtree_children_reusable, and resolve_reused_node validate and synthesize transitive snapshot subtrees while respecting update_reuse_scope and optionality.
Importer ID threading through resolution
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
resolve_importer passes ROOT_IMPORTER_KEY to resolve_importer_with_workspace; signature updated to accept importer_id; all extend_tree calls in peer loops pass importer ID.
Workspace options and context plumbing
pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
WorkspaceResolveOptions gains wanted_lockfile and update_reuse_scope fields; resolve_workspace destructures and applies them to WorkspaceTreeCtx; resolve_importer_with_workspace call passes importer.id.
Installation layer reuse policy wiring
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
Conditionally passes wanted_lockfile when package_extensions_checksum matches; wires update_reuse_scope from UpdateSeedPolicy variants.
Visibility and test fixture updates
pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs, pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
Makes satisfies_including_prerelease pub(crate); updates workspace_opts test helper to initialize new fields.
Integration test for lockfile reuse
pacquet/crates/cli/tests/lockfile_resolution_reuse.rs
Tests that reuse succeeds when registry is unavailable (via dead URL) and that reused tree is structurally identical to fresh resolution; includes dead_registry_url helper.
Lockfile-resolution reuse design document
pacquet/plans/LOCKFILE_RESOLUTION_REUSE.md
Plans lockfile-resolution reuse with reuse gates, subtree traversal, invalidation propagation, and risks (byte-ordering, overrides drift, cycles, benchmarking).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pnpm#11867: Modifies InstallWithFreshLockfile::run—code-adjacent refactor around fresh-lockfile install flow.
  • pnpm/pnpm#12046: Changes InstallWithFreshLockfile::run with lockfile-only early-return logic that may interact with reuse gating.
  • pnpm/pnpm#11816: Fresh-install/lockfile pipeline changes that the reuse logic builds upon.

Poem

🐰 I dug a hole where old lockfiles sleep,

Reused their roots when the registries weep,
A widened range, a dead-host test,
The same lockfile sings—no fresh request,
Hoppity installs, light as a leap.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main change: implementing lockfile resolution reuse during package re-resolution to improve performance.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pacquet-lockfile-resolution-reuse

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 1, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      7.5±0.22ms   574.6 KB/sec    1.01      7.6±0.24ms   567.7 KB/sec

@codecov-commenter

codecov-commenter commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.92248% with 39 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.38%. Comparing base (6f382f4) to head (0e87443).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
...ates/resolving-deps-resolver/src/lockfile_reuse.rs 79.61% 21 Missing ⚠️
...lving-deps-resolver/src/resolve_dependency_tree.rs 93.11% 17 Missing ⚠️
...es/resolving-deps-resolver/src/resolve_importer.rs 92.30% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12113      +/-   ##
==========================================
+ Coverage   87.03%   87.38%   +0.34%     
==========================================
  Files         252      256       +4     
  Lines       28205    29070     +865     
==========================================
+ Hits        24548    25402     +854     
- Misses       3657     3668      +11     

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

@github-actions

github-actions Bot commented Jun 1, 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.104 ± 0.096 2.017 2.318 1.01 ± 0.05
pacquet@main 2.086 ± 0.061 2.011 2.188 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.10408862798,
      "stddev": 0.09597730381986544,
      "median": 2.0974232854799997,
      "user": 2.62746916,
      "system": 3.429632699999999,
      "min": 2.01652245998,
      "max": 2.31769452498,
      "times": [
        2.21136569898,
        2.1078725879799998,
        2.03685020398,
        2.02818033498,
        2.09797070998,
        2.1110220219799998,
        2.31769452498,
        2.01652245998,
        2.09687586098,
        2.0165318759799997
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.08644860228,
      "stddev": 0.06106755388635291,
      "median": 2.08779209748,
      "user": 2.6866411599999998,
      "system": 3.3964288,
      "min": 2.0105681879799997,
      "max": 2.18789522998,
      "times": [
        2.18789522998,
        2.10090311898,
        2.15193930498,
        2.0346494859799997,
        2.0105681879799997,
        2.10583060998,
        2.0386637899799998,
        2.01867093698,
        2.14068428198,
        2.07468107598
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 679.5 ± 37.3 641.2 769.0 1.03 ± 0.06
pacquet@main 661.9 ± 16.5 643.4 699.2 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6794575914800001,
      "stddev": 0.03734588623386306,
      "median": 0.66499914878,
      "user": 0.38689031999999995,
      "system": 1.3396657600000001,
      "min": 0.64117306978,
      "max": 0.76898152678,
      "times": [
        0.76898152678,
        0.71380873878,
        0.69249491978,
        0.66642026278,
        0.66357803478,
        0.65544894478,
        0.66837979578,
        0.66301968578,
        0.66127093578,
        0.64117306978
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.66188030938,
      "stddev": 0.016451937881962867,
      "median": 0.6636792202799999,
      "user": 0.36421761999999996,
      "system": 1.34286766,
      "min": 0.64344324978,
      "max": 0.69924991578,
      "times": [
        0.69924991578,
        0.66456294178,
        0.66388468778,
        0.6472398257799999,
        0.64344324978,
        0.65323830478,
        0.66347375278,
        0.66945715678,
        0.66925508778,
        0.64499817078
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.397 ± 0.022 2.362 2.426 1.00
pacquet@main 2.402 ± 0.047 2.354 2.487 1.00 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.3971526469800004,
      "stddev": 0.022314134397374404,
      "median": 2.3950974668800002,
      "user": 3.8775483399999997,
      "system": 3.23987888,
      "min": 2.3621028288800003,
      "max": 2.42571541088,
      "times": [
        2.40610651788,
        2.3621028288800003,
        2.39090791488,
        2.42391013888,
        2.42571541088,
        2.42146132488,
        2.39412635888,
        2.39606857488,
        2.36971470288,
        2.38141269688
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.40204718908,
      "stddev": 0.04730807498999655,
      "median": 2.3778718218800003,
      "user": 3.8893482399999995,
      "system": 3.2180751800000005,
      "min": 2.3542191258800003,
      "max": 2.48740760188,
      "times": [
        2.45561053688,
        2.45968054788,
        2.3542191258800003,
        2.39905878288,
        2.3802801598800003,
        2.36790413288,
        2.48740760188,
        2.36705721488,
        2.3737903038800003,
        2.3754634838800004
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.568 ± 0.025 1.529 1.604 1.00 ± 0.02
pacquet@main 1.562 ± 0.025 1.533 1.602 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.56820833156,
      "stddev": 0.025032985063063933,
      "median": 1.56989598456,
      "user": 1.8113040999999999,
      "system": 1.8682688200000002,
      "min": 1.52862562506,
      "max": 1.60430447106,
      "times": [
        1.5677750480600001,
        1.55616967206,
        1.52862562506,
        1.58221955506,
        1.5395936480599999,
        1.5505785840600002,
        1.60264030406,
        1.5781594870600002,
        1.57201692106,
        1.60430447106
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.56164503406,
      "stddev": 0.02495774852359846,
      "median": 1.5521237970600001,
      "user": 1.7679203000000001,
      "system": 1.8762905200000002,
      "min": 1.53281589806,
      "max": 1.6021413770600001,
      "times": [
        1.5516739620600002,
        1.6021413770600001,
        1.53281589806,
        1.58936091406,
        1.54105224006,
        1.58977664906,
        1.5353721930600002,
        1.54905098406,
        1.55257363206,
        1.57263249106
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12113
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,397.15 ms
(+1.79%)Baseline: 2,354.97 ms
2,825.97 ms
(84.83%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,568.21 ms
(+3.14%)Baseline: 1,520.53 ms
1,824.64 ms
(85.95%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
2,104.09 ms
(+0.87%)Baseline: 2,085.95 ms
2,503.14 ms
(84.06%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
679.46 ms
(+1.72%)Baseline: 667.98 ms
801.57 ms
(84.77%)
🐰 View full continuous benchmarking report in Bencher

zkochan added 5 commits June 1, 2026 18:37
Port pnpm's resolvedDependencies / parentPkg.updated reuse mechanism so a
non-frozen install reuses the prior lockfile's resolution and transitive
subtree for dependencies that are still satisfied and not being updated,
instead of re-resolving everything from the registry.

resolve_node now decides reuse before the resolver chain: direct deps
match the importer's recorded resolution via semver-satisfies; transitive
deps take their resolved key directly from the parent's snapshot child
refs. A reused node synthesizes its ResolveResult from the lockfile
(cloned resolution + integrity, manifest reconstructed from the recorded
peer/platform metadata) and walks its children from the snapshot graph;
fresh-resolved nodes force their whole subtree to re-resolve.

Reuse is conservative — registry (plain-semver) packages only, and only
when the entire transitive subtree is reusable; any link:/file:/git/
tarball/non-semver shape anywhere makes the subtree fall through to a
fresh resolve. pacquet update suppresses reuse for its targets (and their
subtrees) via UpdateReuseScope so compatible bumps still re-resolve.

See pacquet/plans/LOCKFILE_RESOLUTION_REUSE.md (Stage 3).
@zkochan zkochan marked this pull request as ready for review June 1, 2026 21:35
Copilot AI review requested due to automatic review settings June 1, 2026 21:35
@zkochan zkochan changed the title perf(pacquet): reuse lockfile resolutions on re-resolution (WIP) perf(pacquet): reuse lockfile resolutions on re-resolution Jun 1, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Port pnpm's lockfile-resolution reuse to pacquet for performance

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Port pnpm's lockfile-resolution reuse to pacquet for non-frozen installs
  - Reuse prior lockfile's resolution + transitive subtree for unchanged dependencies
  - Avoid re-resolving from registry when locked version still satisfies manifest range
• Implement hybrid resolve: snapshot-walk unchanged subtrees, fresh-resolve new/changed deps
  - Thread prior lockfile through resolver tree context
  - Semver-satisfies gate for direct deps; snapshot child-refs for transitive deps
• Add update-suppression scope so pacquet update targets re-resolve to highest-in-range
  - Propagate updated flag down subtree to force re-resolution of bumped packages
• Comprehensive test coverage: discriminating no-re-resolve test + structural equivalence test
  - Verify reused lockfile equals fresh resolve (content-identical, modulo byte-ordering)
Diagram
flowchart LR
  A["Prior pnpm-lock.yaml"] -->|"thread into resolver"| B["WorkspaceTreeCtx"]
  B -->|"semver-satisfies gate"| C["reusable_importer_dep"]
  C -->|"direct deps match"| D["try_reuse_node"]
  D -->|"subtree fully reusable?"| E["subtree_fully_reusable"]
  E -->|"yes: snapshot walk"| F["resolve_reused_node"]
  E -->|"no: fresh resolve"| G["resolve_node"]
  F -->|"synthesize from lockfile"| H["ResolveResult"]
  G -->|"registry resolve"| H
  H -->|"merge into graph"| I["DependenciesGraph"]
  J["UpdateReuseScope"] -->|"suppress reuse for targets"| D

Loading

Grey Divider

File Changes

1. pacquet/crates/cli/tests/lockfile_resolution_reuse.rs 🧪 Tests +169/-0

Integration tests for lockfile-resolution reuse

pacquet/crates/cli/tests/lockfile_resolution_reuse.rs


2. pacquet/crates/resolving-deps-resolver/src/lockfile_reuse.rs ✨ Enhancement +191/-0

Reuse gate and result synthesis from lockfile

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


3. pacquet/crates/resolving-deps-resolver/src/lockfile_reuse/tests.rs 🧪 Tests +158/-0

Unit tests for reuse gate and manifest synthesis

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


View more (8)
4. pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs ✨ Enhancement +452/-21

Hybrid resolve with snapshot-walk and reuse propagation

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


5. pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs ✨ Enhancement +13/-4

Thread importer ID for reuse gate matching

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


6. pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs ✨ Enhancement +23/-2

Wire lockfile and update scope through workspace resolver

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


7. pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs 🧪 Tests +2/-0

Update workspace resolver tests with reuse options

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


8. pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs ✨ Enhancement +1/-1

Export prerelease-satisfies helper for reuse gate

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


9. pacquet/crates/resolving-deps-resolver/src/lib.rs ✨ Enhancement +2/-1

Export lockfile-reuse module and UpdateReuseScope type

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


10. pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs ✨ Enhancement +28/-0

Pass prior lockfile and update scope to resolver

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


11. pacquet/plans/LOCKFILE_RESOLUTION_REUSE.md 📝 Documentation +111/-0

Design document for lockfile-resolution reuse feature

pacquet/plans/LOCKFILE_RESOLUTION_REUSE.md


Grey Divider

Qodo Logo

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR ports pnpm’s “lockfile-resolution reuse” behavior into pacquet so non-frozen installs can reuse already-resolved subtrees from the prior pnpm-lock.yaml (when still satisfied and not update-targeted), reducing re-resolution work and registry traffic.

Changes:

  • Thread the prior lockfile (wanted_lockfile) and an update reuse suppression scope through the workspace resolver context.
  • Add a lockfile reuse gate + subtree-walk path that synthesizes ResolveResult from lockfile metadata and walks children from the snapshot graph.
  • Add unit and integration tests covering semver-satisfies reuse gating and reuse-vs-fresh structural equivalence.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pacquet/plans/LOCKFILE_RESOLUTION_REUSE.md Design/staging plan and known follow-ups for lockfile subtree reuse.
pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs Updates test workspace options to include new reuse fields.
pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs Threads wanted_lockfile + update_reuse_scope into WorkspaceTreeCtx; passes importer id into importer resolution.
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs Plumbs importer id down to direct-dependency resolution so importer-level reuse can consult importer snapshots.
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs Implements reuse source threading, subtree reusability memoization, and reused-node resolution/walk.
pacquet/crates/resolving-deps-resolver/src/lockfile_reuse.rs Adds semver-satisfies gate and ResolveResult synthesis from lockfile metadata.
pacquet/crates/resolving-deps-resolver/src/lockfile_reuse/tests.rs Unit tests for importer-gate semantics and synthesized result behavior.
pacquet/crates/resolving-deps-resolver/src/lib.rs Wires in new module + exports UpdateReuseScope.
pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs Exposes satisfies_including_prerelease for reuse gate semver checks.
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs Passes prior lockfile + update reuse scope into workspace resolve options (with packageExtensions drift guard).
pacquet/crates/cli/tests/lockfile_resolution_reuse.rs New integration test proving reuse can succeed with registry unreachable; adds reuse-vs-fresh structural equivalence test.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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

🧹 Nitpick comments (2)
pacquet/crates/cli/tests/lockfile_resolution_reuse.rs (2)

55-58: 💤 Low value

Appending to pnpm-workspace.yaml without validation.

The test appends minimumReleaseAge: 0\n to the workspace config by concatenating strings. If the existing file doesn't end with a newline, this could produce invalid YAML (though in practice the test fixture likely has a trailing newline).

🛡️ Optional: ensure newline before append
     let existing = fs::read_to_string(&workspace_yaml).expect("read pnpm-workspace.yaml");
-    fs::write(&workspace_yaml, format!("{existing}minimumReleaseAge: 0\n"))
+    let separator = if existing.ends_with('\n') { "" } else { "\n" };
+    fs::write(&workspace_yaml, format!("{existing}{separator}minimumReleaseAge: 0\n"))
         .expect("append minimumReleaseAge to pnpm-workspace.yaml");
🤖 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/cli/tests/lockfile_resolution_reuse.rs` around lines 55 - 58,
The test currently concatenates to the existing workspace file (variables
workspace_yaml and existing) which can produce invalid YAML if the file doesn't
end with a newline; update the write logic used with fs::write so you first
ensure existing ends with a newline (e.g., check existing.ends_with('\n') and if
not, add one) before appending "minimumReleaseAge: 0\n" and then write the
combined string back, or alternatively parse/modify the YAML and reserialize it
instead of raw string concatenation.

31-42: 💤 Low value

Potential race condition in dead port selection.

The dead_registry_url helper binds an ephemeral port, reads it, then drops the listener. Between the drop and the registry connection attempt, another process could bind the same port. While unlikely in a test environment, this could cause flakiness.

Consider documenting this race or accepting it as an acceptable test-environment tradeoff.

📝 Optional: document the inherent race
 fn dead_registry_url() -> String {
     // Bind to an ephemeral port, read it, then drop the listener so the
     // port is (almost certainly) free again — anything that connects to
-    // it gets refused.
+    // it gets refused. Note: a race exists where another process could
+    // bind this port before the test connects, but in practice this is
+    // extremely unlikely in an isolated test environment.
     let listener =
🤖 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/cli/tests/lockfile_resolution_reuse.rs` around lines 31 - 42,
The helper dead_registry_url() has a potential race where the ephemeral port
dropped by TcpListener could be re-bound by another process before the test
connects; update the function's surrounding comment (or add a doc comment above
dead_registry_url) to explicitly acknowledge this race condition and justify it
as an acceptable test-environment tradeoff (or note acceptable flakiness), or
alternatively implement a retry/verify strategy before returning the URL;
reference dead_registry_url when making the comment or change so reviewers can
find the fix quickly.
🤖 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/plans/LOCKFILE_RESOLUTION_REUSE.md`:
- Around line 38-40: Update the stale caveat sentence about
readPackageHook/packageExtensions invalidation: remove or replace the “Out of
scope for the first cut” phrasing and mark that
packageExtensions/readPackageHook invalidation is implemented in this PR series
(or reference the corresponding change), so the LOCKFILE_RESOLUTION_REUSE.md
note no longer suggests it’s pending; ensure the text mentions readPackageHook
and packageExtensions explicitly and that reuse is invalidated when they change.

---

Nitpick comments:
In `@pacquet/crates/cli/tests/lockfile_resolution_reuse.rs`:
- Around line 55-58: The test currently concatenates to the existing workspace
file (variables workspace_yaml and existing) which can produce invalid YAML if
the file doesn't end with a newline; update the write logic used with fs::write
so you first ensure existing ends with a newline (e.g., check
existing.ends_with('\n') and if not, add one) before appending
"minimumReleaseAge: 0\n" and then write the combined string back, or
alternatively parse/modify the YAML and reserialize it instead of raw string
concatenation.
- Around line 31-42: The helper dead_registry_url() has a potential race where
the ephemeral port dropped by TcpListener could be re-bound by another process
before the test connects; update the function's surrounding comment (or add a
doc comment above dead_registry_url) to explicitly acknowledge this race
condition and justify it as an acceptable test-environment tradeoff (or note
acceptable flakiness), or alternatively implement a retry/verify strategy before
returning the URL; reference dead_registry_url when making the comment or change
so reviewers can find the fix quickly.
🪄 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: 7a4c6d04-6105-4656-855c-44b5b79007cf

📥 Commits

Reviewing files that changed from the base of the PR and between 71eea4d and c262d5e.

📒 Files selected for processing (11)
  • pacquet/crates/cli/tests/lockfile_resolution_reuse.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/lib.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/plans/LOCKFILE_RESOLUTION_REUSE.md
📜 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: Agent
  • GitHub Check: Code Coverage
  • GitHub Check: Dylint
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (2)
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/hoist_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/lib.rs
  • pacquet/crates/cli/tests/lockfile_resolution_reuse.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
pacquet/**/tests/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/tests/**/*.rs: Prefer real fixtures over dependency-injection seams — use tempfile::TempDir, the mocked registry, or integration tests spawning the actual binary for happy paths and most error paths; use the DI seam only for filesystem error kinds, deterministic time, shared process-global state, or external-service happy paths
Follow test-logging guidance — log before non-assert_eq! assertions, dbg! complex structures, skip logging for simple scalar assert_eq!
Tests must not be tolerant of missing build/runtime environment by silently returning early when a tool isn't found — let the test panic when a required tool is absent
Prefer #[cfg_attr(target_os = "windows", ignore = "...")] (or matching #[cfg(unix)] gates) over runtime probe-and-skip helpers for platform-locked tools
Port relevant pnpm tests to Rust tests whenever they translate when porting behavior from pnpm
Use snapshot tests with insta and carefully review diffs when intentional changes alter snapshots; accept with cargo insta review only after careful review
Tests that need the mocked registry should start pnpr through pacquet-testing-utils; cargo test / cargo nextest run should not require a separate just registry-mock launch step

Files:

  • pacquet/crates/cli/tests/lockfile_resolution_reuse.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/hoist_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/lib.rs
  • pacquet/crates/cli/tests/lockfile_resolution_reuse.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse/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/hoist_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/lib.rs
  • pacquet/crates/cli/tests/lockfile_resolution_reuse.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse/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/hoist_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/lib.rs
  • pacquet/crates/cli/tests/lockfile_resolution_reuse.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse/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/hoist_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/lib.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse/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/hoist_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/lib.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
🪛 LanguageTool
pacquet/plans/LOCKFILE_RESOLUTION_REUSE.md

[grammar] ~28-~28: Ensure spelling is correct
Context: ...skips fetch. ## Key simplification for pacquet A given package **version's dependency s...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[style] ~32-~32: Consider an alternative for the overused word “exactly”.
Context: ...rent version, its transitive subtree is exactly the snapshot's recorded child-refs — we...

(EXACTLY_PRECISELY)

🔇 Additional comments (25)
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs (1)

822-849: LGTM!

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

214-214: LGTM!

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

134-135: LGTM!

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

1-192: LGTM!

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

1-159: LGTM!

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

70-70: LGTM!

Also applies to: 90-90

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

39-54: LGTM!


56-77: LGTM!


251-252: LGTM!


418-434: LGTM!

Also applies to: 449-451, 482-503


714-759: LGTM!


783-810: LGTM!

Also applies to: 1032-1043


1103-1141: LGTM!


1143-1152: LGTM!


1154-1182: LGTM!


1211-1378: LGTM!


1400-1407: LGTM!


1184-1209: Resolve dep_ref.resolve() API on lockfile dependency refs

dep_ref.resolve(child_name) is backed by pacquet-crates/lockfile’s SnapshotDepRef-style API: pacquet/crates/lockfile/src/snapshot_dep_ref.rs defines pub fn resolve(&self, alias_name: &PkgName) -> Option<PkgNameVerPeer>, matching the call’s expected input/output types.


1380-1398: dep_ref.resolve() call is valid (method exists with the right signature). SnapshotDepRef defines pub fn resolve(&self, alias_name: &PkgName) -> Option<PkgNameVerPeer>, and in snapshot_child_refs the map iteration yields alias: &PkgName and dep_ref: &SnapshotDepRef, so dep_ref.resolve(alias) matches.

pacquet/crates/cli/tests/lockfile_resolution_reuse.rs (2)

44-110: LGTM!


112-169: LGTM!

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

236-236: LGTM!

Also applies to: 281-281, 347-347, 370-370


217-226: ⚡ Quick win

ROOT_IMPORTER_KEY is compatible with importer_id: &str

pacquet_lockfile::Lockfile::ROOT_IMPORTER_KEY is used throughout as a borrowed string (e.g., calling .to_string() and passing it directly to get(...)/comparisons), so passing it into the &str-typed importer_id parameter is type-correct across the crate boundary.

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

21-23: LGTM!

Also applies to: 82-91


133-141: LGTM!

Also applies to: 175-175

Comment thread pacquet/plans/LOCKFILE_RESOLUTION_REUSE.md Outdated
@zkochan zkochan merged commit 73a294e into main Jun 1, 2026
25 checks passed
@zkochan zkochan deleted the pacquet-lockfile-resolution-reuse branch June 1, 2026 22:37
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.

3 participants