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

feat(package-manager): hoisted dep-graph type skeleton (#438 slice 4a)#478

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

feat(package-manager): hoisted dep-graph type skeleton (#438 slice 4a)#478
zkochan merged 2 commits into
mainfrom
feat/438-slice-4a

Conversation

@zkochan

@zkochan zkochan commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Sub-slice 4a of #438 (the nodeLinker: 'hoisted' umbrella). Types-only skeleton for the directory-keyed dependency graph that the hoisted-linker install path will produce.

Following the same shape as Sub-slice 3a (pacquet-real-hoist crate skeleton): pin the types first, fill in the walker / store-fetch / installability-check / prev-graph diff in follow-up sub-slices.

What lands

New module crates/package-manager/src/hoisted_dep_graph.rs with:

  • DependenciesGraphNode — mirrors upstream's DependenciesGraphNode minus fetching and files_index_file (only the store-controller-bound walker can populate those; deferred to 4c).
  • DependenciesGraph = BTreeMap<PathBuf, DependenciesGraphNode>, keyed by absolute directory path. Unlike pacquet's existing depPath-keyed deps_graph module (a side-effects-cache adapter), this graph keys by directory because hoisted nodes can occupy several directories when a name conflict forces nesting.
  • DepHierarchy — recursive directory tree the linker walks to decide population order and which <dir>/node_modules/.bin to wire up. Newtype-wrapped because Rust doesn't allow recursive type aliases.
  • DirectDependenciesByImporterId — per-importer alias → dir table the bin pass consumes.
  • LockfileToDepGraphResult — bundle of everything the walker returns, including prev_graph for the orphan-diff pass (Slice 5) and injection_targets_by_dep_path for the injected-workspace re-mirror step.
  • LockfileToHoistedDepGraphOptions — subset of upstream's options that the to-be-ported walker actually reads today (lockfile dir, autoInstallPeers, skipped set). Store-controller and workspace project list will be added when their consumers land.

Decisions worth flagging

  • Module placement — went into crates/package-manager/src/ as a sibling to existing deps_graph.rs and hoist.rs rather than a new crate. Can extract later if the walker grows enough to warrant it; for now, factoring it out would just add pacquet- plumbing without any callers in or out of the workspace.
  • PartialEq only, not EqLockfileResolution is PartialEq only (the lockfile-types convention; some inner variants don't qualify), so propagating Eq here would force a divergence. Matches the existing lockfile convention.
  • pkg_id_with_patch_hash: String — kept as a plain String to match the convention virtual_store_layout already uses; the upstream brand is non-validating so the type-safety win is small.

Test plan

  • default_result_is_empty — empty-result smoke, the walker's no-importer case.
  • graph_node_inserts_by_dir — types compose, a node round-trips through BTreeMap insertion.
  • hierarchy_nests_recursively — the recursive DepHierarchy newtype nests, supports Default, and survives equality.
  • options_default_is_emptyLockfileToHoistedDepGraphOptions::default() yields the empty options shape.
  • just ready (859 → 863 tests, all green).
  • cargo doc --document-private-items, taplo format --check, just dylint all clean.

What's next

  • 4bwalk_hoist_result: given a pacquet_real_hoist::HoisterResult + the lockfile, build the graph / hierarchy / hoisted_locations. No store I/O, no installability check.
  • 4c — wire the store fetch + installability check; populate fetching / files_index_file and respect the skipped set.
  • 4dprev_graph diff via the current-lockfile fall-back loop, unblocking Slice 5's orphan removal.

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

Summary by CodeRabbit

  • Refactor

    • Introduced new public types and re-exports to support a directory-keyed hoisted dependency graph and its walker options.
  • Tests

    • Added unit tests covering empty defaults, graph insertion/query behavior, recursive hierarchy semantics, and options defaults.

Review Change Stack

Review Change Stack

Defines the directory-keyed dependency graph types used by the
hoisted-linker install path. Types only — the walker that fills
them from a lockfile lands in a follow-up.

Ported from upstream:

- `DependenciesGraphNode` mirrors deps/graph-builder
  `DependenciesGraphNode` minus the `fetching` / `files_index_file`
  fields that only the store-controller-bound walker can populate.
- `DependenciesGraph` = `BTreeMap<PathBuf, DependenciesGraphNode>`,
  keyed by absolute directory path (unlike pacquet's existing
  depPath-keyed `deps_graph` module — hoisted nodes can occupy
  several directories when a name conflict forces nesting).
- `DepHierarchy` is the recursive directory tree the linker
  walks to decide population order and which
  `<dir>/node_modules/.bin` to wire up. Newtype-wrapped because
  Rust doesn't allow recursive type aliases.
- `DirectDependenciesByImporterId` is the per-importer
  alias-to-directory table the linker hands to the bin pass.
- `LockfileToDepGraphResult` bundles everything the walker
  returns to the install pipeline, including `prevGraph` for the
  orphan-diff pass and `injectionTargetsByDepPath` for the
  injected-workspace re-mirror step.
- `LockfileToHoistedDepGraphOptions` carries the subset of
  upstream's options that the to-be-ported walker actually reads
  today; fields tied to the store controller, fetch concurrency,
  and workspace project list will be added when their consumers
  land.

Smoke tests cover empty-result default construction, node
insertion by `dir`, recursive hierarchy nesting, and
default-options shape.

Upstream:
- <https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-restorer/src/lockfileToHoistedDepGraph.ts>
- <https://github.com/pnpm/pnpm/blob/94240bc046/deps/graph-builder/src/lockfileToDepGraph.ts>
Copilot AI review requested due to automatic review settings May 13, 2026 18:45
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1946b1f2-658b-4847-9e93-8b7e93edebf3

📥 Commits

Reviewing files that changed from the base of the PR and between 8d21cc5 and d6f9a07.

📒 Files selected for processing (1)
  • crates/package-manager/src/hoisted_dep_graph.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/package-manager/src/hoisted_dep_graph.rs
📜 Recent 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 (ubuntu-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Dylint
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest

📝 Walkthrough

Walkthrough

Adds a new types-only module hoisted_dep_graph that defines directory-keyed hoisted dependency graph types (node, graph map, recursive hierarchy), walker input/output contracts, and unit tests; the module is declared and re-exported from the crate root.

Changes

Hoisted Dependency Graph Types and Integration

Layer / File(s) Summary
Hoisted dependency graph types and validation
crates/package-manager/src/hoisted_dep_graph.rs
Introduces DependenciesGraphNode with package identity, paths, children, and metadata; DependenciesGraph as a BTreeMap keyed by PathBuf; DepHierarchy as a recursive nested map; DirectDependenciesByImporterId mapping; LockfileToDepGraphResult as the walker output including hoisted/symlinked maps and optional previous-graph; and LockfileToHoistedDepGraphOptions for walker inputs. Adds module docs, imports, and unit tests validating defaults, insertion/query, and recursive nesting/equality.
Module declaration and re-export
crates/package-manager/src/lib.rs
Declares mod hoisted_dep_graph; and re-exports its public items via pub use hoisted_dep_graph::*;.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

Poem

🐰 A tiny graph beneath my paws I sow,
Nodes in neat BTree rows begin to grow,
Hierarchies nest like tunnels, snug and deep,
Tests hop in to check that promises keep,
The hoisted map now hums — a carrot-coded glow! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: introduction of a type skeleton for a hoisted dependency graph in the package-manager crate, directly matching the changeset.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering summary, design decisions, and test plan. However, it does not include an explicit 'Linked issue' section with a reference to the Stage 1 roadmap item.
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-4a

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.

Pull request overview

Introduces a new types-only module in package-manager that defines the directory-keyed dependency graph shapes needed for the future nodeLinker: 'hoisted' install path, mirroring pnpm’s lockfileToHoistedDepGraph result structure.

Changes:

  • Added crates/package-manager/src/hoisted_dep_graph.rs with hoisted dep-graph node/result/options types plus basic smoke tests.
  • Wired the new module into crates/package-manager/src/lib.rs (module declaration + re-export).

Reviewed changes

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

File Description
crates/package-manager/src/lib.rs Exposes the new hoisted_dep_graph module via mod and pub use.
crates/package-manager/src/hoisted_dep_graph.rs Adds directory-keyed hoisted dep-graph types (DependenciesGraphNode, DependenciesGraph, DepHierarchy, result/options structs) and tests.
Comments suppressed due to low confidence (1)

crates/package-manager/src/hoisted_dep_graph.rs:161

  • LockfileToHoistedDepGraphOptions.skipped is described as a set of depPaths, but it’s currently BTreeSet<String>. Consider making this BTreeSet<DepPath> (matching DependenciesGraphNode.dep_path) to keep the depPath concept type-safe and avoid accidentally inserting non-depPath strings.
    /// Packages the previous install decided not to fetch
    /// (installability check failed; the package was added here).
    /// The walker skips any depPath in this set without consulting
    /// the snapshot. Cloned + extended on the way out.
    pub skipped: BTreeSet<String>,
}

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

Comment thread crates/package-manager/src/hoisted_dep_graph.rs
Comment thread crates/package-manager/src/hoisted_dep_graph.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@crates/package-manager/src/hoisted_dep_graph.rs`:
- Around line 51-55: Replace plain String fields with dedicated newtype
wrappers: declare branded newtypes (e.g., struct PkgIdWithPatchHash(String);
struct DepPath(String);) and change the HoistedDepGraph struct fields
pkg_id_with_patch_hash: PkgIdWithPatchHash and skipped: Option<DepPath> (or
Vec<DepPath> depending on existing shape). Implement or derive common traits
used across the crate (Clone, Debug, Eq, Hash, PartialEq, Serialize/Deserialize,
AsRef<str>, From<String>) to preserve behavior, update all usages of
pkg_id_with_patch_hash and skipped accordingly, and adjust any serde attributes
or conversions to serialize/deserialize as plain strings so external format
stays unchanged.
- Around line 4-6: Update the module doc comment in hoisted_dep_graph.rs that
references pnpm files pinned to a historical SHA: change the two URLs in the
top-level doc comment (the links to
installing/deps-restorer/src/lockfileToHoistedDepGraph.ts and
deps/graph-builder/src/lockfileToDepGraph.ts) to use
https://github.com/pnpm/pnpm/blob/main/... instead of the current SHA-permalink
so the rustdoc references follow the pnpm-porting guideline; locate the
module-level comment at the top of hoisted_dep_graph.rs and replace the two
blob/94240bc046 links with blob/main equivalents.
🪄 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: f20c9adc-c3c6-4b00-91c3-3095b2080c32

📥 Commits

Reviewing files that changed from the base of the PR and between d8e7807 and 8d21cc5.

📒 Files selected for processing (2)
  • crates/package-manager/src/hoisted_dep_graph.rs
  • crates/package-manager/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). (7)
  • GitHub Check: Code Coverage
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Agent
🧰 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/hoisted_dep_graph.rs
🧠 Learnings (1)
📚 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/hoisted_dep_graph.rs
🔇 Additional comments (2)
crates/package-manager/src/hoisted_dep_graph.rs (1)

163-263: LGTM!

crates/package-manager/src/lib.rs (1)

11-11: LGTM!

Also applies to: 38-38

Comment thread crates/package-manager/src/hoisted_dep_graph.rs
Comment thread crates/package-manager/src/hoisted_dep_graph.rs
@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.16%. Comparing base (b799b7e) to head (d6f9a07).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #478      +/-   ##
==========================================
+ Coverage   89.07%   90.16%   +1.09%     
==========================================
  Files         116      120       +4     
  Lines       10537    11190     +653     
==========================================
+ Hits         9386    10090     +704     
+ Misses       1151     1100      -51     

☔ 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

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.02     15.9±0.59ms   273.4 KB/sec    1.00     15.6±0.38ms   277.7 KB/sec

@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.543 ± 0.099 2.375 2.688 1.00
pacquet@main 2.592 ± 0.091 2.488 2.778 1.02 ± 0.05
pnpm 5.952 ± 0.087 5.830 6.091 2.34 ± 0.10
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.54298726562,
      "stddev": 0.0988009517369913,
      "median": 2.53583516942,
      "user": 2.62217738,
      "system": 3.55876476,
      "min": 2.3754539764200002,
      "max": 2.6883421314200002,
      "times": [
        2.55197400942,
        2.51969632942,
        2.6883421314200002,
        2.68595170142,
        2.5993628024200004,
        2.5772607834200003,
        2.4857345524200003,
        2.46347073642,
        2.48262563342,
        2.3754539764200002
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.59159664262,
      "stddev": 0.09113541510934174,
      "median": 2.56235451442,
      "user": 2.6006301799999996,
      "system": 3.5319191599999997,
      "min": 2.48834877542,
      "max": 2.77790607242,
      "times": [
        2.69517858842,
        2.51135268342,
        2.53640362942,
        2.77790607242,
        2.48834877542,
        2.57111424442,
        2.53082892242,
        2.60018718442,
        2.55359478442,
        2.65105154142
      ]
    },
    {
      "command": "pnpm",
      "mean": 5.951930542919999,
      "stddev": 0.08747994144381466,
      "median": 5.945351586419999,
      "user": 8.779565380000001,
      "system": 4.328316060000001,
      "min": 5.83033964642,
      "max": 6.09062137842,
      "times": [
        5.889903611419999,
        6.002673166419999,
        6.09062137842,
        6.06319456442,
        5.91030641042,
        5.9983306144199995,
        5.83033964642,
        5.98039676242,
        5.87133149642,
        5.88220777842
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 761.0 ± 33.2 723.5 831.0 1.00
pacquet@main 828.7 ± 41.9 786.2 903.7 1.09 ± 0.07
pnpm 2541.2 ± 105.3 2417.7 2726.6 3.34 ± 0.20
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.7610469290800002,
      "stddev": 0.03318120273592099,
      "median": 0.75630045158,
      "user": 0.38207409999999997,
      "system": 1.64145072,
      "min": 0.72351142758,
      "max": 0.83103234758,
      "times": [
        0.83103234758,
        0.77891187358,
        0.76850358758,
        0.73180079158,
        0.74224290758,
        0.7310728145800001,
        0.7907926375800001,
        0.72351142758,
        0.76604875058,
        0.74655215258
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.8286955598799999,
      "stddev": 0.041912958968324476,
      "median": 0.81300396408,
      "user": 0.37940970000000007,
      "system": 1.66425612,
      "min": 0.78616306958,
      "max": 0.90372159358,
      "times": [
        0.89329378358,
        0.80129011958,
        0.82340047858,
        0.90372159358,
        0.79603295758,
        0.79616168758,
        0.78616306958,
        0.83089931258,
        0.85338514658,
        0.80260744958
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.54120520338,
      "stddev": 0.10529132250452168,
      "median": 2.51994132508,
      "user": 2.9451355,
      "system": 2.22479902,
      "min": 2.41769332258,
      "max": 2.72656625158,
      "times": [
        2.51073329958,
        2.4671339465799997,
        2.65993191258,
        2.60941122358,
        2.52914935058,
        2.44212749658,
        2.41769332258,
        2.60976891158,
        2.43953631858,
        2.72656625158
      ]
    }
  ]
}

…478 review)

- Replace v5-era `/accepts/1.3.7` depPath strings in the smoke
  test with v9-shape `accepts@1.3.7` (the format pacquet's
  `PkgNameVerPeer` produces and the lockfile snapshots use). The
  legacy `/name/version` shape is only kept for read-side
  `hoistedAliases` compatibility. Caught by Copilot.
- Tighten doc-comments on `hoisted_locations`,
  `injection_targets_by_dep_path`, and `skipped` to spell out
  *why* the keys are plain `String` (not `DepPath`): upstream
  types those fields with raw `string` in the hoisted-specific
  interface, even though the values are populated from depPaths.
  Mirrors upstream's literal typing.
@zkochan zkochan merged commit 996eb08 into main May 13, 2026
16 checks passed
@zkochan zkochan deleted the feat/438-slice-4a branch May 13, 2026 19:33
zkochan added a commit that referenced this pull request May 13, 2026
Sub-slice 4b of #438. Implements `lockfile_to_hoisted_dep_graph(lockfile, opts) -> LockfileToDepGraphResult` — the walker that consumes the Slice 3 hoister output and produces the directory-keyed graph + hierarchy + hoisted_locations + direct-deps-by-importer-id + injection-targets-by-dep-path that 4a (#478) pinned the type shape of.

Single-importer only (multi-importer lockfiles surface as `HoistError::UnsupportedWorkspace` via the underlying hoister).

## What's deferred

Three things are intentionally left to follow-ups so 4b stays focused:

- **Store fetch wiring** — `fetching` / `files_index_file` on the graph node are still defaulted. 4c will wire `StoreController::fetchPackage` (and the skip-fetch optimization that consults `currentHoistedLocations`).
- **Installability check** — the walker honors `opts.skipped` as input but doesn't run `packageIsInstallable` to add new entries. 4c will plumb `package_is_installable` from `pacquet-package-is-installable` (already used by the isolated linker path).
- **`prev_graph` diff** — `prev_graph` is `None`. 4d will walk the *current* lockfile alongside the wanted one (using `force: true` + empty `skipped` per upstream) and produce the orphan-removal input Slice 5's linker consumes.

## Two-pass design

Upstream's `fetchDeps` walks asynchronously via `await Promise.all(deps.map(async (dep) => { ... }))`. The key invariant: each sibling's async function runs its sync prologue (insert + push to `pkgLocationsByDepPath`) before any continuation fires, because `Promise.all` sync-invokes each function and the inner `await fetchDeps(...)` only yields after the prologue completes. So by the time any node's post-recursion `graph[dir].children = getChildren(...)` line runs, every node in the entire tree is already in `pkgLocationsByDepPath`.

Pacquet runs synchronously. To preserve that invariant cleanly:

1. **Pass 1 (recursive walk).** Insert each node into the graph with `children: BTreeMap::new()` and push its dir to `pkg_locations_by_dep_path`. Recursion order matches upstream's outer body (insert → push → recurse → push to `hoistedLocations`).
2. **Pass 2 (`fill_children`).** Iterate every graph node and resolve `children: alias → dir` from the snapshot's `dependencies` + `optionalDependencies` via `SnapshotDepRef::resolve`, consulting the now-complete `pkg_locations_by_dep_path`.

A single-pass walker that computed children inline produced incorrect `children` maps for hoisted transitive deps — the canonical case `root → a → b` with `b` flattened to root left `a.children["b"]` empty because `b`'s pkg_locations entry hadn't been recorded yet. Caught by `walker_transitive_dep_flattens_under_root` before the refactor.

## Tests

- `walker_empty_lockfile_produces_empty_result` — empty importer → empty graph, with the `"."` root importer key still present.
- `walker_single_root_dep_emits_one_node` — `root → a`, node at `<lockfile_dir>/node_modules/a`.
- `walker_transitive_dep_flattens_under_root` — `root → a → b`: `b` hoists to root, `a.children["b"]` points at the root-level dir.
- `walker_version_conflict_keeps_loser_nested` — `root → {a@1, c}` with `c → a@2`: `a@1` at root, `a@2` nested under `c`; `hoisted_locations` records both directories; `c.children["a"]` points at the nested copy.
- `walker_honors_pre_skipped_dep_path` — depPaths in `opts.skipped` are dropped from the walk entirely (and not recorded in `hoisted_locations`).
- `walker_records_directory_resolution_as_injection_target` — `directory:` resolutions populate `injection_targets_by_dep_path` for the post-install re-mirror pass.
zkochan added a commit that referenced this pull request May 13, 2026
…ph nodes (#481) (#504)

* feat(lockfile): PkgIdWithPatchHash newtype, replace `String` in dep-graph nodes (#481)

Port upstream's [`PkgIdWithPatchHash`](https://github.com/pnpm/pnpm/blob/94240bc046/core/types/src/misc.ts) branded
string (`string & { __brand: 'PkgIdWithPatchHash' }`) as a
non-validating newtype in `pacquet-lockfile`, matching
`CLAUDE.md`'s rule 3 for non-validating brands: `From<String>` /
`From<&str>` via `derive_more`, `#[serde(transparent)]` for
wire-format identity with `String`, no validating constructor.

Modeled on `pacquet_modules_yaml::DepPath` — the sibling brand from
the same upstream `misc.ts` file under the same rules.

Replaces the plain `String` field/parameter in the two pacquet
consumers CodeRabbit flagged on #478:

- `DependenciesGraphNode.pkg_id_with_patch_hash` in
  `package-manager/src/hoisted_dep_graph.rs`.
- `create_full_pkg_id`'s first parameter and the
  `lockfile_to_dep_graph` local in `package-manager/src/virtual_store_layout.rs`.

A workspace audit (`grep "pkg_id_with_patch_hash"`) turns up no
other slots typed as plain `String`.

Two new tests in `pkg_id_with_patch_hash.rs` pin the contract: a
serde round-trip (`#[serde(transparent)]` keeps the on-disk shape a
raw string) and the non-validating-construction property (empty
string and `From<&str>`/`From<String>` both work).

* fix(lockfile): replace intra-doc links to pacquet-modules-yaml with plain text

CI \`Doc\` job runs with \`RUSTDOCFLAGS=-D warnings\`, so the two
\`[\`pacquet_modules_yaml::DepPath\`]\` intra-doc links in
\`pkg_id_with_patch_hash.rs\` failed \`rustdoc::broken-intra-doc-links\`:
\`pacquet-lockfile\` doesn't depend on \`pacquet-modules-yaml\` (the
dependency would go the wrong way), so the rustdoc resolver can't
follow the link.

Switched both references to bare \`pacquet_modules_yaml::DepPath\`
prose, with a note that the bare reference is intentional. The
docstring still tells a reader where to find the sibling brand; it
just stops resolving as a clickable link, which costs nothing
practical since the type name is searchable in any IDE.

Adding \`pacquet-modules-yaml\` as a dev-dep purely for a doc link
would invert the natural crate ordering (lockfile is upstream of
modules-yaml in the build graph) — rejected as a cosmetic fix that
introduces a real architectural smell.
@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