Skip to content
This repository was archived by the owner on May 14, 2026. It is now read-only.

feat(package-manager): linkHoistedModules linker (#438 slice 5)#505

Merged
zkochan merged 2 commits into
mainfrom
feat/438-slice-5
May 13, 2026
Merged

feat(package-manager): linkHoistedModules linker (#438 slice 5)#505
zkochan merged 2 commits into
mainfrom
feat/438-slice-5

Conversation

@zkochan

@zkochan zkochan commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Slice 5 of #438. The hoisted linker — produces the on-disk node_modules/ tree from Slice 4's LockfileToDepGraphResult. New module crates/package-manager/src/link_hoisted_modules.rs (~280 LoC + ~360 LoC tests). Ports installing/deps-restorer/src/linkHoistedModules.ts.

Three phases

  1. Orphan removal. Every directory in prev_graph but not in graph is silently rimraf'd before any insert. try_remove_dir swallows all errors (NotFound + PermissionDenied + EBUSY), matching upstream's tryRemoveDir tolerance at linkHoistedModules.ts:70-86.
  2. Per-node import. Hierarchy walked top-down, rayon-parallel at each level. Every node goes through Slice 1's import_indexed_dir with force: true, keep_modules_dir: true — exactly the hoisted-linker call shape Slice 1 was designed for.
  3. Per-node_modules bin link. After a level's children are all imported, link_direct_dep_bins populates <parent>/node_modules/.bin. Bin link runs only after every child's subtree is done so the package.json reads always see the fully-populated package.

API shape

pub fn link_hoisted_modules<R: Reporter>(
    opts: &LinkHoistedModulesOpts<'_>,
) -> Result<(), LinkHoistedModulesError>;

LinkHoistedModulesOpts borrows graph, prev_graph, hierarchy, plus a cas_paths_by_pkg_id: HashMap<String, HashMap<String, PathBuf>> keyed by pkg_id_with_patch_hash.

Decoupling from the store

Upstream's linker is async and calls storeController.fetchPackage() inline during the walk. Pacquet's is sync and takes pre-fetched CAS paths. Slice 6 (the install pipeline) will be responsible for fetching every package through pacquet's existing tarball / store-dir / git-fetcher chain before invoking the linker.

Rationale: pacquet's fetch infrastructure is already mature and exercised by the isolated path. Decoupling fetch from layout keeps Slice 5 focused on the on-disk topology, makes the linker easily testable with synthetic CAS files (no mock store needed), and avoids inventing a hoisted-only async fetch path.

The same CAS map is keyed per-package (not per-directory) because version-conflict layouts have one pkg_id_with_patch_hash landing at multiple directories — the CAS contents are the same regardless.

Optional-dep tolerance

A graph node whose pkg_id_with_patch_hash is missing from cas_paths_by_pkg_id:

  • If node.optional: silently skipped, no directory created. Mirrors upstream's if (depNode.optional) return on fetch failure.
  • Otherwise: surfaces as LinkHoistedModulesError::MissingCasPaths { pkg_id, dir } — caller bug (incomplete pre-fetch), not a runtime failure.

Tests

7 real-tempdir tests in link_hoisted_modules/tests.rs. Each plants synthetic CAS files (<cas_root>/<pkg_id>/<rel_path> with arbitrary content), builds a graph + hierarchy by hand, runs the linker, then asserts the on-disk tree.

  • import_pass_creates_package_directory — single-package smoke.
  • orphan_directory_is_removedprev_graph diff produces rimraf; planted stale contents are gone after.
  • nested_hierarchy_materializes_inner_node_modules — version-conflict layout; loser ends up at <outer>/node_modules/<inner>.
  • missing_cas_for_required_dep_errors — required + missing CAS → typed MissingCasPaths error with pkg_id for caller diagnostics.
  • missing_cas_for_optional_dep_skips_silently — optional + missing CAS → no error, no directory.
  • no_prev_graph_skips_orphan_pass — fresh install (no prior lockfile) path.
  • orphan_already_removed_is_tolerated — phantom orphan in prev_graph not present on disk doesn't error (matches tryRemoveDir's NotFound tolerance).

Test plan

  • All 7 linker tests pass.
  • just ready (878 → 885 tests, all green).
  • cargo doc --document-private-items, taplo format --check, just dylint clean.
  • Sabotage check: stubbing try_remove_dir to a no-op re-fails orphan_directory_is_removed (stale dir still there). Stubbing import_node to early-return re-fails import_pass_creates_package_directory and nested_hierarchy_materializes_inner_node_modules.

What's next

Slice 6 — wire the linker into the install pipeline. The pipeline:

  1. Calls lockfile_to_hoisted_dep_graph(lockfile, current_lockfile, opts) (Slice 4).
  2. Fetches every package in result.graph via existing pacquet-tarball / pacquet-store-dir infrastructure, building a cas_paths_by_pkg_id map.
  3. Calls link_hoisted_modules(opts) (this slice).
  4. Persists .modules.yaml.hoisted_dependencies and .modules.yaml.hoisted_locations from result.
  5. Branches the install pipeline on Config.node_linker == Hoisted to take this path instead of the isolated path.

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

Summary by CodeRabbit

  • New Features

    • Added a hoisted module linker to materialize node_modules with parallel package-level processing and optional cleanup of orphaned directories from previous installs.
    • Introduced explicit error handling for missing package data and missing graph references.
  • Tests

    • Added comprehensive tests covering package import, orphan cleanup behavior, nested hierarchies, optional vs required dependency handling, and related error cases.

Review Change Stack

New module `crates/package-manager/src/link_hoisted_modules.rs`
produces the on-disk `node_modules/` tree from Slice 4's
`LockfileToDepGraphResult`. Ports
installing/deps-restorer/src/linkHoistedModules.ts.

## Three phases

1. **Orphan removal.** Every directory in `prev_graph` but not
   in `graph` is silently `rimraf`'d before any insert.
   `try_remove_dir` swallows all errors (NotFound +
   PermissionDenied + EBUSY), matching upstream's `tryRemoveDir`
   tolerance.
2. **Per-node import.** Hierarchy walked top-down, rayon-parallel
   at each level. Every node goes through `import_indexed_dir`
   with `force: true, keep_modules_dir: true` — the hoisted-linker
   call shape Slice 1 was designed for.
3. **Per-`node_modules` bin link.** After a level's children are
   all imported, `link_direct_dep_bins` populates
   `<parent>/node_modules/.bin` from the just-materialized direct
   children. Bin link runs only after every child's subtree is
   done so package.json reads always see the fully-populated
   package.

## API shape

```rust
pub fn link_hoisted_modules<R: Reporter>(
    opts: &LinkHoistedModulesOpts<'_>,
) -> Result<(), LinkHoistedModulesError>;
```

`LinkHoistedModulesOpts` borrows `graph`, `prev_graph`,
`hierarchy`, and a `cas_paths_by_pkg_id: HashMap<String,
HashMap<String, PathBuf>>` keyed by `pkg_id_with_patch_hash`.

## Decoupling from the store

Upstream's linker is async and calls
`storeController.fetchPackage()` inline during the walk —
pacquet's is sync and takes pre-fetched CAS paths. Slice 6 (the
install pipeline) is responsible for fetching every package
through pacquet's existing tarball / store-dir / git-fetcher
machinery before invoking the linker. This keeps the linker
focused on file-system layout and reuses the proven fetch
chain that the isolated path already exercises.

## Optional-dep tolerance

A graph node whose `pkg_id_with_patch_hash` is missing from
`cas_paths_by_pkg_id`:
- If `node.optional`: silently skipped, no directory created.
  Mirrors upstream's `if (depNode.optional) return` on fetch
  failure.
- Otherwise: surfaces as
  `LinkHoistedModulesError::MissingCasPaths { pkg_id, dir }`.

## Tests

7 real-tempdir tests in `link_hoisted_modules/tests.rs`:

- `import_pass_creates_package_directory` — single-package smoke.
- `orphan_directory_is_removed` — `prev_graph` diff produces
  rimraf of stale directory; planted contents are gone after.
- `nested_hierarchy_materializes_inner_node_modules` —
  version-conflict layout; loser ends up at
  `<outer>/node_modules/<inner>`.
- `missing_cas_for_required_dep_errors` — required + missing
  CAS → typed `MissingCasPaths` error.
- `missing_cas_for_optional_dep_skips_silently` — optional +
  missing CAS → no error, no directory.
- `no_prev_graph_skips_orphan_pass` — fresh install (no prior
  lockfile) path.
- `orphan_already_removed_is_tolerated` — phantom orphan in
  `prev_graph` not present on disk doesn't error.

Each test plants synthetic CAS files in a tempdir and asserts
the on-disk tree after the linker runs.
Copilot AI review requested due to automatic review settings May 13, 2026 22:21
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds a synchronous hoisted-module linker that materializes node_modules/ hierarchies, removes orphaned directories from a previous graph, imports packages per-dependency level in parallel, and links package binaries; it exposes public types/errors and includes comprehensive tests.

Changes

Hoisted Module Linker Implementation

Layer / File(s) Summary
Module declaration and export
crates/package-manager/src/lib.rs
New link_hoisted_modules module declared and re-exported via pub use to expose the linker API.
Public API types and configuration
crates/package-manager/src/link_hoisted_modules.rs
Module docs/imports and public contract: CasPathsByPkgId alias, LinkHoistedModulesOpts<'a> config struct, and LinkHoistedModulesError enum (including MissingCasPaths and MissingGraphNode).
Core linking logic
crates/package-manager/src/link_hoisted_modules.rs
link_hoisted_modules entrypoint orchestrates orphan removal and per-importer parallel hierarchy processing. remove_orphans computes and removes directories present in prev_graph but absent from current graph (removal errors ignored). link_all_pkgs_in_order performs level-by-level parallel imports and links direct-dep bins. import_node looks up CAS paths, skips missing optional nodes, and calls import_indexed_dir with force: true and keep_modules_dir: true. try_remove_dir tolerates missing paths.
Test fixtures and helpers
crates/package-manager/src/link_hoisted_modules/tests.rs (helpers)
Test helpers and fixture builders: stub resolution, make_node, plant_cas_file, plant_package, and flat_layout to prepare graph/hierarchy/CAS test inputs.
Test cases
crates/package-manager/src/link_hoisted_modules/tests.rs (tests)
Tests cover: importing a package from CAS into node_modules, orphan-directory removal, nested node_modules materialization, required vs optional missing CAS behavior, skipping orphan pass when prev_graph is None, tolerating already-removed orphans, and reporting missing graph nodes referenced by the hierarchy.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pacquet#441: Introduces import_indexed_dir, which this linker calls with force: true, keep_modules_dir: true.
  • pnpm/pacquet#494: Produces prev_graph data consumed by this linker for orphan removal.
  • pnpm/pacquet#478: Defines the hoisted dependency graph types that this linker consumes.

Suggested reviewers

  • anthonyshew
  • anonrig

Poem

🐰 I hopped through CAS and directories bright,
Imported packages each level by night.
Orphans politely sent on their way,
Binaries linked to dance and to play.
Hoisted modules snug, all tidy and right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(package-manager): linkHoistedModules linker (#438 slice 5)' clearly describes the main feature being added—a new hoisted module linker implementation in the package-manager crate, directly matching the PR's primary change.
Description check ✅ Passed The description comprehensively covers the changes with detailed sections on the implementation phases, API design, decoupling rationale, test coverage, and next steps. However, the 'Upstream reference' section is not populated with a specific pnpm commit link, and the checklist items are not checked off.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/438-slice-5

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

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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

31-48: ⚡ Quick win

Add one explicit test for phase-3 .bin linking.

Current cases validate import/orphan flows, but none verify that direct-dep bins are actually linked into <parent>/node_modules/.bin. Since this is core behavior of this linker, adding a focused case (has_bin = true + manifest/bin entry + assert linked shim exists) would close a meaningful coverage gap.

Also applies to: 108-136

🤖 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 `@crates/package-manager/src/link_hoisted_modules/tests.rs` around lines 31 -
48, Add an explicit test that verifies phase-3 `.bin` linking by creating a
DependenciesGraphNode with has_bin = true and a manifest that declares a bin
(use or extend the helper make_node to set has_bin = true and add a manifest/bin
entry), run the hoisted/linking flow used by the existing tests, and assert that
the linked shim file exists under the parent node's node_modules/.bin; update or
add assertions analogous to the import/orphan cases (see the other tests around
lines 108-136) to check for the presence and correctness of the shim.
crates/package-manager/src/link_hoisted_modules.rs (1)

9-10: ⚡ Quick win

Use pnpm blob/main links in porting references, not pinned SHAs.

These doc links point to a fixed commit SHA. For porting notes in this repo, link to https://github.com/pnpm/pnpm/blob/main/... so references track upstream tip.

As per coding guidelines: "If reading on GitHub, open files from https://github.com/pnpm/pnpm/blob/main/... (which always resolves to the tip of main) rather than from a permalinked SHA."

Also applies to: 75-76, 166-167, 180-180

🤖 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 `@crates/package-manager/src/link_hoisted_modules.rs` around lines 9 - 10,
Update the pinned GitHub URLs in the module doc comments so they point to the
upstream main branch instead of a specific SHA; replace occurrences like
"https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-restorer/src/linkHoistedModules.ts"
(and the other similar blob/<sha> links found in this file) with
"https://github.com/pnpm/pnpm/blob/main/..." so the porting/reference links
track the tip of upstream main (look for the doc comment at the top referencing
installing/deps-restorer/src/linkHoistedModules.ts and the other occurrences
noted in the file).
🤖 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 `@crates/package-manager/src/link_hoisted_modules.rs`:
- Around line 203-207: The code currently ignores hierarchy entries not present
in opts.graph (in the block around import_node::<R> and
link_all_pkgs_in_order::<R>), which can hide inconsistent state; change this to
fail fast: when iterating hierarchy entries, if opts.graph.get(dir) returns
None, return an Err (or propagate a suitable error) instead of continuing, so
the caller sees the missing node; update the call sites in
link_all_pkgs_in_order::<R> (and any surrounding closure) to propagate that
error and ensure import_node::<R> is only called when the node exists.

---

Nitpick comments:
In `@crates/package-manager/src/link_hoisted_modules.rs`:
- Around line 9-10: Update the pinned GitHub URLs in the module doc comments so
they point to the upstream main branch instead of a specific SHA; replace
occurrences like
"https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-restorer/src/linkHoistedModules.ts"
(and the other similar blob/<sha> links found in this file) with
"https://github.com/pnpm/pnpm/blob/main/..." so the porting/reference links
track the tip of upstream main (look for the doc comment at the top referencing
installing/deps-restorer/src/linkHoistedModules.ts and the other occurrences
noted in the file).

In `@crates/package-manager/src/link_hoisted_modules/tests.rs`:
- Around line 31-48: Add an explicit test that verifies phase-3 `.bin` linking
by creating a DependenciesGraphNode with has_bin = true and a manifest that
declares a bin (use or extend the helper make_node to set has_bin = true and add
a manifest/bin entry), run the hoisted/linking flow used by the existing tests,
and assert that the linked shim file exists under the parent node's
node_modules/.bin; update or add assertions analogous to the import/orphan cases
(see the other tests around lines 108-136) to check for the presence and
correctness of the shim.
🪄 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: 49d0ba8a-d172-4c79-bdf3-58759699132b

📥 Commits

Reviewing files that changed from the base of the PR and between e10d0e5 and b9c870f.

📒 Files selected for processing (3)
  • crates/package-manager/src/lib.rs
  • crates/package-manager/src/link_hoisted_modules.rs
  • crates/package-manager/src/link_hoisted_modules/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: copilot-pull-request-reviewer
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (windows-latest)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Preserve existing method chains and pipe-trait chains; do not break them into intermediate let bindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Path over &PathBuf, &str over &String) when it does not force extra copies.
Prefer Arc::clone(&x) and Rc::clone(&x) over x.clone() for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*; for any glob whose target is a module you control. External-crate preludes (e.g., use rayon::prelude::*;) and root-of-module re-exports (e.g., pub use submodule::*; in lib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plain String or &str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only via TryFrom<String> and/or FromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallible From<String> and From<&str> constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bare as assertion), add a from_str_unchecked (or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...

Files:

  • crates/package-manager/src/lib.rs
  • crates/package-manager/src/link_hoisted_modules/tests.rs
  • crates/package-manager/src/link_hoisted_modules.rs
🧠 Learnings (4)
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.

Applied to files:

  • crates/package-manager/src/lib.rs
  • crates/package-manager/src/link_hoisted_modules/tests.rs
  • crates/package-manager/src/link_hoisted_modules.rs
📚 Learning: 2026-05-13T19:22:48.951Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 478
File: crates/package-manager/src/hoisted_dep_graph.rs:51-55
Timestamp: 2026-05-13T19:22:48.951Z
Learning: When reviewing this Rust codebase, avoid introducing/using a newtype like `PkgIdWithPatchHash` in only one module (e.g., `hoisted_dep_graph.rs`) if other related pacquet modules still represent the upstream pnpm “pkg id with patch hash” as a plain `String` (e.g., `virtual_store_layout`). For type consistency, either keep `pkg_id_with_patch_hash` as `String` here, or require a workspace-wide sweep that defines/extracts the `PkgIdWithPatchHash` newtype once and updates all call sites together (otherwise defer the refactor to a coordinated follow-up PR).

Applied to files:

  • crates/package-manager/src/lib.rs
  • crates/package-manager/src/link_hoisted_modules/tests.rs
  • crates/package-manager/src/link_hoisted_modules.rs
📚 Learning: 2026-05-13T20:09:22.171Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 486
File: crates/package-manager/src/hoisted_dep_graph.rs:472-476
Timestamp: 2026-05-13T20:09:22.171Z
Learning: In pnpm/pacquet, when generating serialized `hoisted_locations` strings (used for `.modules.yaml`), normalize path separators to forward slashes (e.g., replace `\\` with `/`). This preserves cross-platform portability: the output must not use OS-native separators because upstream pnpm’s `path.relative` can return platform-specific separators, which would break consistency with pnpm/pacquet’s other serialized formats on Windows.

Applied to files:

  • crates/package-manager/src/lib.rs
  • crates/package-manager/src/link_hoisted_modules/tests.rs
  • crates/package-manager/src/link_hoisted_modules.rs
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.

Applied to files:

  • crates/package-manager/src/link_hoisted_modules/tests.rs
🔇 Additional comments (1)
crates/package-manager/src/lib.rs (1)

22-22: LGTM!

Also applies to: 51-51

Comment thread crates/package-manager/src/link_hoisted_modules.rs Outdated
@github-actions

github-actions Bot commented May 13, 2026

Copy link
Copy Markdown

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.02     16.0±0.99ms   271.8 KB/sec    1.00     15.7±0.34ms   275.9 KB/sec

@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 88.77%. Comparing base (e10d0e5) to head (1c8bd36).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
crates/package-manager/src/link_hoisted_modules.rs 98.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #505      +/-   ##
==========================================
+ Coverage   88.64%   88.77%   +0.13%     
==========================================
  Files         122      124       +2     
  Lines       13273    13481     +208     
==========================================
+ Hits        11766    11968     +202     
- Misses       1507     1513       +6     

☔ 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 13, 2026

Copy link
Copy Markdown

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.536 ± 0.149 2.416 2.919 1.00 ± 0.07
pacquet@main 2.526 ± 0.077 2.436 2.654 1.00
pnpm 5.788 ± 0.082 5.616 5.895 2.29 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.53620429276,
      "stddev": 0.14877825075382206,
      "median": 2.48826210446,
      "user": 2.5879179199999998,
      "system": 3.5229927399999994,
      "min": 2.4157728089600004,
      "max": 2.9190474869600003,
      "times": [
        2.46346541196,
        2.49042132096,
        2.4574863169600003,
        2.49827702296,
        2.9190474869600003,
        2.5903062929600003,
        2.48610288796,
        2.4157728089600004,
        2.61330365496,
        2.42785972296
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.52625407056,
      "stddev": 0.07666256647697456,
      "median": 2.5226902679600003,
      "user": 2.57613512,
      "system": 3.4889083399999996,
      "min": 2.43599593796,
      "max": 2.65370025096,
      "times": [
        2.56186010796,
        2.65370025096,
        2.47358514496,
        2.44473736296,
        2.5699225779600003,
        2.43599593796,
        2.62660440596,
        2.45075438096,
        2.5150820359600004,
        2.5302984999600002
      ]
    },
    {
      "command": "pnpm",
      "mean": 5.78752966726,
      "stddev": 0.08170896348108272,
      "median": 5.79738035046,
      "user": 8.52204042,
      "system": 4.250535739999999,
      "min": 5.61555806196,
      "max": 5.89507961896,
      "times": [
        5.85952344796,
        5.61555806196,
        5.84960905296,
        5.89507961896,
        5.81607234296,
        5.75676881396,
        5.82962287096,
        5.77868835796,
        5.76197953096,
        5.712394573959999
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 734.7 ± 32.3 701.3 818.3 1.00
pacquet@main 798.6 ± 28.4 758.0 841.5 1.09 ± 0.06
pnpm 2410.8 ± 57.8 2334.1 2528.8 3.28 ± 0.16
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.7347355185,
      "stddev": 0.0323450196251902,
      "median": 0.7288519614,
      "user": 0.38175722,
      "system": 1.6118197200000002,
      "min": 0.7013344709,
      "max": 0.8183022909,
      "times": [
        0.8183022909,
        0.7168065459,
        0.7453467889,
        0.7389289629,
        0.7270919559,
        0.7306119669,
        0.7013344709,
        0.7104549589,
        0.7208800909,
        0.7375971529
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.7986435606,
      "stddev": 0.028447303754218094,
      "median": 0.7981882034000001,
      "user": 0.37981712,
      "system": 1.6360847200000002,
      "min": 0.7579989379,
      "max": 0.8414859769,
      "times": [
        0.8414859769,
        0.7986936019,
        0.7786476409,
        0.7691700369,
        0.7579989379,
        0.8131094729,
        0.7760288619,
        0.8163438029,
        0.8372744689,
        0.7976828049
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.4107878164,
      "stddev": 0.05783420265254261,
      "median": 2.3887526394,
      "user": 2.85625852,
      "system": 2.17947262,
      "min": 2.3340787319,
      "max": 2.5287781469,
      "times": [
        2.5287781469,
        2.3838982389,
        2.3340787319,
        2.4839500189000003,
        2.3779281999,
        2.3927630199,
        2.4105214929,
        2.3847422589000002,
        2.4378942149,
        2.3733238409
      ]
    }
  ]
}

…review)

The hierarchy walk silently skipped entries missing from
`graph`, which would produce a partial install layout instead
of surfacing the bug. Slice 4's walker keeps the two in sync
today, but a future bug there shouldn't yield a partial tree.

Add `LinkHoistedModulesError::MissingGraphNode { dir }` and
return it when a hierarchy directory has no graph entry.
Upstream effectively errors here too — its `graph[dir].fetching`
read would throw `Cannot read properties of undefined` — pacquet
just spells the failure out.

Regression test `hierarchy_entry_missing_from_graph_errors`
exercises the path with an empty graph and a hierarchy
referencing a phantom dir.

Caught by Coderabbit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/package-manager/src/link_hoisted_modules/tests.rs (1)

152-152: ⚡ Quick win

Add failure context to bare assert! checks.

Line 152 and Line 343 use bare assert! without diagnostic context, which makes failures harder to triage from CI logs.

Suggested patch
-    assert!(orphan_dir.exists());
+    assert!(orphan_dir.exists(), "expected planted orphan dir to exist before linker run: {orphan_dir:?}");
...
-    assert!(lockfile_dir.join("node_modules").join("a").join("package").join("index.js").exists());
+    let installed = lockfile_dir.join("node_modules").join("a").join("package").join("index.js");
+    assert!(installed.exists(), "expected installed file to exist: {installed:?}");

Based on learnings: In Rust test code, add logging/diagnostic context for assert!-style assertions so failures are debuggable without reruns.

Also applies to: 343-343

🤖 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 `@crates/package-manager/src/link_hoisted_modules/tests.rs` at line 152,
Replace the bare assertions that call assert!(orphan_dir.exists()) (at the
occurrences referencing the orphan_dir variable around the tests) with
assertions that include diagnostic context; e.g. change to
assert!(orphan_dir.exists(), "expected orphan_dir to exist: {}",
orphan_dir.display()) or use Debug: assert!(orphan_dir.exists(), "orphan_dir
missing: {:?}", orphan_dir) so failures print the path and make CI failures
debuggable. Ensure you update both occurrences (the one around the orphan_dir
variable at the first failing check and the second at line ~343) to include
similar messages.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/package-manager/src/link_hoisted_modules/tests.rs`:
- Line 152: Replace the bare assertions that call assert!(orphan_dir.exists())
(at the occurrences referencing the orphan_dir variable around the tests) with
assertions that include diagnostic context; e.g. change to
assert!(orphan_dir.exists(), "expected orphan_dir to exist: {}",
orphan_dir.display()) or use Debug: assert!(orphan_dir.exists(), "orphan_dir
missing: {:?}", orphan_dir) so failures print the path and make CI failures
debuggable. Ensure you update both occurrences (the one around the orphan_dir
variable at the first failing check and the second at line ~343) to include
similar messages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: caf0d379-6746-4465-9409-e4ff0df3ab28

📥 Commits

Reviewing files that changed from the base of the PR and between b9c870f and 1c8bd36.

📒 Files selected for processing (2)
  • crates/package-manager/src/link_hoisted_modules.rs
  • crates/package-manager/src/link_hoisted_modules/tests.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/package-manager/src/link_hoisted_modules.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: Code Coverage
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Dylint
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Preserve existing method chains and pipe-trait chains; do not break them into intermediate let bindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Path over &PathBuf, &str over &String) when it does not force extra copies.
Prefer Arc::clone(&x) and Rc::clone(&x) over x.clone() for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*; for any glob whose target is a module you control. External-crate preludes (e.g., use rayon::prelude::*;) and root-of-module re-exports (e.g., pub use submodule::*; in lib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plain String or &str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only via TryFrom<String> and/or FromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallible From<String> and From<&str> constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bare as assertion), add a from_str_unchecked (or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...

Files:

  • crates/package-manager/src/link_hoisted_modules/tests.rs
🧠 Learnings (4)
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.

Applied to files:

  • crates/package-manager/src/link_hoisted_modules/tests.rs
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.

Applied to files:

  • crates/package-manager/src/link_hoisted_modules/tests.rs
📚 Learning: 2026-05-13T19:22:48.951Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 478
File: crates/package-manager/src/hoisted_dep_graph.rs:51-55
Timestamp: 2026-05-13T19:22:48.951Z
Learning: When reviewing this Rust codebase, avoid introducing/using a newtype like `PkgIdWithPatchHash` in only one module (e.g., `hoisted_dep_graph.rs`) if other related pacquet modules still represent the upstream pnpm “pkg id with patch hash” as a plain `String` (e.g., `virtual_store_layout`). For type consistency, either keep `pkg_id_with_patch_hash` as `String` here, or require a workspace-wide sweep that defines/extracts the `PkgIdWithPatchHash` newtype once and updates all call sites together (otherwise defer the refactor to a coordinated follow-up PR).

Applied to files:

  • crates/package-manager/src/link_hoisted_modules/tests.rs
📚 Learning: 2026-05-13T20:09:22.171Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 486
File: crates/package-manager/src/hoisted_dep_graph.rs:472-476
Timestamp: 2026-05-13T20:09:22.171Z
Learning: In pnpm/pacquet, when generating serialized `hoisted_locations` strings (used for `.modules.yaml`), normalize path separators to forward slashes (e.g., replace `\\` with `/`). This preserves cross-platform portability: the output must not use OS-native separators because upstream pnpm’s `path.relative` can return platform-specific separators, which would break consistency with pnpm/pacquet’s other serialized formats on Windows.

Applied to files:

  • crates/package-manager/src/link_hoisted_modules/tests.rs
🔇 Additional comments (1)
crates/package-manager/src/link_hoisted_modules/tests.rs (1)

1-151: LGTM!

Also applies to: 153-342, 344-425

@zkochan zkochan merged commit d5b33e7 into main May 13, 2026
16 checks passed
@zkochan zkochan deleted the feat/438-slice-5 branch May 13, 2026 22:53
zkochan added a commit that referenced this pull request May 13, 2026
…508)

The Slice 5 linker (#505) was written against `String` for
`pkg_id_with_patch_hash` and merged onto a main that had already
switched the underlying `DependenciesGraphNode` field to the
`PkgIdWithPatchHash` newtype (#504). The merge auto-resolved
without flagging the type incompatibility — `cargo check` on
main now errors with:

    error[E0277]: the trait bound
      `String: Borrow<PkgIdWithPatchHash>` is not satisfied
    error[E0308]: mismatched types
      expected `String`, found `PkgIdWithPatchHash`

Switch the two slice-5-introduced surfaces to match the newtype:

- `CasPathsByPkgId = HashMap<PkgIdWithPatchHash, HashMap<String,
  PathBuf>>` — the per-package CAS index now keys on the brand
  the graph node carries, matching the post-#504 invariant
  across the workspace.
- `LinkHoistedModulesError::MissingCasPaths.pkg_id_with_patch_hash:
  PkgIdWithPatchHash` — same type the graph node has, so the
  error round-trips the value without `.to_string()`.

Tests updated to construct `PkgIdWithPatchHash::from(...)`
keys/fields. All 8 linker tests pass on the fixed branch.

This unblocks main building again.
@zkochan zkochan mentioned this pull request May 14, 2026
59 tasks
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants