Skip to content

fix(pacquet,package-manager): walk every workspace project in fresh-resolve install#11905

Merged
zkochan merged 7 commits into
mainfrom
perf/11901
May 25, 2026
Merged

fix(pacquet,package-manager): walk every workspace project in fresh-resolve install#11905
zkochan merged 7 commits into
mainfrom
perf/11901

Conversation

@zkochan

@zkochan zkochan commented May 24, 2026

Copy link
Copy Markdown
Member

Summary

Closes #11901.

Pacquet's fresh-resolve install path (no --frozen-lockfile, no usable
lockfile) previously only walked the workspace root manifest, so sibling
workspace projects' own dependencies never landed in the lockfile or on
disk. Per the issue, this was the dominant factor in the babylon
benchmark's 4–7× slowdown across every variation.

This PR makes the fresh-resolve dispatch:

  • Load every workspace project's manifest once via find_workspace_projects
    (re-used by the existing workspace:-spec lookup so each manifest is
    read off disk exactly once).
  • Call resolve_importer per importer with its own project_dir, sharing
    the install-scoped meta_cache / fetch_locker / picked_manifest_cache
    so packument and JSON-parse work is amortized across importers.
  • Merge per-importer graphs into one DependenciesGraph for
    dependencies_graph_to_lockfile.
  • Emit one importers[<id>] entry per project in pnpm-lock.yaml.
  • Loop link_bins per importer so each project gets its own
    node_modules/.bin.
  • Loop GVS register_project over every importer key the freshly-built
    lockfile carries, mirroring the frozen-lockfile branch.

Mirrors upstream's
resolveRootDependencies
iteration shape: one shared resolution context, per-importer direct-deps
slices.

link: lockfile rendering

importer_dep_version and snapshot_dep_ref learned a link:
short-circuit so workspace-sibling edges render as
ImporterDepVersion::Link(<rel-path>) /
SnapshotDepRef::Link(<rel-path>) instead of failing to parse as
name@version. The existing variants on those enums already supported
the shape — only the conversion needed wiring.

Test plan

  • New integration test pacquet/crates/cli/tests/workspace_install.rs
    spins up a two-project workspace (packages/a, packages/b), runs a
    fresh pacquet install, and asserts each importer's node_modules
    symlinks exist and the lockfile records both importers[packages/a]
    and importers[packages/b].
  • New unit tests in
    pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs:
    workspace-link direct dep → ImporterDepVersion::Link; workspace-link
    child edge → SnapshotDepRef::Link; multi-importer workspace → one
    importers[<id>] entry per project, shared transitive dep deduped to
    one snapshot.
  • cargo nextest run -p pacquet-package-manager — 302/302 pass.
  • cargo nextest run -p pacquet-cli --test add — 5/5 pass
    (Install.manifest exception for pacquet add's in-memory manifest
    mutation: the root importer reuses the in-memory manifest, sibling
    importers come from the freshly-read workspace walk).
  • cargo nextest run -p pacquet-resolving-deps-resolver,
    pacquet-workspace — clean.
  • cargo clippy --locked --workspace --all-targets -- --deny warnings
    — clean.
  • cargo fmt / taplo format / typos pacquet — clean.

Out of scope (follow-ups)

  • Cross-importer TreeCtx sharing. Each resolve_importer call
    still allocates its own TreeCtx, so transitive packages get resolved
    per importer. Network-side caches amortize the HTTP and packument JSON
    parsing — only per-resolve semver matching duplicates. Full upstream
    parity (one shared resolution context with per-importer hoist loops in
    the resolver crate) is a separate refactor that touches TreeCtx,
    ResolvedTree, resolve_peers, and ~30 test files.
  • Per-importer pnpm:package-manifest initial reporter emit. Still
    root-only.
  • WantedKey widening to include project_dir. Sidestepped by the
    per-importer TreeCtx; the cross-importer-sharing follow-up will
    need it to keep workspace:* cache entries unambiguous.

Notes for reviewers

  • The Install::run dispatch carves out the root importer to reuse
    Install.manifest (rather than the freshly-read root manifest from
    find_workspace_projects). pacquet add mutates Install.manifest
    in memory before calling install; re-reading from disk would walk
    the pre-add shape and miss the freshly-added dep. The add tests
    caught this in the first pass.
  • Per the pacquet AGENTS.md, no changeset is included — this is a
    pacquet-only change and pacquet doesn't ship via the same release
    pipeline.

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

Summary by CodeRabbit

  • Tests

    • Added integration and unit tests covering fresh-resolve multi-project installs, per-importer behavior, workspace sibling linking, lockfile importer sections, and registry/store canonicalization.
  • Refactor

    • Install and lockfile flows now operate across multiple workspace projects: per-importer manifests, merged dependency graph, per-project .bin linking, and single root registration.
  • Bug Fixes

    • Improved path normalization to handle lexical ../ and . cases for registry/store checks.
  • Chores

    • Exported importer ID helper and added a workspace utility dependency.

Review Change Stack

@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

Restructures fresh-lockfile install to resolve every workspace importer: load workspace projects, build importer_manifests, resolve each importer into a merged dependency graph, produce per-importer lockfile entries, run per-importer bin linking, and adjust GVS registration and tests.

Changes

Multi-importer fresh-resolve workspace path

Layer / File(s) Summary
Importer ID utility for path-based project identification
pacquet/crates/workspace/Cargo.toml, pacquet/crates/workspace/src/importer_id.rs, pacquet/crates/workspace/src/lib.rs
importer_id_from_root_dir computes POSIX-relative importer ids ("." for root) with a fallback; adds pathdiff workspace dependency and unit tests.
Lockfile adapter refactored for multi-importer graphs
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
dependencies_graph_to_lockfile now accepts per-importer manifests + a shared DependenciesGraph; adds ImporterLockfileInput, updates build_importer, importer_dep_version, compute_corrected_optional, and snapshot_dep_ref for multi-importer and link: handling.
Lockfile adapter test updates and workspace/link coverage
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
Tests refactored to use single_importer_opts; adds make_link_node and workspace/link tests, multi-importer dedupe and optional recomputation validations.
Install flow workspace loading and importer manifests construction
pacquet/crates/package-manager/src/install.rs
Adds load_workspace_projects, builds resolver workspace_packages from loaded projects, constructs importer_manifests (root plus siblings), updates build_workspace_packages_map, and adds early best-effort GVS root registration.
Fresh-lockfile multi-importer resolution and bin linking
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
InstallWithFreshLockfile now takes importer_manifests; preferred-versions seeding aggregates from all importers, resolve loops per-importer to build a merged graph and direct_by_importer, prefetch keys are collected from merged graph, build_fresh_lockfile builds per-importer importer entries, and link_bins runs per importer.
Fresh-resolve workspace installation integration test
pacquet/crates/cli/tests/workspace_install.rs
Adds E2E test fresh_resolve_walks_every_workspace_importer creating a two-project workspace, running fresh pacquet install, and asserting per-importer symlinks, shared virtual-store entries, and importer sections in pnpm-lock.yaml.
Install tests: GVS workspace-registration expectations
pacquet/crates/package-manager/src/install/tests.rs
Updates GVS test to assert a single <store_dir>/v11/projects entry exists (workspace root canonicalized), and tweaks imports/comments accordingly.
Store-dir lexical normalization fallback
pacquet/crates/store-dir/src/project_registry.rs
path_contains falls back to lexical_normalize (collapsing ./..) when canonicalization fails; adds helper and unit test for non-existent paths.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant InstallWithFreshLockfile
  participant resolve_importer
  participant DependenciesGraph
  participant dependencies_graph_to_lockfile
  participant GlobalVirtualStore
  Client->>InstallWithFreshLockfile: run(importer_manifests)
  InstallWithFreshLockfile->>resolve_importer: resolve_importer(importer_manifest_i)
  resolve_importer->>DependenciesGraph: emit nodes (per-importer)
  InstallWithFreshLockfile->>DependenciesGraph: merge per-importer graphs
  InstallWithFreshLockfile->>dependencies_graph_to_lockfile: build_lockfile(importer_manifests, merged_graph, direct_by_importer)
  InstallWithFreshLockfile->>GlobalVirtualStore: register workspace root (best-effort)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pnpm#11816: Related fresh-install lockfile work and resolver→lockfile wiring for multi-importer scenarios.
  • pnpm/pnpm#11877: Related changes to importer dependency/specifier generation in lockfile construction.
  • pnpm/pnpm#11919: Related BFS recomputation of transitive optional flags used by lockfile pruner logic.

Poem

"I hopped through each package, root to nested tree,
Linked bins and built locks so every importer's free.
Siblings now share stores and each gets its own bin,
I twitched my nose, wrote tests, then boxed it up with a grin.
🥕 — a rabbit who loves tidy monorepos"

🚥 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 accurately summarizes the main change: updating the fresh-resolve install path to walk every workspace project instead of only the root.
Linked Issues check ✅ Passed All coding requirements from issue #11901 are met: loads all workspace manifests, resolves each importer, merges graphs, emits per-importer lockfile entries, links bins per importer, and handles workspace-sibling edges as link: depPaths.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the PR objectives. Helper modules (importer_id, path_contains lexical normalization) and test coverage serve the multi-importer resolution objective without introducing unrelated functionality.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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/11901

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.00      8.4±0.20ms   517.2 KB/sec    1.02      8.6±0.70ms   506.0 KB/sec

@codecov-commenter

codecov-commenter commented May 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.93088% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.66%. Comparing base (d8a79a9) to head (fb1782e).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/package-manager/src/install.rs 90.24% 4 Missing ⚠️
...package-manager/src/install_with_fresh_lockfile.rs 95.60% 4 Missing ⚠️
pacquet/crates/workspace/src/importer_id.rs 90.90% 2 Missing ⚠️
pacquet/crates/store-dir/src/project_registry.rs 96.96% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #11905      +/-   ##
==========================================
+ Coverage   87.61%   87.66%   +0.05%     
==========================================
  Files         219      220       +1     
  Lines       26666    26780     +114     
==========================================
+ Hits        23364    23478     +114     
  Misses       3302     3302              

☔ 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 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.081 ± 0.088 1.938 2.270 1.03 ± 0.05
pacquet@main 2.016 ± 0.050 1.968 2.099 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.0813698216,
      "stddev": 0.0883391025648184,
      "median": 2.0806421649,
      "user": 2.73459796,
      "system": 3.3552278600000003,
      "min": 1.9377619844,
      "max": 2.2698043354000004,
      "times": [
        2.0968951214000002,
        2.1402016654000002,
        1.9377619844,
        2.0441229264,
        2.0305569724000003,
        2.0743186774,
        2.1208987974,
        2.2698043354000004,
        2.0869656524,
        2.0121720834000003
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.0156760836000003,
      "stddev": 0.04960481641907888,
      "median": 1.9996550498999999,
      "user": 2.756370059999999,
      "system": 3.38692956,
      "min": 1.9683130404,
      "max": 2.0994513224,
      "times": [
        2.0108227824,
        2.0949763584000003,
        2.0261276614000003,
        1.9683130404,
        1.9691507074,
        2.0430283694,
        2.0994513224,
        1.9771497464,
        1.9884873174,
        1.9792535304
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 674.5 ± 44.3 651.9 800.0 1.01 ± 0.07
pacquet@main 664.9 ± 17.8 642.1 707.3 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6745418242400001,
      "stddev": 0.044310850919803864,
      "median": 0.66219082494,
      "user": 0.36276392,
      "system": 1.4696764,
      "min": 0.65187713694,
      "max": 0.80004579694,
      "times": [
        0.80004579694,
        0.6595529289400001,
        0.66735121294,
        0.66314118194,
        0.66389601294,
        0.6622652289400001,
        0.65187713694,
        0.6558037359400001,
        0.66211642094,
        0.65936858594
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.66486694484,
      "stddev": 0.017803650976564295,
      "median": 0.6618010724400001,
      "user": 0.36756222,
      "system": 1.4696258,
      "min": 0.6421068549400001,
      "max": 0.70727225494,
      "times": [
        0.70727225494,
        0.65160886994,
        0.6581838199400001,
        0.67610966994,
        0.6700006429400001,
        0.65458849294,
        0.65737921594,
        0.6654183249400001,
        0.6421068549400001,
        0.6660013019400001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.507 ± 0.036 2.442 2.560 1.00
pacquet@main 2.530 ± 0.032 2.460 2.580 1.01 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.50709399194,
      "stddev": 0.03615962778133472,
      "median": 2.5106795546400003,
      "user": 4.35471506,
      "system": 3.3743764199999995,
      "min": 2.4420806766400003,
      "max": 2.56034813164,
      "times": [
        2.53957350664,
        2.49282806764,
        2.56034813164,
        2.5291739996400002,
        2.53168050264,
        2.4790251476400003,
        2.52853104164,
        2.48748588564,
        2.48021295964,
        2.4420806766400003
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.5304964525399996,
      "stddev": 0.032186319320799184,
      "median": 2.53320625764,
      "user": 4.363230960000001,
      "system": 3.4064678200000005,
      "min": 2.4595790656400003,
      "max": 2.58012752564,
      "times": [
        2.53392119164,
        2.4595790656400003,
        2.53565574864,
        2.54092255864,
        2.52217078464,
        2.50659967464,
        2.56273993764,
        2.5307567146400003,
        2.53249132364,
        2.58012752564
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.698 ± 0.018 1.674 1.725 1.00
pacquet@main 1.737 ± 0.057 1.673 1.843 1.02 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.6980263944,
      "stddev": 0.017960931154754077,
      "median": 1.6994840429,
      "user": 2.0220623200000003,
      "system": 2.1646251,
      "min": 1.6735413478999999,
      "max": 1.7245118999,
      "times": [
        1.6791626308999998,
        1.7051523019,
        1.7230177719000002,
        1.6828550969,
        1.6735413478999999,
        1.7245118999,
        1.7036446409000001,
        1.6953234448999999,
        1.7087264619,
        1.6843283468999999
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.7374809892999998,
      "stddev": 0.05716856612507281,
      "median": 1.7216004564,
      "user": 2.0623148199999997,
      "system": 2.1725880999999996,
      "min": 1.6730279219,
      "max": 1.8425915049,
      "times": [
        1.7219198139,
        1.8285008259,
        1.7247565719,
        1.8425915049,
        1.7212810989,
        1.7658578429,
        1.6974547308999999,
        1.6730279219,
        1.6996646929,
        1.6997548889
      ]
    }
  ]
}

@github-actions

github-actions Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/11905
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,507.09 ms
(-42.87%)Baseline: 4,388.55 ms
5,266.26 ms
(47.61%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,698.03 ms
(-49.81%)Baseline: 3,382.93 ms
4,059.51 ms
(41.83%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
2,081.37 ms
(-9.11%)Baseline: 2,289.87 ms
2,747.84 ms
(75.75%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
674.54 ms
(+2.34%)Baseline: 659.10 ms
790.92 ms
(85.29%)
🐰 View full continuous benchmarking report in Bencher

zkochan added 2 commits May 25, 2026 11:15
…nstall

The fresh-resolve install path (no `--frozen-lockfile`, no usable lockfile)
only resolved the workspace root manifest, so sibling workspace projects'
own dependencies never landed in the lockfile or on disk. Re-run
`resolve_importer` per importer with shared install caches
(`meta_cache`, `fetch_locker`, `picked_manifest_cache`), merge the
per-importer graphs, and emit one `importers[<id>]` entry per project.

Mirrors upstream's [`resolveRootDependencies`](https://github.com/pnpm/pnpm/blob/3422cecfd3/installing/deps-resolver/src/resolveDependencies.ts#L327-L437)
iteration shape — one shared resolution context, per-importer
direct-deps slices.

Per-importer `link_bins` so each project gets its own
`node_modules/.bin`. GVS `register_project` now loops every importer
key the freshly-built lockfile carries, mirroring the frozen path.

`importer_dep_version` and `snapshot_dep_ref` learned a `link:`
short-circuit so workspace-sibling edges emit
`ImporterDepVersion::Link` / `SnapshotDepRef::Link` instead of
falling through to the `name@version` parser.

Cross-importer `TreeCtx` sharing (full upstream parity: one
resolution context with per-importer hoist loops) is deferred — each
`resolve_importer` call still has its own context. Network-side
caches still amortize packument fetches and JSON parsing across
importers; only per-resolve semver matching duplicates.

Closes #11901.
@zkochan zkochan marked this pull request as ready for review May 25, 2026 09:39
@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 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

🧹 Nitpick comments (1)
pacquet/crates/cli/tests/workspace_install.rs (1)

87-126: ⚡ Quick win

Add debug output before these non-assert_eq! assertions.

These assertions are doing structural checks over paths and lockfile content; adding dbg! right before them makes CI failures much easier to diagnose.

Suggested update
     let a_dep = workspace.join("packages/a/node_modules/@pnpm.e2e/hello-world-js-bin-parent");
+    dbg!(&a_dep);
     assert!(
         is_symlink_or_junction(&a_dep).expect("query packages/a symlink"),
         "packages/a/node_modules direct-dep symlink missing — sibling importer's deps weren't walked",
     );
     let b_dep = workspace.join("packages/b/node_modules/@pnpm.e2e/hello-world-js-bin");
+    dbg!(&b_dep);
     assert!(
         is_symlink_or_junction(&b_dep).expect("query packages/b symlink"),
         "packages/b/node_modules direct-dep symlink missing — sibling importer's deps weren't walked",
     );
@@
     let lockfile_path = workspace.join("pnpm-lock.yaml");
     let lockfile = fs::read_to_string(&lockfile_path).expect("read pnpm-lock.yaml");
+    dbg!(&lockfile_path);
+    dbg!(&lockfile);

As per coding guidelines "Log before non-assert_eq! assertions, dbg! complex structures, skip logging for simple scalar assert_eq!."

🤖 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/workspace_install.rs` around lines 87 - 126, Add
debug output (use dbg! or similar) immediately before each non-assert_eq!
structural assertion in the test so CI failures show the inspected values:
before the is_symlink_or_junction(&a_dep) and is_symlink_or_junction(&b_dep)
checks, before the
workspace.join("node_modules/.pnpm/@pnpm.e2e+hello-world-js-bin-parent@1.0.0").exists()
and
workspace.join("node_modules/.pnpm/@pnpm.e2e+hello-world-js-bin@1.0.0").exists()
checks, and before reading/inspecting the lockfile (the lockfile string used for
lockfile.contains(...) assertions). Ensure the dbg! calls print the path values
and the lockfile string so failing assertions show the actual values.
🤖 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/cli/tests/workspace_install.rs`:
- Around line 123-126: The assertion currently checks
lockfile.contains("hello-world-js-bin-parent") which can match anywhere; change
it to assert that the importer-scoped section for packages/a contains the entry.
Update the test to search within the "importers:\n  packages/a" block (e.g., use
a regex or substring search that finds the "importers:\n  packages/a" header and
then verifies "hello-world-js-bin-parent" appears after it) instead of a global
contains on lockfile; reference the existing lockfile variable and the string
"hello-world-js-bin-parent" and replace the single assert in
workspace_install.rs with an importer-scoped check.

In `@pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs`:
- Around line 484-492: The loop in compute_corrected_optional is treating
unknown aliases as Prod (using
manifest_alias_to_group(...).get(alias).copied().unwrap_or(DependencyGroup::Prod)),
which wrongly seeds optionality for aliases that aren’t declared in the
manifest; instead, only consider aliases that exist in manifest_alias_to_group:
if manifest_alias_to_group(input.manifest).get(alias) returns None, skip that
alias (do not push to dev_seeds/optional_seeds/prod_seeds). Update the matching
logic in the loop over importer_inputs to only act on Some(group) (referencing
compute_corrected_optional, manifest_alias_to_group, importer_inputs, and
direct_dependencies_by_alias).

---

Nitpick comments:
In `@pacquet/crates/cli/tests/workspace_install.rs`:
- Around line 87-126: Add debug output (use dbg! or similar) immediately before
each non-assert_eq! structural assertion in the test so CI failures show the
inspected values: before the is_symlink_or_junction(&a_dep) and
is_symlink_or_junction(&b_dep) checks, before the
workspace.join("node_modules/.pnpm/@pnpm.e2e+hello-world-js-bin-parent@1.0.0").exists()
and
workspace.join("node_modules/.pnpm/@pnpm.e2e+hello-world-js-bin@1.0.0").exists()
checks, and before reading/inspecting the lockfile (the lockfile string used for
lockfile.contains(...) assertions). Ensure the dbg! calls print the path values
and the lockfile string so failing assertions show the actual values.
🪄 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: f879cb5d-8f78-446a-b36a-dcbe69fb6ea8

📥 Commits

Reviewing files that changed from the base of the PR and between 494cdca and 7616edb.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • pacquet/crates/cli/tests/workspace_install.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/workspace/Cargo.toml
  • pacquet/crates/workspace/src/importer_id.rs
  • pacquet/crates/workspace/src/lib.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). (1)
  • GitHub Check: ubuntu-latest / Node.js 24 / Test
🧰 Additional context used
📓 Path-based instructions (2)
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/workspace/src/importer_id.rs
  • pacquet/crates/workspace/src/lib.rs
  • pacquet/crates/cli/tests/workspace_install.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
pacquet/**/{tests,test}/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/{tests,test}/**/*.rs: Prefer real fixtures; reach for the dependency-injection seam only when they can't cover the branch. Most happy paths and error paths should be tested with tempfile::TempDir, the mocked registry, or an integration test that spawns the actual binary.
Use the DI seam — a capability trait on the Host provider, threaded as Sys: <Bounds> — only for branches a real fixture can't reach portably: filesystem error kinds, deterministic time, shared process-global state, or external-service happy paths.
Log before non-assert_eq! assertions, dbg! complex structures, skip logging for simple scalar assert_eq!. Follow the test-logging guidance in CODE_STYLE_GUIDE.md.
Tests must not be tolerant of missing build/runtime environment by silently returning early when a tool isn't found. If the test needs a tool, let it panic when the tool is absent.
Prefer #[cfg_attr(target_os = "windows", ignore = "...")] (or #[cfg(unix)]) over a runtime probe-and-skip helper for platform-locked tools.
Port relevant tests from pnpm when porting behavior. Matching test coverage is the easiest way to prove behavioral parity.
When porting behavior from pnpm, consult plans/TEST_PORTING.md before adding ported tests. Use allow_known_failure! at the not-yet-implemented boundary and update checkboxes as items land.

Files:

  • pacquet/crates/cli/tests/workspace_install.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/workspace/src/importer_id.rs
  • pacquet/crates/workspace/src/lib.rs
  • pacquet/crates/cli/tests/workspace_install.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.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/workspace/src/importer_id.rs
  • pacquet/crates/workspace/src/lib.rs
  • pacquet/crates/cli/tests/workspace_install.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.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/workspace/src/importer_id.rs
  • pacquet/crates/workspace/src/lib.rs
  • pacquet/crates/cli/tests/workspace_install.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
🧬 Code graph analysis (6)
pacquet/crates/workspace/src/lib.rs (1)
pacquet/crates/workspace/src/importer_id.rs (4)
  • returns_dot_for_root (38-40)
  • returns_posix_relative_for_subproject (43-48)
  • nested_subproject (51-53)
  • importer_id_from_root_dir (15-30)
pacquet/crates/cli/tests/workspace_install.rs (2)
pacquet/crates/testing-utils/src/bin.rs (1)
  • AddMockedRegistry (40-49)
pacquet/crates/testing-utils/src/fs.rs (1)
  • is_symlink_or_junction (45-59)
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs (1)
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs (3)
  • dependencies_graph_to_lockfile (87-117)
  • GraphToLockfileOptions (45-69)
  • ImporterLockfileInput (33-42)
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs (3)
pacquet/crates/package-manager/src/install.rs (1)
  • a (280-896)
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs (3)
  • GraphToLockfileOptions (45-69)
  • ImporterLockfileInput (33-42)
  • dependencies_graph_to_lockfile (87-117)
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs (2)
  • ResolveImporterOptions (52-94)
  • resolve_importer (119-259)
pacquet/crates/package-manager/src/install.rs (4)
pacquet/crates/lockfile/src/lib.rs (2)
  • Lockfile (61-91)
  • Lockfile (93-174)
pacquet/crates/package-manager/src/symlink_direct_dependencies.rs (1)
  • importer_root_dir (308-319)
pacquet/crates/workspace/src/importer_id.rs (1)
  • importer_id_from_root_dir (15-30)
pacquet/crates/workspace/src/manifest.rs (1)
  • WorkspaceManifest (47-71)
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs (2)
pacquet/crates/resolving-deps-resolver/src/dependencies_graph.rs (1)
  • DependenciesGraph (15-15)
pacquet/crates/lockfile/src/snapshot_entry.rs (1)
  • SnapshotEntry (17-45)
🔇 Additional comments (14)
pacquet/crates/package-manager/src/install.rs (4)

690-727: LGTM!

The workspace project loading and importer_manifests construction correctly:

  1. Loads projects once via load_workspace_projects for reuse
  2. Always inserts the root importer (".") with the in-memory manifest (preserving pacquet add behavior)
  3. Skips re-inserting the root if it appears in workspace_projects (the continue guard at line 721)
  4. Uses importer_id_from_root_dir for consistent POSIX-relative path computation

764-786: LGTM!

The GVS registration loop correctly mirrors the frozen-lockfile path's behavior: iterates all importers from the freshly-built lockfile, falls back to root when no lockfile was written (config.lockfile=false), and uses the same best-effort warn-on-failure pattern.


1215-1223: LGTM!

The helper correctly distinguishes the three states: no workspace manifest (Ok(None)), workspace manifest with patterns (Some with patterns), and workspace manifest without patterns (defaults handled by find_workspace_projects).


1238-1257: LGTM!

The refactored signature correctly accepts the pre-loaded projects slice and propagates the None state for non-workspace installs, avoiding duplicate disk I/O.

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs (6)

94-101: LGTM!

The field change from single manifest to BTreeMap<String, &'a PackageManifest> correctly models the multi-importer input shape, with clear documentation of the importer id format.


497-513: LGTM!

Preferred-version seeding correctly collects manifests from all importers, ensuring workspace siblings' direct dependencies contribute to the version tie-break table alongside the root's.


537-601: LGTM!

The per-importer resolve loop correctly:

  1. Derives project_dir from each importer's manifest path for correct workspace:/link: path resolution
  2. Merges graphs with first-writer-wins (safe since identical depPaths yield equivalent nodes by resolver construction)
  3. Records direct dependencies per importer for lockfile construction
  4. Emits per-importer peer-issue warnings

863-875: LGTM!

Per-importer bin linking correctly:

  1. Extracts the modules basename (defaults to node_modules if absent)
  2. Computes each importer's project directory via importer_root_dir
  3. Constructs per-importer modules_dir and bins_dir paths
  4. Calls link_bins for each, ensuring every workspace project gets its own .bin/ populated

969-992: LGTM!

The helper correctly collects cache keys from the merged graph using the same key format as the frozen-lockfile path, with appropriate filtering for tarball resolutions that have both integrity and structured name@version.


1002-1033: LGTM!

The helper correctly constructs per-importer ImporterLockfileInput entries by combining each manifest with its direct-deps map from direct_by_importer, then passes the shared merged graph to dependencies_graph_to_lockfile for deduped packages: and snapshots: generation.

pacquet/crates/workspace/Cargo.toml (1)

19-19: LGTM!

pacquet/crates/workspace/src/importer_id.rs (1)

1-7: LGTM!

Also applies to: 10-30, 32-54

pacquet/crates/workspace/src/lib.rs (1)

21-21: LGTM!

Also applies to: 28-28

pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs (1)

1-1: LGTM!

Also applies to: 6-7, 10-12, 16-43, 150-152, 212-214, 291-293, 356-358, 399-401, 453-455, 536-538, 625-627, 639-824

Comment thread pacquet/crates/cli/tests/workspace_install.rs
zkochan added 2 commits May 25, 2026 12:01
…, matching pnpm

Pacquet was looping `register_project` over every importer in both
the frozen-lockfile and fresh-lockfile branches, but upstream pnpm
calls `registerProject(opts.storeDir, opts.lockfileDir)` exactly once
per install against the workspace root — store prune walks the
workspace's `node_modules/.pnpm/` to find every installed package, so
one registry entry per workspace is enough.

Consolidate to a single call near the start of `Install::run`,
matching pnpm's `getContext` ordering at
<https://github.com/pnpm/pnpm/blob/d8a79a9c30/installing/context/src/index.ts#L128>.

Also port two upstream-derived tests that the multi-importer rewrite
of `compute_corrected_optional` and the per-importer link rendering
were previously missing direct coverage for:

- `multi_importer_pruner_marks_shared_dep_non_optional_when_any_importer_reaches_via_prod`
  ports the spirit of pnpm's
  [`pruneSharedLockfile`](https://github.com/pnpm/pnpm/blob/d8a79a9c30/lockfile/pruner/src/index.ts#L17)
  cross-importer pooling: a depPath reached via a non-optional path
  from any importer ends up `optional: false` even when another
  importer reaches it only via an optional path.
- `workspace_sibling_link_renders_per_importer_with_link_ref`
  exercises the multi-importer `workspace:`-link case — importer A
  depends on importer B via a `link:`-resolved depPath, both render
  their own `importers[<id>]` entries, and the link node stays out
  of `packages:` / `snapshots:`.
Addresses CodeRabbit's review on PR #11905. Pacquet's resolver hoists
auto-installed peers into `direct_dependencies_by_alias` even when
they aren't in the importer's manifest (see
`resolve_importer::direct.extend(...)` after each `hoist_peers` call).
`build_importer` correctly excludes those undeclared aliases from the
importer's lockfile entry, but `compute_corrected_optional` was
seeding the pruner BFS from the full `direct_dependencies_by_alias`
and defaulting unknown aliases to `DependencyGroup::Prod`. That
diverges from upstream's
[`pruneSharedLockfile`](https://github.com/pnpm/pnpm/blob/d8a79a9c30/lockfile/pruner/src/index.ts#L27-L29),
which seeds purely from `lockfile.importers[*].{dev,optional,}dependencies`
— i.e., from the same set `build_importer` writes. The mismatch
forced auto-peers reachable only via an optional parent's chain to
`optional: false`, leaking them into non-optional installs.

Skip aliases not in the manifest when seeding. The new test
`auto_installed_peer_not_declared_in_manifest_is_skipped_from_pruner_seeds`
pins the corrected behavior — `peer-x` (auto-installed for an
`optionalDependencies` parent) stays `optional: true`, matching pnpm.
Verified the test fails against the pre-fix code.

Also tightens the multi-importer integration test's lockfile
assertion: scope the `hello-world-js-bin-parent` check to the
`packages/a:` importer section instead of a global substring match,
so the test proves the direct-dep entry — not just any mention in
`packages:`.

@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/package-manager/src/install.rs (1)

718-752: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate the fresh lockfile into workspace-state generation.

When this branch starts without a lockfile, Line 751 captures the newly built multi-importer lockfile, but the later workspace-state write still reads the pre-install lockfile. On a fresh workspace install that means .pnpm-workspace-state-v1.json only records ".", so sibling importers are omitted even though this branch just resolved them. Thread fresh_lockfile.as_ref().or(lockfile) into build_workspace_state(...) so the recorded project set matches the install result.

🤖 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/package-manager/src/install.rs` around lines 718 - 752, The
workspace-state is being built from the pre-install `lockfile` instead of the
newly resolved multi-importer lockfile returned by InstallWithFreshLockfile;
update the call to build_workspace_state to pass the fresh lockfile when present
by threading `fresh_lockfile.as_ref().or(lockfile)` (use the
`fresh_result.wanted_lockfile` / `fresh_lockfile` from the
InstallWithFreshLockfile run) so the generated .pnpm-workspace-state-v1.json
reflects the resolved set of importers rather than the stale/empty pre-install
lockfile.
🧹 Nitpick comments (1)
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs (1)

825-844: ⚡ Quick win

Trim test doc comments to “why” only.

These blocks over-describe setup/flow that the tests already document. Please keep only the non-obvious rationale (and upstream reference), not a prose replay of the test body.

✂️ Suggested simplification
-/// Multi-importer cross-importer pruner BFS. Ported from upstream
-/// pnpm's [...] behavior — `copyPackageSnapshots` pools every importer's
-/// ...
-/// path is all non-optional.
+/// Ported from pnpm pruner behavior: if any importer reaches a shared dep
+/// through a non-optional path, the pruned snapshot must be `optional: false`.

-/// Multi-importer with a `workspace:` link between two siblings.
-/// Ported from the spirit of upstream pnpm's [...]
-/// ...
-/// `snapshots:`.
+/// Ported from pnpm multi-importer workspace-link behavior: importer-to-importer
+/// `workspace:*` edges render as `ImporterDepVersion::Link`, without adding the
+/// linked sibling to `packages`/`snapshots`.

As per coding guidelines "Tests are documentation. Do not duplicate them in prose." and "Doc comments ... are not a re-narration of the body."

Also applies to: 945-953

🤖 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/package-manager/src/dependencies_graph_to_lockfile/tests.rs`
around lines 825 - 844, The long doc comment starting "Multi-importer
cross-importer pruner BFS. Ported from upstream pnpm's `pruneSharedLockfile`..."
should be shortened to a brief "why" note: keep the upstream reference and a
one-sentence rationale explaining the non-obvious behavior being tested (that
importer-level non-optional edges must override optional resolver state), and
delete the step-by-step scenario prose (the bullet list and flow replay) since
the test body already documents setup and behavior; update the comment block
above the test accordingly.
🤖 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/package-manager/src/install.rs`:
- Around line 718-752: The workspace-state is being built from the pre-install
`lockfile` instead of the newly resolved multi-importer lockfile returned by
InstallWithFreshLockfile; update the call to build_workspace_state to pass the
fresh lockfile when present by threading `fresh_lockfile.as_ref().or(lockfile)`
(use the `fresh_result.wanted_lockfile` / `fresh_lockfile` from the
InstallWithFreshLockfile run) so the generated .pnpm-workspace-state-v1.json
reflects the resolved set of importers rather than the stale/empty pre-install
lockfile.

---

Nitpick comments:
In `@pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs`:
- Around line 825-844: The long doc comment starting "Multi-importer
cross-importer pruner BFS. Ported from upstream pnpm's `pruneSharedLockfile`..."
should be shortened to a brief "why" note: keep the upstream reference and a
one-sentence rationale explaining the non-obvious behavior being tested (that
importer-level non-optional edges must override optional resolver state), and
delete the step-by-step scenario prose (the bullet list and flow replay) since
the test body already documents setup and behavior; update the comment block
above the test accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 22eaab6e-30c0-4775-979b-f2e9a47167ce

📥 Commits

Reviewing files that changed from the base of the PR and between 7616edb and 5ce7cec.

📒 Files selected for processing (3)
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/install/tests.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/package-manager/src/install/tests.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
  • pacquet/crates/package-manager/src/install.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/package-manager/src/install/tests.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
  • pacquet/crates/package-manager/src/install.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/package-manager/src/install/tests.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
  • pacquet/crates/package-manager/src/install.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/package-manager/src/install/tests.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
  • pacquet/crates/package-manager/src/install.rs
🔇 Additional comments (2)
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs (1)

846-943: LGTM!

Also applies to: 955-1029

pacquet/crates/package-manager/src/install/tests.rs (1)

2146-2177: ⚡ Quick win

Make this fixture an actual workspace.

Without a pnpm-workspace.yaml, find_workspace_dir falls back to manifest.path().parent(), so this still passes on the single-project path. Add a minimal workspace manifest here so the test really exercises workspace-root discovery instead of the fallback.

[ suggest_recommended_refactor ]

Suggested setup
     let web_dir = workspace_root.join("packages/web");
     std::fs::create_dir_all(&web_dir).expect("create packages/web");
+    std::fs::write(
+        workspace_root.join("pnpm-workspace.yaml"),
+        "packages:\n  - packages/*\n",
+    )
+    .expect("write pnpm-workspace.yaml");
     let manifest_path = workspace_root.join("package.json");
     let manifest = PackageManifest::create_if_needed(manifest_path).unwrap();

…tering project

CI failure: `fresh_install_honors_enable_global_virtual_store` started
failing after the previous register_project consolidation. Two
compounding bugs:

1. `register_project` now runs early in `Install::run`, before any
   install phase has materialized the store. With the test's
   relative `storeDir: ../pacquet-store` in `pnpm-workspace.yaml`,
   `config.store_dir.root()` ends up shaped like
   `<workspace>/../pacquet-store/v11` — a path that doesn't yet
   exist on disk.

2. `path_contains`'s "lexical fallback" wasn't actually lexical —
   it called `dunce::canonicalize`, and on failure (path doesn't
   exist) it kept the path verbatim and ran `starts_with`. So
   `<workspace>/../pacquet-store/v11`.starts_with(`<workspace>`)
   returned true, the early-return guard fired, and the call
   silently skipped without writing the registry entry.

Two-part fix matching upstream:

- `Install::run` now calls `fs::create_dir_all(store_dir.root())`
  before `register_project`, mirroring pnpm's
  [`fs.mkdir(opts.storeDir, { recursive: true })`](https://github.com/pnpm/pnpm/blob/d8a79a9c30/installing/context/src/index.ts#L125)
  call right before `registerProject`. Once the store exists,
  `canonicalize` succeeds and `path_contains` resolves both sides
  correctly.

- `path_contains` now lexically normalizes `.` / `..` components
  when canonicalize fails. Matches upstream's `is-subdir` semantics
  (which uses `path.relative`, purely lexical). New test
  `path_contains_resolves_parent_components_when_paths_do_not_exist`
  pins the behavior; verified it fails against the pre-fix code.

@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 (2)
pacquet/crates/package-manager/src/install.rs (2)

860-863: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Persist workspace state from the importer set that was actually installed.

build_workspace_state derives projects from lockfile.importers, but this write still passes the pre-install lockfile. On a fresh multi-importer workspace install, that can be None or stale, so .pnpm-workspace-state-v1.json misses sibling projects and verifyDepsBeforeRun will keep treating the workspace as outdated.

🤖 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/package-manager/src/install.rs` around lines 860 - 863, The
workspace-state write is using the pre-install lockfile (so
build_workspace_state reads lockfile.importers that may be None/stale); change
the call in update_workspace_state(...) to pass the post-install lockfile (or
the importer set produced by the install operation) instead of the original
`lockfile` so `build_workspace_state` computes `projects` from the actual
installed importers; locate the call to update_workspace_state and replace the
`lockfile` argument with the updated lockfile variable returned by the installer
(or construct the workspace state from that install result) so
`.pnpm-workspace-state-v1.json` includes all sibling projects and
`verifyDepsBeforeRun` sees the correct state.

899-1002: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Run the frozen-lockfile freshness check across every importer.

check_lockfile_freshness still validates only importers["."]. After this multi-importer workspace flow, a sibling package.json can drift while the root stays unchanged, and --frozen-lockfile / auto-frozen can still take the frozen path or even the no-op fast path instead of erroring or falling back to fresh resolution. Based on learnings: when reviewing pacquet Rust port, 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/package-manager/src/install.rs` around lines 899 - 1002, The
function check_lockfile_freshness currently only inspects the single importer
fetched via Lockfile::ROOT_IMPORTER_KEY; change it to iterate over
lockfile.importers (or its keys/entries) and run the same per-importer checks
for each importer: apply parsed_overrides_opt and create a per-importer
manifest_for_freshness (using VersionsOverrider as you do for the root), compute
the ignored_set/is_ignored_optional for that importer, and call
satisfies_package_manifest for each importer id instead of using the single
importer variable; ensure errors map to FreshnessCheckError::Stale as before and
preserve the earlier global settings check
(pacquet_lockfile::check_lockfile_settings) once.
🤖 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/package-manager/src/install.rs`:
- Around line 860-863: The workspace-state write is using the pre-install
lockfile (so build_workspace_state reads lockfile.importers that may be
None/stale); change the call in update_workspace_state(...) to pass the
post-install lockfile (or the importer set produced by the install operation)
instead of the original `lockfile` so `build_workspace_state` computes
`projects` from the actual installed importers; locate the call to
update_workspace_state and replace the `lockfile` argument with the updated
lockfile variable returned by the installer (or construct the workspace state
from that install result) so `.pnpm-workspace-state-v1.json` includes all
sibling projects and `verifyDepsBeforeRun` sees the correct state.
- Around line 899-1002: The function check_lockfile_freshness currently only
inspects the single importer fetched via Lockfile::ROOT_IMPORTER_KEY; change it
to iterate over lockfile.importers (or its keys/entries) and run the same
per-importer checks for each importer: apply parsed_overrides_opt and create a
per-importer manifest_for_freshness (using VersionsOverrider as you do for the
root), compute the ignored_set/is_ignored_optional for that importer, and call
satisfies_package_manifest for each importer id instead of using the single
importer variable; ensure errors map to FreshnessCheckError::Stale as before and
preserve the earlier global settings check
(pacquet_lockfile::check_lockfile_settings) once.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ed399953-71c8-466d-8aa2-a498b3d519b7

📥 Commits

Reviewing files that changed from the base of the PR and between 43380ea and 5376c00.

📒 Files selected for processing (2)
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/store-dir/src/project_registry.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). (8)
  • GitHub Check: Code Coverage
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Doc
  • GitHub Check: Analyze (javascript)
  • 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/store-dir/src/project_registry.rs
  • pacquet/crates/package-manager/src/install.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/store-dir/src/project_registry.rs
  • pacquet/crates/package-manager/src/install.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/store-dir/src/project_registry.rs
  • pacquet/crates/package-manager/src/install.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/store-dir/src/project_registry.rs
  • pacquet/crates/package-manager/src/install.rs
🧬 Code graph analysis (1)
pacquet/crates/package-manager/src/install.rs (2)
pacquet/crates/store-dir/src/store_dir.rs (2)
  • StoreDir (38-59)
  • root (181-183)
pacquet/crates/store-dir/src/project_registry.rs (1)
  • register_project (88-156)
🔇 Additional comments (5)
pacquet/crates/store-dir/src/project_registry.rs (4)

366-371: LGTM!


373-378: LGTM!


380-396: LGTM!


421-443: LGTM!

pacquet/crates/package-manager/src/install.rs (1)

378-405: LGTM!

@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/store-dir/src/project_registry.rs (1)

381-394: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Leading .. components are lost for relative paths.

When lexical_normalize encounters ParentDir on an empty PathBuf, pop() returns false and does nothing. For a relative path like ../foo, the leading .. is silently dropped, producing foo instead of ../foo. The current call site (line 95) likely only passes absolute paths, so this doesn't affect existing behavior, but the function has a general-purpose contract and should handle all valid Path inputs correctly.

🔧 Proposed fix: preserve leading `..` for relative paths
 fn lexical_normalize(path: &Path) -> PathBuf {
     use std::path::Component;
     let mut out = PathBuf::new();
     for component in path.components() {
         match component {
             Component::ParentDir => {
-                out.pop();
+                if !out.pop() {
+                    // pop() returned false → path was empty or at root.
+                    // Preserve leading `..` for relative paths.
+                    out.push(component.as_os_str());
+                }
             }
             Component::CurDir => {}
             other => out.push(other.as_os_str()),
         }
     }
     out
 }
🤖 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/store-dir/src/project_registry.rs` around lines 381 - 394,
lexical_normalize currently drops leading ParentDir components because out.pop()
is a no-op when out is empty; change the ParentDir arm so when out.pop() returns
false you preserve the parent marker by pushing ".." into out (e.g. if
!out.pop() { out.push(".."); }) instead of silently doing nothing; update the
match arm in lexical_normalize (handling Component::ParentDir) so relative paths
like "../foo" keep their leading ".." while existing absolute-path behavior
remains unchanged.
🧹 Nitpick comments (1)
pacquet/crates/store-dir/src/project_registry.rs (1)

428-441: ⚡ Quick win

Consider adding test coverage for relative paths with leading ...

The test validates that lexical_normalize correctly collapses .. in absolute paths, but doesn't cover relative paths with leading .. (e.g., ../sibling). Adding a relative-path test case would catch the bug flagged in lexical_normalize and ensure the fallback path behaves correctly when canonicalization fails on relative inputs.

📋 Example test case
#[test]
fn path_contains_preserves_leading_parent_in_relative_paths() {
    let outer = Path::new("workspace");
    let inner_sibling = Path::new("workspace/../sibling");
    assert!(
        !path_contains(outer, inner_sibling),
        "`workspace/../sibling` lexically resolves to `sibling` — outside workspace",
    );

    let inner_child = Path::new("workspace/child");
    assert!(
        path_contains(outer, inner_child),
        "`workspace/child` is genuinely inside workspace",
    );
}
🤖 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/store-dir/src/project_registry.rs` around lines 428 - 441, Add
a unit test that exercises relative paths with a leading `..` to ensure
path_contains and lexical_normalize handle them correctly: create a new #[test]
(e.g., path_contains_preserves_leading_parent_in_relative_paths) alongside the
existing tests in project_registry.rs that sets outer = Path::new("workspace"),
inner_sibling = Path::new("workspace/../sibling") and inner_child =
Path::new("workspace/child"), then assert that path_contains(outer,
inner_sibling) is false and path_contains(outer, inner_child) is true; this will
catch bugs in lexical_normalize's handling of relative inputs and the
canonicalization fallback used by path_contains.
🤖 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/store-dir/src/project_registry.rs`:
- Around line 381-394: lexical_normalize currently drops leading ParentDir
components because out.pop() is a no-op when out is empty; change the ParentDir
arm so when out.pop() returns false you preserve the parent marker by pushing
".." into out (e.g. if !out.pop() { out.push(".."); }) instead of silently doing
nothing; update the match arm in lexical_normalize (handling
Component::ParentDir) so relative paths like "../foo" keep their leading ".."
while existing absolute-path behavior remains unchanged.

---

Nitpick comments:
In `@pacquet/crates/store-dir/src/project_registry.rs`:
- Around line 428-441: Add a unit test that exercises relative paths with a
leading `..` to ensure path_contains and lexical_normalize handle them
correctly: create a new #[test] (e.g.,
path_contains_preserves_leading_parent_in_relative_paths) alongside the existing
tests in project_registry.rs that sets outer = Path::new("workspace"),
inner_sibling = Path::new("workspace/../sibling") and inner_child =
Path::new("workspace/child"), then assert that path_contains(outer,
inner_sibling) is false and path_contains(outer, inner_child) is true; this will
catch bugs in lexical_normalize's handling of relative inputs and the
canonicalization fallback used by path_contains.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a7a0bd7e-4eb3-4aa2-8a4e-bd0a0d1cb4c5

📥 Commits

Reviewing files that changed from the base of the PR and between 5376c00 and 7271503.

📒 Files selected for processing (2)
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
  • pacquet/crates/store-dir/src/project_registry.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: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: 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/store-dir/src/project_registry.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/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/store-dir/src/project_registry.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/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/store-dir/src/project_registry.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/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/store-dir/src/project_registry.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
🧬 Code graph analysis (1)
pacquet/crates/store-dir/src/project_registry.rs (1)
pacquet/crates/cli/tests/store.rs (1)
  • canonicalize (18-20)
🔇 Additional comments (2)
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs (1)

938-941: LGTM!

pacquet/crates/store-dir/src/project_registry.rs (1)

373-374: LGTM!

… lexical_normalize

Two issues:

1. The multi-line `assert!` in
   `multi_importer_pruner_marks_shared_dep_non_optional_when_any_importer_reaches_via_prod`
   was missing its trailing comma after `cargo fmt` reformatted it from
   one-line to multi-line. Perfectionist's `macro-trailing-comma` rule
   (which CI enforces via Dylint) flagged it. Added the comma.

2. CodeRabbit pointed out that `lexical_normalize` silently dropped
   leading `..` components because `PathBuf::pop()` is a no-op when the
   path is empty. For the current `path_contains` callers (both inputs
   are absolute paths) this doesn't matter, but the helper is now a
   general-purpose utility and the bug would bite any future caller
   passing a relative path.

   Replaced the naive `out.pop()` with a match on the trailing
   component:
   - `Component::Normal(_)` → pop (real segment collapses with `..`)
   - `Component::RootDir | Prefix(_)` → drop the `..` (`/..` is `/` per
     POSIX)
   - else → push `..` (preserve leading `..` chain in relative paths)

   Matches Go's `path.Clean` semantics. New test
   `lexical_normalize_handles_parent_dir_corner_cases` pins all four
   corner cases.
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.

pacquet: resolve all workspace projects in the fresh-resolve install path

2 participants