feat(pacquet): port auto-install-peers algorithm#11784
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📜 Recent review details🧰 Additional context used📓 Path-based instructions (1)pacquet/**/*.rs📄 CodeRabbit inference engine (pacquet/AGENTS.md)
Files:
🧠 Learnings (2)📚 Learning: 2026-05-20T19:40:55.051ZApplied to files:
📚 Learning: 2026-05-20T23:07:58.444ZApplied to files:
🔇 Additional comments (1)
📝 WalkthroughWalkthroughImplements importer-level multi-pass peer auto-install hoisting seeded by preferred-versions. Adds a preferred-versions crate, config/workspace/env wiring, resolver refactor (TreeCtx), hoist decision logic, resolve_importer orchestration, install integration, and unit + e2e tests. ChangesPeer Auto-Install with Preferred Versions
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Replaces the placeholder peer-folding behavior with a faithful port of pnpm's `hoistPeers` algorithm. Missing required peers are hoisted to the importer's direct deps (shared across consumers, not nested), with a multi-pass loop that re-resolves until no required peer remains and the optional-peer pass picks already-available versions from the preferred-versions map. - New `pacquet-lockfile-preferred-versions` crate seeds the version- picker tie-break table from manifest + lockfile snapshots. - New `hoist_peers` module ports `hoistPeers` and `getHoistableOptionalPeers` with the full upstream test suite. - New `resolve_importer` orchestrator drives the multi-pass hoist loop and threads `parent_pkg_aliases` / `all_preferred_versions` across iterations. - `resolve_dependency_tree` exposes `TreeCtx` / `extend_tree` / `snapshot` so the orchestrator can extend the tree incrementally without re-walking already-resolved subtrees. - `auto-install-peers-from-highest-match` config setting added, mirroring upstream's flag for range-merge behavior. Ported from pnpm's installing/deps-resolver `resolveRootDependencies` and hoistPeers.ts at commit 097983f. --- Written by an agent (Claude Code, claude-opus-4-7).
29c3fcd to
6a082b8
Compare
Pacquet changes don't get changesets. --- Written by an agent (Claude Code, claude-opus-4-7).
Micro-Benchmark ResultsLinux |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #11784 +/- ##
==========================================
- Coverage 89.69% 89.62% -0.07%
==========================================
Files 174 179 +5
Lines 20781 21540 +759
==========================================
+ Hits 18640 19306 +666
- Misses 2141 2234 +93 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Port nine portable scenarios from
installing/deps-installer/test/install/autoInstallPeers.ts as
orchestrator-level unit tests in resolve_importer/tests.rs:
- skip optional peers without a preferred-version hint
- dedupe via range intersection when consumers agree
- drop the peer when ranges conflict (default)
- install via `||` join when autoInstallPeersFromHighestMatch is on
- reuse a peer already brought by a sibling (preferred-versions)
- skip hoisting when the root already has the dep as a direct
- collapse the same missing peer across a transitive chain
- prefer the version pinned in the importer's own peerDependencies
- reuse a regular-dep version over re-resolving via the peer arm
The last case exposed a gap: the orchestrator wasn't including the
importer's own `peerDependencies` as initial wanted deps when
auto-install-peers is on, mirroring upstream's
`getAllDependenciesFromManifest({ autoInstallPeers: true })`. Fixed
in resolve_importer alongside the test ports.
Workspace, frozen-lockfile mutation, hook-using, override-using,
and webpack-circular-deps tests from the upstream file remain
un-ported pending the matching features in pacquet.
---
Written by an agent (Claude Code, claude-opus-4-7).
CI's Dylint job (Perfectionist lints) and the Doc job both fail on the prior commits' style. Cleanups: - Rename single-letter closure params (`|c|`, `|d|`, `|e|`, `|r|`) to descriptive names in resolve_importer, hoist_peers, version_selector_type, and the install + orchestrator test files. Triggered `perfectionist::single-letter-closure-param`. - Add trailing commas to multi-line `assert!` invocations in resolve_importer/tests.rs. Triggered `perfectionist::macro-trailing-comma`. - Disambiguate `[`crate::resolve_importer`]` and friends with the `[`fn@crate::resolve_importer`]` form — `resolve_importer`, `resolve_peers`, `hoist_peers`, and `get_hoistable_optional_peers` are each both a module and a re-exported function, which rustdoc flags as ambiguous. - Replace the broken `[`resolve_dependency_tree`]` and `[`resolve_peers`]` intra-doc links in install_without_lockfile.rs with prose now that those symbols are no longer in scope there (the orchestrator wraps them). - Drop the link to the private `add_weight_to_version_selector` helper from `get_preferred_versions_from_lockfile_and_manifests`' public docs and the link to the private `resolve_node` from `extend_tree`'s public docs. - Remove the redundant explicit `(crate::extend_tree)` link target in lib.rs's module doc. No behavior change; tests pass. --- Written by an agent (Claude Code, claude-opus-4-7).
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
Integrated-Benchmark Report (Linux)Scenario: Frozen Lockfile
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.4163893410999995,
"stddev": 0.08110956945001237,
"median": 2.3985466618999998,
"user": 2.88233636,
"system": 3.6451205799999995,
"min": 2.3048469174,
"max": 2.5424662774,
"times": [
2.5313044024,
2.3984464124,
2.3986469113999997,
2.3048469174,
2.5424662774,
2.3490470874,
2.3840019094,
2.4411022574,
2.4800195694,
2.3340116664
]
},
{
"command": "pacquet@main",
"mean": 2.3300494469999995,
"stddev": 0.058589129665680174,
"median": 2.3187627599,
"user": 2.8117994600000005,
"system": 3.6234116800000002,
"min": 2.2413363964,
"max": 2.4209025234,
"times": [
2.2963213103999998,
2.3258661694,
2.3116593504,
2.4209025234,
2.3402613514,
2.3072336144,
2.3963413264,
2.2660326734,
2.2413363964,
2.3945397543999998
]
},
{
"command": "pnpm",
"mean": 4.534833480500001,
"stddev": 0.07182000731404627,
"median": 4.5272255234,
"user": 7.59248716,
"system": 4.04474208,
"min": 4.4509111884,
"max": 4.6701781234,
"times": [
4.554055085400001,
4.4898705644,
4.4509111884,
4.457876247400001,
4.542195756400001,
4.5220738334,
4.4909915354,
4.5323772134,
4.6701781234,
4.6378052574
]
}
]
}Scenario: Frozen Lockfile (Hot Cache)
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6988071763600001,
"stddev": 0.02660125506300929,
"median": 0.6915126127600001,
"user": 0.37689414,
"system": 1.59372332,
"min": 0.6791670912600001,
"max": 0.77062211826,
"times": [
0.77062211826,
0.7044259742600001,
0.6908895292600001,
0.6791670912600001,
0.68011071326,
0.6961760482600001,
0.69213569626,
0.6830784312600001,
0.7013890212600001,
0.6900771402600001
]
},
{
"command": "pacquet@main",
"mean": 0.72465910676,
"stddev": 0.06191050679351998,
"median": 0.7037780142600001,
"user": 0.37686733999999994,
"system": 1.5786784199999997,
"min": 0.6629467102600001,
"max": 0.8528714612600001,
"times": [
0.8528714612600001,
0.7117298162600001,
0.6629467102600001,
0.67234057826,
0.7037242452600001,
0.7038317832600001,
0.7019744922600001,
0.6698881992600001,
0.78416508626,
0.7831186952600001
]
},
{
"command": "pnpm",
"mean": 2.4493426161599996,
"stddev": 0.12575504214472605,
"median": 2.38783714576,
"user": 2.9241101400000002,
"system": 2.17693202,
"min": 2.35331689626,
"max": 2.7038990512599996,
"times": [
2.40851200826,
2.37861332726,
2.58996005126,
2.3970609642599996,
2.57637329026,
2.3602561562599997,
2.35331689626,
2.36779367226,
2.35764074426,
2.7038990512599996
]
}
]
} |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
pacquet/crates/resolving-deps-resolver/src/hoist_peers/tests.rs (1)
65-72: ⚡ Quick winAdd a regression case for exact range + no satisfying preferred version with
auto_install_peers = false.That branch should currently omit hoisting, and this specific scenario is not covered yet.
Also applies to: 90-97
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pacquet/crates/resolving-deps-resolver/src/hoist_peers/tests.rs` around lines 65 - 72, Add regression tests that cover the case where a dependency has an exact-range requirement and no preferred version satisfies it while auto_install_peers is false: create a test (similar to falls_back_to_range_when_no_preferred_version_satisfies_it) that calls hoist_peers with opts(auto_install_peers=false, &preferred) and a missing("chai","4.3.0") input, and assert that the result is empty (no hoisting). Repeat the same pattern for the other pair of tests referenced (lines 90-97) to ensure both branches omit hoisting when auto_install_peers is false and the preferred set cannot satisfy the exact selector.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs`:
- Around line 120-133: The current branch lets an exact peer range
(is_exact_version true) fall through to the max_satisfying_any logic when
opts.auto_install_peers is false, which can choose an incompatible preferred
version; instead, detect the exact-range case when auto_install_peers is false
and insert the original exact range into dependencies (using
dependencies.insert(peer_name.clone(), range.clone())) rather than running
max_satisfying_any/parts logic. Update the conditional around
is_exact_version/opts.auto_install_peers (and the else branch that uses
max_satisfying_any, non_versions, parts) so that exact ranges are preserved when
auto_install_peers is false, referencing is_exact_version,
opts.auto_install_peers, max_satisfying_any, non_versions, peer_name and range.
- Around line 170-172: The current max_satisfying implementation uses
Range::satisfies (called on the ranges iterator and later on the candidate
check) which applies strict prerelease rules and thus rejects valid prerelease
versions; update both uses to follow the prerelease-strip-and-retry pattern
implemented in resolve_peers.rs (satisfies_with_prereleases): attempt
Range::satisfies normally, and if it fails and the candidate version is a
prerelease while the range has no prerelease identifiers, strip or ignore the
prerelease and retry the check (i.e., mirror semver.maxSatisfying(..., {
includePrerelease: true })); replace the direct Range::satisfies calls in
max_satisfying with a call to the same helper logic (or extract and reuse
satisfies_with_prereleases) so peer hoisting includes valid prerelease
candidates.
---
Nitpick comments:
In `@pacquet/crates/resolving-deps-resolver/src/hoist_peers/tests.rs`:
- Around line 65-72: Add regression tests that cover the case where a dependency
has an exact-range requirement and no preferred version satisfies it while
auto_install_peers is false: create a test (similar to
falls_back_to_range_when_no_preferred_version_satisfies_it) that calls
hoist_peers with opts(auto_install_peers=false, &preferred) and a
missing("chai","4.3.0") input, and assert that the result is empty (no
hoisting). Repeat the same pattern for the other pair of tests referenced (lines
90-97) to ensure both branches omit hoisting when auto_install_peers is false
and the preferred set cannot satisfy the exact selector.
🪄 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: 9af3a1fa-23f7-4600-9424-fa07b8161c2e
⛔ Files ignored due to path filters (2)
Cargo.lockis excluded by!**/*.lockpacquet/crates/package-manager/src/install/snapshots/pacquet_package_manager__install__tests__should_install_dependencies.snapis excluded by!**/*.snap
📒 Files selected for processing (19)
Cargo.tomlpacquet/crates/cli/tests/install.rspacquet/crates/config/src/env_overlay.rspacquet/crates/config/src/lib.rspacquet/crates/config/src/workspace_yaml.rspacquet/crates/lockfile-preferred-versions/Cargo.tomlpacquet/crates/lockfile-preferred-versions/src/lib.rspacquet/crates/lockfile-preferred-versions/src/tests.rspacquet/crates/lockfile-preferred-versions/src/version_selector_type.rspacquet/crates/package-manager/Cargo.tomlpacquet/crates/package-manager/src/install_package_from_registry/tests.rspacquet/crates/package-manager/src/install_without_lockfile.rspacquet/crates/resolving-deps-resolver/src/hoist_peers.rspacquet/crates/resolving-deps-resolver/src/hoist_peers/tests.rspacquet/crates/resolving-deps-resolver/src/lib.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rspacquet/crates/resolving-deps-resolver/src/resolve_importer.rspacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rspacquet/crates/resolving-deps-resolver/src/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (2)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: When porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(), orstreamParser.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 plainStringor&str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only viaTryFrom<String>and/orFromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallibleFrom<String>(andFrom<&str>when convenient).
If upstream occasionally constructs without validation, exposefrom_str_uncheckedas 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 manualimplonly when conversion needs custom logic.
String-literal unions should becomeenums, 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 (&Pathover&PathBuf,&strover&String) when it doesn't force extra copies.
PreferArc::clone(&x)/Rc::clone(&x)overx.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. Writeuse super::{Foo, bar}instead ofuse super::*;. Two forms stay allowed: external-crate preludes likeuse rayon::prelude::*;and root-of-module re-...
Files:
pacquet/crates/package-manager/src/install_package_from_registry/tests.rspacquet/crates/lockfile-preferred-versions/src/version_selector_type.rspacquet/crates/config/src/env_overlay.rspacquet/crates/config/src/lib.rspacquet/crates/cli/tests/install.rspacquet/crates/config/src/workspace_yaml.rspacquet/crates/lockfile-preferred-versions/src/lib.rspacquet/crates/resolving-deps-resolver/src/hoist_peers.rspacquet/crates/resolving-deps-resolver/src/lib.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/package-manager/src/install_without_lockfile.rspacquet/crates/resolving-deps-resolver/src/resolve_importer.rspacquet/crates/resolving-deps-resolver/src/hoist_peers/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rspacquet/crates/lockfile-preferred-versions/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.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 withtempfile::TempDir, the mocked registry, or an integration test that spawns the actual binary.
Use the DI seam — a capability trait on theHostprovider, threaded asSys: <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 scalarassert_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. Useallow_known_failure!at the not-yet-implemented boundary and update checkboxes as items land.
Files:
pacquet/crates/cli/tests/install.rs
🧠 Learnings (1)
📚 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_package_from_registry/tests.rspacquet/crates/lockfile-preferred-versions/src/version_selector_type.rspacquet/crates/config/src/env_overlay.rspacquet/crates/config/src/lib.rspacquet/crates/cli/tests/install.rspacquet/crates/config/src/workspace_yaml.rspacquet/crates/lockfile-preferred-versions/src/lib.rspacquet/crates/resolving-deps-resolver/src/hoist_peers.rspacquet/crates/resolving-deps-resolver/src/lib.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/package-manager/src/install_without_lockfile.rspacquet/crates/resolving-deps-resolver/src/resolve_importer.rspacquet/crates/resolving-deps-resolver/src/hoist_peers/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rspacquet/crates/lockfile-preferred-versions/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
🔇 Additional comments (22)
pacquet/crates/config/src/lib.rs (1)
412-417: LGTM!pacquet/crates/config/src/workspace_yaml.rs (1)
125-125: LGTM!Also applies to: 345-345, 415-416
pacquet/crates/config/src/env_overlay.rs (1)
140-140: LGTM!pacquet/crates/package-manager/src/install_package_from_registry/tests.rs (1)
43-43: LGTM!Cargo.toml (1)
27-27: LGTM!pacquet/crates/package-manager/Cargo.toml (1)
14-44: LGTM!pacquet/crates/resolving-deps-resolver/src/lib.rs (1)
4-6: LGTM!Also applies to: 32-40, 47-50, 53-53, 56-56, 64-66, 70-76
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs (6)
1-42: LGTM!
44-96: LGTM!
98-215: LGTM!
217-266: LGTM!
268-293: LGTM!
295-335: LGTM!pacquet/crates/resolving-deps-resolver/src/resolve_importer/tests.rs (3)
1-90: LGTM!
92-260: LGTM!
262-768: LGTM!pacquet/crates/package-manager/src/install_without_lockfile.rs (5)
16-19: LGTM!
50-62: LGTM!Also applies to: 99-105
237-270: LGTM!
305-332: LGTM!
434-443: LGTM!pacquet/crates/cli/tests/install.rs (1)
313-366: LGTM!
Upstream's `hoistPeers` and `getHoistableOptionalPeers` both pass
`{ includePrerelease: true }` to `semver.maxSatisfying`, so a
prerelease candidate from the preferred-versions table (e.g.
`18.0.0-rc.1`) satisfies a regular range (e.g. `^18.0.0`). Rust's
`node_semver::Range::satisfies` follows strict semver semantics and
rejects prereleases when the range has none of its own, which
silently dropped valid picks in the hoist loop.
Mirror the strip-and-retry pattern already used by
`satisfies_with_prereleases` in `resolve_peers.rs`: a new
`satisfies_including_prerelease` helper in `hoist_peers.rs` retries
with the prerelease tag stripped, and `max_satisfying` /
`get_hoistable_optional_peers` now both go through it.
Add two regression tests covering an `^18.0.0` range against an
`18.0.0-rc.1` preferred-versions entry for each function.
Reported by CodeRabbit on PR #11784.
---
Written by an agent (Claude Code, claude-opus-4-7).
The doc comment I added for `satisfies_including_prerelease` linked to `crate::resolve_peers`, which rustdoc flags as ambiguous (both a module and a re-exported function). Replace the link with prose since the helper this paragraph compares to is module-internal anyway — no link can resolve to it from outside the module. --- Written by an agent (Claude Code, claude-opus-4-7).
Summary
hoistPeersalgorithm — missing peers are hoisted to the importer's direct deps, shared across consumers and deduplicated against the preferred-versions map.resolveRootDependenciesshape): inner loop hoists missing required peers and re-resolves until none remain; outer loop hoists optional peers whose version is already in scope, then re-enters the inner loop.auto-install-peers-from-highest-matchsetting mirroring upstream's flag for range-merge behavior across conflicting consumers.What changed
pacquet-lockfile-preferred-versions— port of@pnpm/lockfile.preferred-versions. Seeds the version-picker tie-break table from manifest + lockfile snapshots, with weight-bump on dual-source matches. Includes aget_version_selector_typehelper that mirrors theversion-selector-typenpm package (semver version → range → URI-safe tag →None).hoist_peersmodule inpacquet-resolving-deps-resolver— portshoistPeers+getHoistableOptionalPeersfrom upstream'shoistPeers.tswith all 10 upstream test cases (PR fix: handle non-string version selectors in hoistPeers #11048, fix: invalid specifiers for peers on all non-exact version selectors #11049 regressions, workspace: protocol, exact override pinning, weighted selectors, etc.).resolve_importerorchestrator inpacquet-resolving-deps-resolver— public entry point that runs the tree pass, then drives the hoist loop, then runs the final peer pass. Returns{ resolved_tree, peers_result }for the install layer. Mirrors upstream'sresolveRootDependencies.resolve_dependency_treerefactor: removed the placeholder peer-folding inextract_children; split the entry point into reusable building blocks (TreeCtx::new,extend_tree,into_resolved_tree,snapshot,resolved_versions) so the orchestrator can extend the tree incrementally without re-walking already-resolved subtrees.auto_install_peers_from_highest_match: bool(defaultfalse, matching pnpm) topacquet-configwith yaml deserializer, env-overlay keyAUTO_INSTALL_PEERS_FROM_HIGHEST_MATCH, andclear_workspace_only_fieldsentry.install_without_lockfilenow callsresolve_importerinstead ofresolve_dependency_tree+resolve_peersdirectly, seedingall_preferred_versionsfrom the importer manifest viaget_preferred_versions_from_lockfile_and_manifests(None, &[manifest]).Architecture notes
resolve_peersover a snapshot of the in-flight tree each iteration to find missing peers, then callshoist_peers(orget_hoistable_optional_peersfor the outer pass) to pick specifiers, then callsextend_treeto walk those new deps. The existing per-id dedup gate inresolve_nodemeans re-entering walked packages costs only a cache lookup — equivalent to upstream's inlinepkgAddresses.push(...)extension.all_preferred_versionsis mutated as each new package resolves, mirroring upstream's per-resolveallPreferredVersions[name][version] = 'version'assignment atresolveDependencies.ts:1440.get_hoistable_optional_peersfaithfully ports upstream'sspecType === 'version'filter — which only matches plainVersionselectors, not weighted ones from lockfile/manifest seeding. This means it picks up versions added to the preferred-versions map during the walk (which we now do, per the bullet above), matching upstream behavior including its known limitation.merge_rangesis a simplification of upstream'smergePkgsDeps+safeIntersect: single-range and all-identical-range cases pass through; non-identical multi-range cases drop on intersection (matching upstream'sintersection === nullarm) or||-join whenauto_install_peers_from_highest_matchis set. Full semver-range intersection (via a port ofsemver-range-intersect) is a follow-up.resolve_peers_from_workspace_rootflag is plumbed and uses the importer's direct deps asworkspace_root_depswhen set (single-importer ⇒ importer is root).Test plan
cargo nextest run -p pacquet-lockfile-preferred-versions— 9 testscargo nextest run -p pacquet-resolving-deps-resolver hoist_peers— 10 ported upstream testscargo nextest run -p pacquet-resolving-deps-resolver resolve_importer— 4 orchestrator tests (auto-installs missing required peer, doesn't hoist when disabled, transitive peer hoisting, preferred-version reuse)cargo nextest run -p pacquet-resolving-deps-resolver -p pacquet-lockfile-preferred-versions -p pacquet-config— 238 tests passcargo nextest run -p pacquet-cli auto_install_peers_hoists— E2E install against mock registry, assertspeer-a/peer-b/peer-cland innode_modules/.pnpm/andnode_modules/@pnpm.e2e/symlinkscargo clippy --workspace --all-targets -- --deny warningscleancargo fmt --check+taplo format --checkcleantyposclean on new filesOut of scope (follow-ups)
semver-range-intersectport formerge_rangesnon-identical-range case.resolve_peers_from_workspace_rootfor cross-importer peer satisfaction.all_preferred_versionsfrom the wanted lockfile ininstall_without_lockfile's sibling install paths (frozen-lockfile doesn't re-resolve, so no change needed there).Written by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit