fix(pacquet): share a subtree's peer context across importers + record array-form engines#12349
Conversation
A package.json with `"engines": ["node >= 0.2.0"]` (e.g.
jsonparse@1.3.1) records `engines: {'0': node >= 0.2.0}` in pnpm's
lockfile — the shape Object.entries() yields for an array. pacquet
read engines via as_object only and dropped the field.
Part of #12266.
…s importers A package first resolved under one importer keeps that walk's missing-peer report in pnpm (missingPeersOfChildrenByPkgId, https://github.com/pnpm/pnpm/blob/a751c7f27d/installing/deps-resolver/src/resolveDependencies.ts#L193): an importer revisiting the package neither recomputes its subtree's missing peers nor re-hoists optional peers the first context already satisfied — its occurrence then shares the first context's peer-suffixed variant. pacquet re-walked every importer's tree from scratch, so an importer without e.g. @types/node hoisted the highest satisfying version (25.x) for a subtree the root importer had already resolved against its own @types/node (22.x), forking the snapshot into duplicate suffix variants (verified against pnpm 11.6.0 with a three-importer repro on @pnpm/meta-updater). The walk now records, per package, the importer whose tree first resolved it and the missing-peer names of that first peer walk. The per-importer hoist input drops a missing peer declared under a foreign-claimed ancestor when the claiming walk did not report it missing — misses the first context could not satisfy either (e.g. clipanion's typanion under a root that only hoists it later) stay visible to every importer, which then hoists its own copy exactly like pnpm. The final workspace-wide pass stays unscoped so peer warnings still cover every importer. Whole-monorepo lockfile diff vs fresh pnpm 11.6.0: 445 → 128 changed lines; the remaining divergence is two importer entries (jest's @types/node under __utils__/eslint-config and @CycloneDX's spdx-expression-parse) plus per-level preferred-version picks (varint, es-abstract), tracked in the issue. Part of #12266.
|
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 (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📜 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)
🧰 Additional context used📓 Path-based instructions (1)pacquet/**/*.rs📄 CodeRabbit inference engine (pacquet/AGENTS.md)
Files:
🧠 Learnings (6)📚 Learning: 2026-05-20T19:40:55.051ZApplied to files:
📚 Learning: 2026-05-22T00:08:44.646ZApplied to files:
📚 Learning: 2026-05-20T23:07:58.444ZApplied to files:
📚 Learning: 2026-05-24T21:10:50.292ZApplied to files:
📚 Learning: 2026-05-24T21:10:50.292ZApplied to files:
📚 Learning: 2026-06-06T18:58:37.156ZApplied to files:
🔇 Additional comments (1)
📝 WalkthroughWalkthroughTracks which importer first resolved each package and records first-walk missing-peer names; introduces HoistMissingScope and threads it into peer-resolution hoist passes so missing-peer diagnostics can be suppressed per-importer in shared subtrees. ChangesHoist-scoped peer suppression with first-importer tracking
Sequence DiagramsequenceDiagram
participant Importer
participant resolve_importer
participant resolve_peers
participant WorkspaceTreeCtx
Importer->>resolve_importer: start importer walk (with importer_id)
resolve_importer->>resolve_peers: call resolve_peers (hoist_peers_opts)
resolve_peers->>WorkspaceTreeCtx: read first_importer_by_pkg / first_walk_missing_by_pkg
resolve_peers->>WorkspaceTreeCtx: return missing_names_by_pkg
resolve_importer->>WorkspaceTreeCtx: record_first_walk_missing(...)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
🚥 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 |
Micro-Benchmark ResultsLinux |
Code Review by Qodo
1. Missing-peers union mis-scopes
|
PR Summary by QodoFix pacquet parity: stable peer context for shared subtrees + lockfile array engines WalkthroughsDescription• Preserve first-importer peer context for shared package subtrees to match pnpm hoisting. • Scope hoist-loop missing-peer issues using first-walk missing-peer snapshots (warnings unchanged). • Record array-form engines as index-keyed entries in generated lockfile metadata. Diagramgraph TD
ws["resolve_workspace"] --> imp["resolve_importer"] --> tree["resolve_dependency_tree"] --> ctx[("WorkspaceTreeCtx")]
ctx --> peers["resolve_peers (scoped hoist)"] --> peersws["resolve_peers_workspace"] --> lock["lockfile metadata"]
High-Level AssessmentThe following are alternative approaches to this PR: 1. Recompute subtree missing peers per importer occurrence
2. Key missing-peer reports by (pkg_id, importer_id) instead of first-owner
3. Change importer processing order to approximate pnpm concurrency
Recommendation: Current approach is best for parity: explicitly capturing first-importer ownership and first-walk missing-peer names matches pnpm’s shared-subtree caching behavior while keeping the final workspace-wide peer pass unscoped so diagnostics remain complete. File ChangesEnhancement (1)
Bug fix (5)
Tests (2)
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #12349 +/- ##
==========================================
+ Coverage 87.97% 88.01% +0.03%
==========================================
Files 292 292
Lines 36752 36831 +79
==========================================
+ Hits 32332 32415 +83
+ Misses 4420 4416 -4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/resolve_peers.rs`:
- Around line 613-620: The current construction of missing_names_by_pkg uses
self.node_missing_peers directly and therefore includes names that were
suppressed by missing_issue_suppressed(); change this so the exported map only
contains emitted (non-suppressed) misses: iterate the same nodes in
self.node_missing_peers but, for each candidate name, call
missing_issue_suppressed(...) (or use the existing post-suppression collection
if one exists) and only extend missing_names_by_pkg with names that are NOT
suppressed; keep the original raw map intact for cache compatibility if needed,
and ensure record_first_walk_missing() uses this filtered missing_names_by_pkg
so only actually emitted misses are recorded.
🪄 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: 0afd02d3-d0ee-429a-b0d8-f3e046d02bba
📒 Files selected for processing (8)
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.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_peers.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace/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). (3)
- GitHub Check: windows-latest / Node.js 22.13.0 / Test
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (windows-latest)
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs
📄 CodeRabbit inference engine (pacquet/AGENTS.md)
pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that firespnpm:<channel>events throughglobalLogger,logger.debug(...), orstreamParser.write(...), mirror the call site, payload, and ordering so@pnpm/cli.default-reporterparses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plainStringor&strin Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too viaTryFrom<String>and/orFromStrand do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallibleFrom<String>constructor
If upstream TypeScript occasionally constructs a branded string without validation, exposefrom_str_uncheckedin Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using#[serde(try_from = "String")]for deserialization and#[serde(into = "String")]for serialization
Derive simple conversions for branded strings using#[derive(derive_more::From)]and#[derive(derive_more::Into)]instead of handwritingimplblocks; use manualimplonly when conversion needs custom logic
Model TypeScript string literal unions (like'auto' | 'always' | 'never') as Rustenums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like`${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide inCODE_STYLE_GUIDE.md— imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching,pipe-trait, error handling, test layout, and cloning ofArcandRc
Choose owned vs. borrowed parameters to minimize copies; widen to t...
Files:
pacquet/crates/resolving-deps-resolver/src/lib.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rspacquet/crates/resolving-deps-resolver/src/resolve_importer.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_peers.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
🧠 Learnings (7)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/lib.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rspacquet/crates/resolving-deps-resolver/src/resolve_importer.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_peers.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/lib.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rspacquet/crates/resolving-deps-resolver/src/resolve_importer.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_peers.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/lib.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rspacquet/crates/resolving-deps-resolver/src/resolve_importer.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_peers.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-05-24T21:10:50.292Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:10:50.292Z
Learning: When implementing dependency “revisit”/optional-folding in pacquet, mirror pnpm’s `resolveDependencies.ts` behavior: on revisit, update the `optional` flag only for the directly visited package, and do not automatically change `optional` for transitive descendants. pnpm corrects stale optional flags later via `copyDependencySubGraph` (a BFS in `lockfile/pruner/src/index.ts`/`copyDependencySubGraph`). Until pacquet has an equivalent pruner/copy-subgraph step, be aware that the raw `node.optional` can flow through to the lockfile/virtual store via `dependencies_graph_to_lockfile.rs` → `create_virtual_store.rs` → `installability.rs`, so review changes that affect optional flag propagation for this mismatch.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/lib.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_importer.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_peers.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-05-24T21:10:50.292Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:10:50.292Z
Learning: In pacquet’s Rust resolver layer, the behavior of `ResolvedPackage.optional` “AND-folding”/revisit updates is intentionally aligned with pnpm: when a package is directly revisited, only that package’s `optional` flag is updated; transitive descendants are not re-walked/rescored in this stage. pnpm later corrects any stale optional flags downstream via its pruner/BFS logic (`copyDependencySubGraph` + `nonOptional` reachability stamping in the lockfile pruner). Since pacquet does not yet have that pruner equivalent, review should NOT flag the downstream/stale optional propagation as a bug in pacquet PRs; treat the resolver-layer optional propagation gap as expected parity until the pruning/stamping step is ported.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/lib.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_importer.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_peers.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.
Applied to files:
pacquet/crates/resolving-deps-resolver/src/lib.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace.rspacquet/crates/resolving-deps-resolver/src/tests.rspacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rspacquet/crates/resolving-deps-resolver/src/resolve_importer.rspacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rspacquet/crates/resolving-deps-resolver/src/resolve_peers.rspacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-06-12T07:04:03.703Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12227
File: pacquet/crates/package-manager/src/install.rs:708-719
Timestamp: 2026-06-12T07:04:03.703Z
Learning: In this pnpm-based repo’s package-manager Rust code, `ThrottledClient` uses a two-class semaphore scheduling policy (“reserved half” for tarball/download work and a higher-priority “latency class” for lockfile-verification metadata fetches). Therefore, when reviewing, do NOT flag reuse of the same `Arc<ThrottledClient>` for both downloads and verification as a budget/contention issue—this reuse is intentional and handled by the internal two-class “reserved permits” design. Creating a separate client instance would break the EMFILE guard behavior that relies on shared throttling.
Applied to files:
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
🔇 Additional comments (4)
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs (1)
523-550: LGTM!pacquet/crates/resolving-deps-resolver/src/resolve_workspace/tests.rs (2)
294-397: LGTM!
399-487: LGTM!pacquet/crates/resolving-deps-resolver/src/tests.rs (1)
2058-2058: LGTM!
Integrated-Benchmark Report (Linux)Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 3.8542937427600004,
"stddev": 0.1663006823820761,
"median": 3.83097209646,
"user": 3.6700447600000006,
"system": 3.3939011999999997,
"min": 3.6034235969600004,
"max": 4.06960697696,
"times": [
4.05562806196,
3.75916640496,
3.6034235969600004,
4.00087604696,
4.06960697696,
3.72044742896,
3.85707251996,
3.80487167296,
3.68181780096,
3.9900269169600002
]
},
{
"command": "pacquet@main",
"mean": 3.8665537872600004,
"stddev": 0.1599513087798402,
"median": 3.8303776449600004,
"user": 3.6318252599999994,
"system": 3.353546299999999,
"min": 3.6456865149600004,
"max": 4.08526466296,
"times": [
4.08526466296,
4.0422339779600005,
3.9610131059600002,
4.04923222996,
3.7464899699600003,
3.8995330639600003,
3.6456865149600004,
3.76122222596,
3.72096233396,
3.7538997869600004
]
},
{
"command": "pnpr@HEAD",
"mean": 2.15465338716,
"stddev": 0.11139932981774285,
"median": 2.15546691246,
"user": 2.6744039599999994,
"system": 2.9692128999999996,
"min": 1.9550332169600002,
"max": 2.31747437896,
"times": [
1.9550332169600002,
2.0896727839600002,
2.2281290189600003,
2.0766136909600004,
2.17252124196,
2.3108007169600002,
2.14794269296,
2.16299113196,
2.31747437896,
2.08535499796
]
},
{
"command": "pnpr@main",
"mean": 2.14489457266,
"stddev": 0.09794585706557442,
"median": 2.1392585259600003,
"user": 2.61497376,
"system": 2.9643091,
"min": 2.00063144896,
"max": 2.3212766419600004,
"times": [
2.3212766419600004,
2.1431194979600003,
2.00063144896,
2.27225353696,
2.1630682789600004,
2.08437240696,
2.1901501909600003,
2.0513723929600003,
2.1353975539600003,
2.0873037769600002
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.68582980652,
"stddev": 0.014964070413959621,
"median": 0.68604673222,
"user": 0.40932401999999996,
"system": 1.3349343599999999,
"min": 0.65716534672,
"max": 0.7053291137200001,
"times": [
0.68811548572,
0.6754998267200001,
0.68397797872,
0.69847901372,
0.6817756177200001,
0.7053291137200001,
0.67159353972,
0.70081324272,
0.69554889972,
0.65716534672
]
},
{
"command": "pacquet@main",
"mean": 0.7108750403200002,
"stddev": 0.1192041250188281,
"median": 0.6771219152200001,
"user": 0.41367291999999994,
"system": 1.3241659600000002,
"min": 0.63471989572,
"max": 1.04703364872,
"times": [
0.69149005372,
0.66946065872,
0.67726038672,
0.6744386197200001,
0.69262376272,
0.6790362067200001,
0.6769834437200001,
0.66570372672,
0.63471989572,
1.04703364872
]
},
{
"command": "pnpr@HEAD",
"mean": 0.73782556852,
"stddev": 0.016328708438087456,
"median": 0.7378256347200001,
"user": 0.41770791999999995,
"system": 1.3645822599999997,
"min": 0.71378478572,
"max": 0.76671671072,
"times": [
0.7531320087200001,
0.73039171972,
0.74452486472,
0.7426925617200001,
0.73295870772,
0.71378478572,
0.7213753367200001,
0.72349129572,
0.76671671072,
0.74918769372
]
},
{
"command": "pnpr@main",
"mean": 0.72509847912,
"stddev": 0.02477451131829827,
"median": 0.7242726422200001,
"user": 0.42159932,
"system": 1.3524536600000001,
"min": 0.6975949827200001,
"max": 0.77297859672,
"times": [
0.7400168007200001,
0.72461528172,
0.7093076367200001,
0.6992609457200001,
0.72746160072,
0.7239300027200001,
0.7535275127200001,
0.77297859672,
0.70229143072,
0.6975949827200001
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 3.9673235434199996,
"stddev": 0.05354998358504826,
"median": 3.9643127378200003,
"user": 3.6867505,
"system": 3.3372533399999993,
"min": 3.8927737218200003,
"max": 4.05946073082,
"times": [
3.9852059838200002,
3.91231102482,
4.05946073082,
4.02443331582,
3.8927737218200003,
3.95191841782,
3.92786246782,
4.01153002882,
3.97670705782,
3.93103268482
]
},
{
"command": "pacquet@main",
"mean": 3.95416821702,
"stddev": 0.055997129835163494,
"median": 3.94423642982,
"user": 3.6406913999999992,
"system": 3.33638244,
"min": 3.8773227388200002,
"max": 4.05523308982,
"times": [
4.05523308982,
3.94271391382,
3.91039171182,
3.89287000082,
3.98329323982,
3.8773227388200002,
3.93121577982,
4.01525108282,
3.9457589458199998,
3.98763166682
]
},
{
"command": "pnpr@HEAD",
"mean": 2.2139786959200007,
"stddev": 0.1341124672700319,
"median": 2.20826265232,
"user": 2.4973158999999994,
"system": 2.85523834,
"min": 2.03069827582,
"max": 2.41790598182,
"times": [
2.10841755882,
2.2815025908199997,
2.41790598182,
2.14778418682,
2.13695321482,
2.27559641782,
2.07344964982,
2.26874111782,
2.39873796482,
2.03069827582
]
},
{
"command": "pnpr@main",
"mean": 2.14987852512,
"stddev": 0.11572345988356393,
"median": 2.11821186232,
"user": 2.4897672,
"system": 2.87266304,
"min": 2.0536368718199998,
"max": 2.35887606082,
"times": [
2.05717861382,
2.11849905082,
2.11792467382,
2.06924907182,
2.06908161682,
2.35887606082,
2.1314284898199998,
2.0536368718199998,
2.35819577982,
2.16471502182
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.2132541139800002,
"stddev": 0.008648309140393332,
"median": 1.21331492058,
"user": 1.24878058,
"system": 1.6955334199999996,
"min": 1.19761574408,
"max": 1.2305783350800001,
"times": [
1.21914952008,
1.21521877508,
1.21467844908,
1.2305783350800001,
1.19761574408,
1.20612703508,
1.21195139208,
1.21679610608,
1.20875147808,
1.21167430508
]
},
{
"command": "pacquet@main",
"mean": 1.2166991921799999,
"stddev": 0.0256538288944032,
"median": 1.2122228540800002,
"user": 1.22444948,
"system": 1.71334542,
"min": 1.18519198508,
"max": 1.25828613508,
"times": [
1.2194337260800001,
1.19037497108,
1.25828613508,
1.19408377508,
1.18519198508,
1.25803358608,
1.22870432808,
1.20843770708,
1.21596520508,
1.20848050308
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6841405261800002,
"stddev": 0.033758684945402705,
"median": 0.67505321808,
"user": 0.36189208000000006,
"system": 1.2812436200000001,
"min": 0.66302294608,
"max": 0.77544508708,
"times": [
0.68079969608,
0.66302294608,
0.6639444550800001,
0.66305199808,
0.6662648840800001,
0.68093372908,
0.77544508708,
0.6874306920800001,
0.69120503408,
0.66930674008
]
},
{
"command": "pnpr@main",
"mean": 0.70299622618,
"stddev": 0.06675711282810422,
"median": 0.68090260258,
"user": 0.37289748000000006,
"system": 1.2839881199999998,
"min": 0.6684887500800001,
"max": 0.89020248708,
"times": [
0.67976111208,
0.6733283630800001,
0.6701680720800001,
0.6684887500800001,
0.6779858440800001,
0.70639539608,
0.6909785740800001,
0.69060957008,
0.68204409308,
0.89020248708
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.7755422317600003,
"stddev": 0.04430421004820169,
"median": 2.7766755197600004,
"user": 1.7283163399999997,
"system": 2.0281468999999994,
"min": 2.71138052676,
"max": 2.85820950576,
"times": [
2.74207007176,
2.71138052676,
2.7420210877600004,
2.77379272676,
2.7885811027600003,
2.7795583127600003,
2.82597958876,
2.7951025897600004,
2.73872680476,
2.85820950576
]
},
{
"command": "pacquet@main",
"mean": 2.73712328766,
"stddev": 0.048766958486508576,
"median": 2.7295739397600003,
"user": 1.7311760399999998,
"system": 1.9540101,
"min": 2.67942917076,
"max": 2.86175891476,
"times": [
2.7021937627600003,
2.7215449097600004,
2.7265911427600003,
2.71361375676,
2.74024823976,
2.67942917076,
2.73657711876,
2.7325567367600003,
2.86175891476,
2.75671912376
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6825864001599999,
"stddev": 0.013849486006840763,
"median": 0.67894553326,
"user": 0.35816114000000004,
"system": 1.2866195,
"min": 0.6677972947599999,
"max": 0.70989733076,
"times": [
0.67159060176,
0.70096010376,
0.70989733076,
0.67421082676,
0.6677972947599999,
0.68286789476,
0.69021946876,
0.67851432476,
0.67937674176,
0.67042941376
]
},
{
"command": "pnpr@main",
"mean": 0.7064149409599999,
"stddev": 0.10273173215320594,
"median": 0.67387360326,
"user": 0.36548094000000003,
"system": 1.2794259,
"min": 0.66373892876,
"max": 0.99794614276,
"times": [
0.68379069576,
0.66373892876,
0.66867326476,
0.66385274676,
0.67285546076,
0.67489174576,
0.68255192776,
0.67044322476,
0.99794614276,
0.68540527176
]
}
]
} |
|
| Branch | pr/12349 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 3,967.32 ms(-46.55%)Baseline: 7,422.00 ms | 8,906.40 ms (44.54%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 2,775.54 ms(-33.90%)Baseline: 4,199.17 ms | 5,039.01 ms (55.08%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,213.25 ms(-7.18%)Baseline: 1,307.04 ms | 1,568.45 ms (77.35%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 3,854.29 ms(-52.96%)Baseline: 8,193.23 ms | 9,831.87 ms (39.20%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 685.83 ms(+2.75%)Baseline: 667.46 ms | 800.95 ms (85.63%) |
|
| Branch | pr/12349 |
| Testbed | pnpr |
⚠️ WARNING: No Threshold found!Without a Threshold, no Alerts will ever be generated.
Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the--ci-only-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 2,213.98 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 682.59 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 684.14 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 2,154.65 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 737.83 ms |
The hoist loop cloned the claim and first-walk-missing maps on every iteration; suppression only consults entries recorded by earlier importers (an importer's own loop additions are self-claimed and exempt), so one Arc-shared snapshot per importer is equivalent. Addresses a review comment on the PR.
|
Code review by qodo was updated up to the latest commit 6645acd |
|
Addressed the two findings from the Qodo review:
Written by an agent (Claude Code, claude-fable-5). |
Summary
Two more lockfile-parity fixes for pacquet, continuing #12266. Measured on the monorepo (fresh state,
install --lockfile-only, back-to-back vs pnpm 11.6.0), the real-lockfile document diff drops from 445 to 128 changed lines, and the divergent importer entries drop from ~30 to 2.1. Shared subtrees keep the first importer's peer context (
fix(deps-resolver))The dominant remaining divergence was which
@types/nodea hoisted optional peer binds to. Root cause (established with a three-importer repro on@pnpm/meta-updater, behavior verified against pnpm 11.6.0 in four configurations): pnpm computes a package subtree's missing-peer report once, under the importer whose walk first resolves it (missingPeersOfChildrenByPkgId). An importer revisiting the package reuses that report, so:@types/node@22.x) never reaches a later importer's optional-peer hoist — the later importer's occurrence ends up sharing the first context's suffixed variant; whileclipanion'stypanionunder a root that only hoists it later) stays visible to every importer, and each hoists its own copy.pacquet re-walked each importer's tree from scratch, so an importer without
@types/nodehoisted the max-satisfying25.xfor a subtree the root had already resolved against22.x, forking snapshots into duplicate suffix variants (two@pnpm/cli-utils@…entries, etc.).The walk now records per package the claiming importer and the missing-peer names of that first peer walk; the per-importer hoist input drops a miss declared under a foreign-claimed ancestor only when the claiming walk did not report it missing. The final workspace-wide peer pass stays unscoped, so per-project missing-peer warnings are unchanged. Two regression tests cover both directions (suppressed satisfied-miss; visible unsatisfied-miss), each verified to fail without the fix.
2. Array-form
engines(fix(lockfile))jsonparse@1.3.1declares"engines": ["node >= 0.2.0"]; pnpm recordsengines: {'0': node >= 0.2.0}(theObject.entriesshape for an array). pacquet read the field viaas_objectonly and dropped it.Verification
pacquet-resolving-deps-resolver(111 total) + fullpacquet-package-managersuite (462); clippy--deny warnings, rustfmt, typos clean.@pnpm/meta-updatershared across importers;eslint+cosmiconfig-typescript-loader;debug+concurrently) all match pnpm byte-for-byte.Remaining gaps (tracked in #12266)
jest's@types/nodeunder__utils__/eslint-configand@cyclonedx'sspdx-expression-parseunderdeps/compliance/sbom: pacquet's sequential, lexically-ordered importer processing makes the peer-less importer the first claimer of the shared subtree, where pnpm's concurrent importer resolution lets a peer-satisfying importer win the race. Needs a look at pnpm's project ordering.resolveDependencies.ts#L717-L733): pnpm biases child resolutions toward versions already resolved at the parent level (varint~5.0.0→ sibling-pinned5.0.0;es-abstract; thespdx-expression-parse3-vs-4 pick). pacquet's picker seed is static.packageManagerDependenciesblock andlibc:fields on platform packages.Written by an agent (Claude Code, claude-fable-5).
Summary by CodeRabbit
Refactor
Tests
Style