Skip to content

fix(pacquet): apply overrides during fresh resolution#12283

Merged
zkochan merged 1 commit into
mainfrom
fix/12275
Jun 9, 2026
Merged

fix(pacquet): apply overrides during fresh resolution#12283
zkochan merged 1 commit into
mainfrom
fix/12275

Conversation

@zkochan

@zkochan zkochan commented Jun 9, 2026

Copy link
Copy Markdown
Member

PR Summary by Qodo

Fix fresh lockfile resolution to apply pnpm.overrides (including catalog:)
🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Apply pnpm.overrides via a resolver manifest-hook during fresh resolution.
• Invalidate lockfile reuse when recorded overrides differ from current config.
• Add lockfile-only tests covering direct, transitive, and catalog: override resolution.
Diagram
graph TD
  C["Config (overrides)"] --> I["InstallWithFreshLockfile"] --> H["Manifest hook chain"] --> R["Deps resolver"] --> G["Dependency graph"] --> B["Build fresh lockfile"] --> L[("pnpm-lock.yaml")]
  K["Catalogs"] --> I
Loading
High-Level Assessment

The approach mirrors pnpm’s built-in read-package hook order (packageExtensions → overrides) and applies overrides at the correct layer (manifest read during resolution), ensuring both chosen versions and the resulting lockfile match pnpm semantics. Resolved overrides are persisted (including catalog: expansion) and lockfile reuse is safely invalidated on overrides drift, which is the right correctness tradeoff.

Grey Divider

File Changes

Enhancement (1)
overrides.rs Make VersionsOverrider Arc-friendly and support hook-style application +104/-54

Make VersionsOverrider Arc-friendly and support hook-style application

• Refactors 'VersionsOverrider' to own override entries (no lifetime), adds 'is_empty', and introduces 'apply_to_arc'/'apply_to_value' so the resolver can cheaply apply overrides to shared manifest values with clone-on-write behavior. Tidies internal selection logic via 'choose_override' and extracts parent-scope matching into a helper.

pacquet/crates/package-manager/src/overrides.rs


Bug fix (1)
install_with_fresh_lockfile.rs Compose overrides into fresh resolver manifest hook and lockfile reuse checks +174/-60

Compose overrides into fresh resolver manifest hook and lockfile reuse checks

• Parses and resolves 'config.overrides' (including catalogs), builds a composed manifest-hook chain (packageExtensions then overrides), and applies it during fresh resolution. Also gates reuse of an existing wanted lockfile on overrides equality and writes the resolved overrides map into newly built lockfiles via refactored build options.

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs


Tests (1)
tests.rs Add fresh lockfile-only override resolution tests +134/-0

Add fresh lockfile-only override resolution tests

• Adds async tests verifying that fresh lockfile generation applies overrides to direct and transitive dependencies. Includes a 'catalog:' override case and asserts the lockfile records the resolved override value, plus shared helpers for creating a temp project/config and checking lockfile contents.

pacquet/crates/package-manager/src/install/tests.rs


Grey Divider

Qodo Logo

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

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: d47a0eef-0d6f-4d11-a1bb-cd01ffecb87d

📥 Commits

Reviewing files that changed from the base of the PR and between 8c9edf2 and b21b98d.

📒 Files selected for processing (3)
  • pacquet/crates/package-manager/src/install/tests.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/package-manager/src/overrides.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: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plain String or &str in Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too via TryFrom<String> and/or FromStr and 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 infallible From<String> constructor
If upstream TypeScript occasionally constructs a branded string without validation, expose from_str_unchecked in 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 handwriting impl blocks; use manual impl only when conversion needs custom logic
Model TypeScript string literal unions (like 'auto' | 'always' | 'never') as Rust enums 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 in CODE_STYLE_GUIDE.md — imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Choose owned vs. borrowed parameters to minimize copies; widen to t...

Files:

  • pacquet/crates/package-manager/src/install/tests.rs
  • pacquet/crates/package-manager/src/overrides.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
🧠 Learnings (4)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/package-manager/src/install/tests.rs
  • pacquet/crates/package-manager/src/overrides.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/package-manager/src/install/tests.rs
  • pacquet/crates/package-manager/src/overrides.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/package-manager/src/install/tests.rs
  • pacquet/crates/package-manager/src/overrides.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.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/package-manager/src/install/tests.rs
  • pacquet/crates/package-manager/src/overrides.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
🔇 Additional comments (17)
pacquet/crates/package-manager/src/overrides.rs (6)

1-56: LGTM!


80-103: LGTM!


114-144: LGTM!


146-187: LGTM!


189-264: LGTM!


278-300: LGTM!

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

319-322: LGTM!


779-823: LGTM!


825-840: LGTM!


998-1014: LGTM!


1092-1092: LGTM!


1160-1169: LGTM!


1293-1302: LGTM!


1804-1843: LGTM!


1845-1856: LGTM!


1866-1916: LGTM!

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

6115-6247: LGTM!


📝 Walkthrough

Walkthrough

This PR fixes a critical bug where pnpm.overrides were not applied during fresh-lockfile resolution in pacquet. The changes refactor VersionsOverrider to use owned storage, parse and apply overrides transitively through the resolver via composed manifest hooks, tighten lockfile reuse validation, and add comprehensive test coverage.

Changes

Override Application During Fresh-Lockfile Resolution

Layer / File(s) Summary
VersionsOverrider lifetime refactoring
pacquet/crates/package-manager/src/overrides.rs
Remove lifetime parameters from VersionsOverrider and ResolvedOverride to enable owned override storage; refactor override selection to use choose_override helper; add is_empty method for fast-path checking.
Override parsing and hook composition
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
Parse config.overrides upfront into a resolved map; introduce InvalidOverrides error variant; compose manifest hooks to apply packageExtensions then overrides; build "effective" mutated manifest map for resolution visibility.
Lockfile reuse validation with override drift detection
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
Update wanted_lockfile reuse gate to check both package_extensions_checksum and overrides_match(...); wire composed manifest_hook into resolver workspace options and importer resolution paths.
Fresh-lockfile build options and parameter wiring
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
Extend FreshLockfileBuildOptions with resolved_overrides field; thread resolved overrides through both --lockfile-only and normal build_fresh_lockfile paths; update GraphToLockfileOptions to use precomputed resolved map.
Fresh-lockfile override test suite
pacquet/crates/package-manager/src/install/tests.rs
Add tests for direct dependency overrides, transitive dependency overrides with presence/absence checks, and catalog:-valued override resolution; introduce fresh_lockfile_only_with_overrides(...) harness and assert_package_present/absent(...) helpers.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pnpm#11793: Introduces the VersionsOverrider and override-resolution foundation that this PR builds upon and refactors for owned storage and composition.
  • pnpm/pnpm#11820: Ensures catalog: override values are resolved before validation, aligning with the catalog-expansion logic added in this PR's fresh-lockfile options.

Poem

🐰 Hops through the resolver with springs of cheer,
Overrides now applied, both far and near—
Catalogs expand, transitive paths glow,
Fresh locks now fresh from the top to the toe! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: applying pnpm.overrides during fresh-lockfile resolution, which is the core objective of this PR.
Linked Issues check ✅ Passed All core objectives from issue #12275 are met: overrides are applied during fresh resolution, catalog values are resolved before lockfile storage, and lockfile reuse is invalidated when overrides drift.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing override application during fresh resolution; no unrelated modifications detected in test infrastructure or unrelated features.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/12275

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.03      8.5±0.21ms   508.4 KB/sec    1.00      8.3±0.14ms   525.7 KB/sec

@codecov-commenter

codecov-commenter commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.32990% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.55%. Comparing base (8c9edf2) to head (b21b98d).

Files with missing lines Patch % Lines
...package-manager/src/install_with_fresh_lockfile.rs 92.62% 9 Missing ⚠️
pacquet/crates/package-manager/src/overrides.rs 97.22% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main   #12283    +/-   ##
========================================
  Coverage   87.55%   87.55%            
========================================
  Files         280      280            
  Lines       33540    33664   +124     
========================================
+ Hits        29365    29476   +111     
- Misses       4175     4188    +13     

☔ View full report in Codecov by Harness.
📢 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.

@zkochan zkochan marked this pull request as ready for review June 9, 2026 10:27
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Grey Divider


Remediation recommended

1. Override order ignored 🐞 Bug ≡ Correctness ⭐ New
Description
overrides_match compares overrides as an unordered key/value set, so reordering pnpm.overrides
can still allow wanted-lockfile subtree reuse even though override selection can depend on entry
order when multiple candidates are equally specific. This can reuse stale transitive subtrees and
yield a resolution that doesn’t reflect the newly ordered overrides.
Code

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[R1827-1843]

+fn overrides_match(
+    lockfile: Option<&IndexMap<String, String>>,
+    config: Option<&IndexMap<String, String>>,
+) -> bool {
+    let lockfile = lockfile.filter(|map| !map.is_empty());
+    let config = config.filter(|map| !map.is_empty());
+    match (lockfile, config) {
+        (None, None) => true,
+        (Some(lockfile), Some(config)) => {
+            lockfile.len() == config.len()
+                && lockfile.iter().all(|(key, value)| {
+                    config.get(key).is_some_and(|config_value| config_value == value)
+                })
+        }
+        _ => false,
+    }
+}
Evidence
The reuse gate uses overrides_match to decide whether to pass the prior lockfile for subtree
reuse, but overrides_match ignores ordering. Meanwhile, override selection sorts candidates and
then takes the first; because the comparator can return Equal for some pairs, the original
iteration order can affect which override is chosen, so reordering can change behavior and should
invalidate reuse.

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[1003-1016]
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[1827-1843]
pacquet/crates/package-manager/src/overrides.rs[226-263]
pacquet/crates/package-manager/src/overrides.rs[278-299]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`overrides_match()` currently ignores insertion order by checking only that both maps contain the same key/value pairs. However, `VersionsOverrider` can select an override based on the relative order of equally-specific matching candidates (it sorts, but can return `Ordering::Equal` and then takes the first match), so reordering overrides can change behavior.

This means the fresh-resolution reuse gate may incorrectly reuse the prior lockfile’s resolved subtrees after an override reordering, producing stale/incorrect resolutions.

### Issue Context
- `wanted_lockfile` reuse is gated on `overrides_match(...)`.
- Override selection takes the first candidate after a specificity sort; ties are possible and then input order becomes the tie-breaker.

### Fix Focus Areas
- pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[1827-1843]

### Suggested fix
Change `overrides_match` to be order-sensitive (since both config and lockfile use `IndexMap` specifically to preserve insertion order):
- After filtering empty maps, compare via `lockfile == config` (IndexMap equality is order-sensitive).

(Alternative, if you *want* ordering to be irrelevant: make override selection deterministic and order-independent by adding a strict tie-breaker, then keep the unordered comparison. But today, selection can depend on order, so the reuse gate should too.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Absolute local override paths 🐞 Bug ≡ Correctness
Description
The fresh resolver’s overrides ManifestHook calls VersionsOverrider::apply_to_arc(..., None), so
relative link:/file: override values cannot be re-relativized and are rendered as absolute
paths. This can make overridden transitive dependency specs machine-specific and inconsistent with
the overrider’s documented local-target behavior.
Code

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[R830-838]

+        let overrides_hook: Option<ManifestHook> =
+            versions_overrider.as_ref().and_then(|overrider| {
+                if overrider.is_empty() {
+                    None
+                } else {
+                    let overrider = Arc::clone(overrider);
+                    Some(Arc::new(move |manifest| overrider.apply_to_arc(manifest, None))
+                        as ManifestHook)
+                }
Evidence
The overrides hook explicitly passes None as the manifest directory, and the overrider’s local
override rendering falls back to emitting the absolute path when no directory is provided.

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[825-838]
pacquet/crates/package-manager/src/overrides.rs[105-111]
pacquet/crates/package-manager/src/overrides.rs[329-372]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Fresh resolution applies overrides via a `ManifestHook` but passes `manifest_dir=None` into `VersionsOverrider::apply_to_arc`, which forces relative `link:`/`file:` override values to serialize as absolute paths when applied transitively.
## Issue Context
`resolve_local_override_spec` only re-relativizes when `pkg_dir` is `Some(_)`; otherwise it emits the absolute path stored in `LocalTarget`.
## Fix Focus Areas
- pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[825-839]
- pacquet/crates/package-manager/src/overrides.rs[329-372]
## Suggested fix approach
Prefer a contained fix in `VersionsOverrider`:
1. When parsing local override targets, also store the original normalized relative path component (or a root-dir-relative form) in `LocalTarget` when `specified_via_relative_path == true`.
2. Update `resolve_local_override_spec` so that when `pkg_dir` is `None` and the target was specified via a relative path, it returns a stable relative specifier (e.g. the stored root-relative form) instead of the absolute path.
This avoids changing the `ManifestHook` signature while preventing machine-specific absolute paths from being introduced by the hook path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Overrides hook double-scans manifests 🐞 Bug ➹ Performance
Description
VersionsOverrider::apply_to_arc scans dependency maps to decide whether any override applies and
then scans again to apply overrides after cloning, duplicating override-selection work for matching
manifests. Since ManifestHook runs on every resolved package manifest during fresh resolution,
this extra pass can add noticeable overhead on large graphs or large override sets.
Code

pacquet/crates/package-manager/src/overrides.rs[R134-171]

+    /// Apply overrides to the resolver's shared manifest value,
+    /// cloning only when at least one configured override can rewrite
+    /// the manifest.
+    pub fn apply_to_arc(&self, manifest: Arc<Value>, manifest_dir: Option<&Path>) -> Arc<Value> {
+        if self.is_empty() || !self.has_applicable_override(&manifest) {
+            return manifest;
       }
+        let mut cloned = (*manifest).clone();
+        self.apply_to_value(&mut cloned, manifest_dir);
+        Arc::new(cloned)
+    }
+
+    fn applicable_parent_scoped<'b>(&'b self, manifest: &Value) -> Vec<&'b ResolvedOverride> {
+        let manifest_name = manifest.get("name").and_then(Value::as_str);
+        let manifest_version = manifest.get("version").and_then(Value::as_str);
+
+        self.parent_scoped
+            .iter()
+            .filter(|entry| {
+                let Some(parent) = entry.inner.parent_pkg.as_ref() else { return false };
+                let name_matches = manifest_name == Some(parent.name.as_str());
+                let range_matches = match (parent.bare_specifier.as_deref(), manifest_version) {
+                    (None, _) => true,
+                    (Some(_), None) => false,
+                    (Some(range), Some(version)) => semver_satisfies(version, range),
+                };
+                name_matches && range_matches
+            })
+            .collect()
+    }
+
+    fn has_applicable_override(&self, value: &Value) -> bool {
+        let applicable_parent_scoped = self.applicable_parent_scoped(value);
+        [DependencyGroup::Prod, DependencyGroup::Optional, DependencyGroup::Dev]
+            .iter()
+            .copied()
+            .any(|group| self.group_has_override(value, group, &applicable_parent_scoped))
+    }
Evidence
The resolver applies manifest_hook to every resolved package manifest, and apply_to_arc performs
a full applicability scan before cloning and applying, which repeats override selection logic for
manifests where overrides match.

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[117-128]
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[904-944]
pacquet/crates/package-manager/src/overrides.rs[134-187]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `apply_to_arc` path runs an applicability scan (`has_applicable_override`) and, if true, clones and applies overrides, which repeats similar per-dependency override selection work. This is on the resolver hot path because the resolver applies `manifest_hook` to every resolved manifest.
## Issue Context
The resolver applies `manifest_hook` before any downstream consumers read the manifest. Any extra per-manifest scanning scales with package count.
## Fix Focus Areas
- pacquet/crates/package-manager/src/overrides.rs[134-234]
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[889-944]
## Suggested fix approach
1. Pre-index overrides by target package name during `VersionsOverrider::new` (e.g. `HashMap<String, Vec<ResolvedOverride>>` for generic and parent-scoped). This makes both the applicability check and application avoid scanning unrelated overrides.
2. Make the applicability check cheaper than full selection (avoid building/sorting vectors just to detect a match), or restructure `apply_to_arc` to compute the needed information once (e.g. compute applicable parent-scoped set once and reuse it for both the “any match?” decision and mutation).
3. Keep the existing “clone only on match” behavior, but ensure the “match” determination is close to O(#deps * #candidate-overrides-for-those-deps), not O(#deps * #all-overrides).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit b21b98d

Results up to commit b21b98d


🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Remediation recommended
1. Absolute local override paths 🐞 Bug ≡ Correctness
Description
The fresh resolver’s overrides ManifestHook calls VersionsOverrider::apply_to_arc(..., None), so
relative link:/file: override values cannot be re-relativized and are rendered as absolute
paths. This can make overridden transitive dependency specs machine-specific and inconsistent with
the overrider’s documented local-target behavior.
Code

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[R830-838]

+        let overrides_hook: Option<ManifestHook> =
+            versions_overrider.as_ref().and_then(|overrider| {
+                if overrider.is_empty() {
+                    None
+                } else {
+                    let overrider = Arc::clone(overrider);
+                    Some(Arc::new(move |manifest| overrider.apply_to_arc(manifest, None))
+                        as ManifestHook)
+                }
Evidence
The overrides hook explicitly passes None as the manifest directory, and the overrider’s local
override rendering falls back to emitting the absolute path when no directory is provided.

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[825-838]
pacquet/crates/package-manager/src/overrides.rs[105-111]
pacquet/crates/package-manager/src/overrides.rs[329-372]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Fresh resolution applies overrides via a `ManifestHook` but passes `manifest_dir=None` into `VersionsOverrider::apply_to_arc`, which forces relative `link:`/`file:` override values to serialize as absolute paths when applied transitively.

## Issue Context
`resolve_local_override_spec` only re-relativizes when `pkg_dir` is `Some(_)`; otherwise it emits the absolute path stored in `LocalTarget`.

## Fix Focus Areas
- pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[825-839]
- pacquet/crates/package-manager/src/overrides.rs[329-372]

## Suggested fix approach
Prefer a contained fix in `VersionsOverrider`:
1. When parsing local override targets, also store the original normalized relative path component (or a root-dir-relative form) in `LocalTarget` when `specified_via_relative_path == true`.
2. Update `resolve_local_override_spec` so that when `pkg_dir` is `None` and the target was specified via a relative path, it returns a stable relative specifier (e.g. the stored root-relative form) instead of the absolute path.

This avoids changing the `ManifestHook` signature while preventing machine-specific absolute paths from being introduced by the hook path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Overrides hook double-scans manifests 🐞 Bug ➹ Performance
Description
VersionsOverrider::apply_to_arc scans dependency maps to decide whether any override applies and
then scans again to apply overrides after cloning, duplicating override-selection work for matching
manifests. Since ManifestHook runs on every resolved package manifest during fresh resolution,
this extra pass can add noticeable overhead on large graphs or large override sets.
Code

pacquet/crates/package-manager/src/overrides.rs[R134-171]

+    /// Apply overrides to the resolver's shared manifest value,
+    /// cloning only when at least one configured override can rewrite
+    /// the manifest.
+    pub fn apply_to_arc(&self, manifest: Arc<Value>, manifest_dir: Option<&Path>) -> Arc<Value> {
+        if self.is_empty() || !self.has_applicable_override(&manifest) {
+            return manifest;
        }
+        let mut cloned = (*manifest).clone();
+        self.apply_to_value(&mut cloned, manifest_dir);
+        Arc::new(cloned)
+    }
+
+    fn applicable_parent_scoped<'b>(&'b self, manifest: &Value) -> Vec<&'b ResolvedOverride> {
+        let manifest_name = manifest.get("name").and_then(Value::as_str);
+        let manifest_version = manifest.get("version").and_then(Value::as_str);
+
+        self.parent_scoped
+            .iter()
+            .filter(|entry| {
+                let Some(parent) = entry.inner.parent_pkg.as_ref() else { return false };
+                let name_matches = manifest_name == Some(parent.name.as_str());
+                let range_matches = match (parent.bare_specifier.as_deref(), manifest_version) {
+                    (None, _) => true,
+                    (Some(_), None) => false,
+                    (Some(range), Some(version)) => semver_satisfies(version, range),
+                };
+                name_matches && range_matches
+            })
+            .collect()
+    }
+
+    fn has_applicable_override(&self, value: &Value) -> bool {
+        let applicable_parent_scoped = self.applicable_parent_scoped(value);
+        [DependencyGroup::Prod, DependencyGroup::Optional, DependencyGroup::Dev]
+            .iter()
+            .copied()
+            .any(|group| self.group_has_override(value, group, &applicable_parent_scoped))
+    }
Evidence
The resolver applies manifest_hook to every resolved package manifest, and apply_to_arc performs
a full applicability scan before cloning and applying, which repeats override selection logic for
manifests where overrides match.

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[117-128]
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[904-944]
pacquet/crates/package-manager/src/overrides.rs[134-187]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `apply_to_arc` path runs an applicability scan (`has_applicable_override`) and, if true, clones and applies overrides, which repeats similar per-dependency override selection work. This is on the resolver hot path because the resolver applies `manifest_hook` to every resolved manifest.

## Issue Context
The resolver applies `manifest_hook` before any downstream consumers read the manifest. Any extra per-manifest scanning scales with package count.

## Fix Focus Areas
- pacquet/crates/package-manager/src/overrides.rs[134-234]
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[889-944]

## Suggested fix approach
1. Pre-index overrides by target package name during `VersionsOverrider::new` (e.g. `HashMap<String, Vec<ResolvedOverride>>` for generic and parent-scoped). This makes both the applicability check and application avoid scanning unrelated overrides.
2. Make the applicability check cheaper than full selection (avoid building/sorting vectors just to detect a match), or restructure `apply_to_arc` to compute the needed information once (e.g. compute applicable parent-scoped set once and reuse it for both the “any match?” decision and mutation).
3. Keep the existing “clone only on match” behavior, but ensure the “match” determination is close to O(#deps * #candidate-overrides-for-those-deps), not O(#deps * #all-overrides).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

@zkochan zkochan marked this pull request as draft June 9, 2026 10:27
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 9.834 ± 0.136 9.699 10.120 1.89 ± 0.04
pacquet@main 9.888 ± 0.199 9.724 10.247 1.90 ± 0.05
pnpr@HEAD 5.213 ± 0.090 5.141 5.455 1.00 ± 0.02
pnpr@main 5.203 ± 0.074 5.131 5.348 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 9.834321535339999,
      "stddev": 0.13629637550044385,
      "median": 9.79092797194,
      "user": 3.4120327799999997,
      "system": 2.2502731199999997,
      "min": 9.69874350944,
      "max": 10.120198865439999,
      "times": [
        9.94457493344,
        10.120198865439999,
        9.75651984344,
        9.69874350944,
        9.74304200544,
        9.717357545439999,
        9.981906991439999,
        9.79674414644,
        9.79901571544,
        9.785111797439999
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 9.88796067244,
      "stddev": 0.19909482168784703,
      "median": 9.81258123944,
      "user": 3.47584918,
      "system": 2.26701382,
      "min": 9.724486515439999,
      "max": 10.24683009044,
      "times": [
        9.83717702044,
        10.24683009044,
        9.724486515439999,
        9.95676094744,
        9.73767913244,
        9.78798545844,
        9.84292130644,
        9.734353989439999,
        9.773333492439999,
        10.23807877144
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 5.21272527984,
      "stddev": 0.09011313189881642,
      "median": 5.1854467924400005,
      "user": 2.83419478,
      "system": 2.0060142199999995,
      "min": 5.14077341844,
      "max": 5.45453640844,
      "times": [
        5.24786520844,
        5.15703111144,
        5.45453640844,
        5.201920368440001,
        5.21017035144,
        5.14077341844,
        5.181424387440001,
        5.1804322454400005,
        5.18946919744,
        5.163630101440001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 5.20346837284,
      "stddev": 0.0737353204357959,
      "median": 5.19019545444,
      "user": 2.8124940799999996,
      "system": 2.0079448199999996,
      "min": 5.130839536440001,
      "max": 5.3481042504400005,
      "times": [
        5.3481042504400005,
        5.2175339454400005,
        5.13211659644,
        5.1595651594400005,
        5.179424329440001,
        5.208744852440001,
        5.130839536440001,
        5.3112662654400005,
        5.20096657944,
        5.14612221344
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 499.0 ± 5.1 492.2 507.8 1.00
pacquet@main 512.1 ± 29.6 496.6 594.5 1.03 ± 0.06
pnpr@HEAD 611.9 ± 19.6 591.1 642.3 1.23 ± 0.04
pnpr@main 610.6 ± 14.1 598.0 642.9 1.22 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.4990146106,
      "stddev": 0.005138112035447161,
      "median": 0.49724253850000005,
      "user": 0.38092541999999996,
      "system": 0.8159348599999999,
      "min": 0.4922201090000001,
      "max": 0.5078067850000001,
      "times": [
        0.5034277060000001,
        0.4922201090000001,
        0.5011145140000001,
        0.49739551400000004,
        0.49372897500000007,
        0.49645061200000007,
        0.5078067850000001,
        0.49708956300000007,
        0.49569939500000004,
        0.505212933
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.5120584560999999,
      "stddev": 0.02964852684557901,
      "median": 0.5010302405,
      "user": 0.38081541999999996,
      "system": 0.83427936,
      "min": 0.49659049600000005,
      "max": 0.594526145,
      "times": [
        0.49904455500000006,
        0.5088913340000001,
        0.5127742860000001,
        0.512275405,
        0.49878552700000006,
        0.503015926,
        0.49727245800000003,
        0.49740842900000004,
        0.49659049600000005,
        0.594526145
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.611946642,
      "stddev": 0.019596109741740572,
      "median": 0.6038856695000001,
      "user": 0.39545062,
      "system": 0.8359194599999998,
      "min": 0.591118048,
      "max": 0.642297599,
      "times": [
        0.642297599,
        0.6029100150000001,
        0.5976104870000001,
        0.5974736780000001,
        0.5920455520000001,
        0.604861324,
        0.6223899430000001,
        0.591118048,
        0.638994292,
        0.629765482
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6105714346,
      "stddev": 0.014110336688613409,
      "median": 0.6041032105,
      "user": 0.40204981999999995,
      "system": 0.8314315599999998,
      "min": 0.5980334420000001,
      "max": 0.642909905,
      "times": [
        0.6036830630000001,
        0.603471964,
        0.603165108,
        0.5980334420000001,
        0.6103498940000001,
        0.598139932,
        0.616296147,
        0.604523358,
        0.6251415330000001,
        0.642909905
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 9.255 ± 0.034 9.189 9.318 1.85 ± 0.04
pacquet@main 9.232 ± 0.065 9.155 9.351 1.84 ± 0.04
pnpr@HEAD 5.013 ± 0.116 4.919 5.231 1.00
pnpr@main 5.046 ± 0.165 4.872 5.342 1.01 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 9.25521777612,
      "stddev": 0.0342772683398088,
      "median": 9.26037506482,
      "user": 4.108352579999999,
      "system": 2.3156082,
      "min": 9.18945026932,
      "max": 9.318453997319999,
      "times": [
        9.26087987532,
        9.24709626332,
        9.22929991432,
        9.18945026932,
        9.28334744732,
        9.318453997319999,
        9.23374664132,
        9.26232618632,
        9.259870254319999,
        9.26770691232
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 9.23173138592,
      "stddev": 0.06463914006801665,
      "median": 9.23455797532,
      "user": 4.100974479999999,
      "system": 2.3258551,
      "min": 9.154911993319999,
      "max": 9.35095324432,
      "times": [
        9.31174931232,
        9.23754599132,
        9.24374777932,
        9.35095324432,
        9.23156995932,
        9.22478684332,
        9.154911993319999,
        9.24464577532,
        9.16231084032,
        9.155092120319999
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 5.012678608819999,
      "stddev": 0.11616921427313696,
      "median": 4.944413235819999,
      "user": 2.6128703799999995,
      "system": 1.932741,
      "min": 4.91895692632,
      "max": 5.2312168113199995,
      "times": [
        4.9389904393199995,
        5.1777127283199995,
        5.2312168113199995,
        4.92952891632,
        4.9498360323199995,
        4.91895692632,
        4.93165602932,
        5.0437458163199995,
        4.91995321732,
        5.08518917132
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 5.046348280919999,
      "stddev": 0.16470438670877655,
      "median": 4.96044676632,
      "user": 2.6356757799999997,
      "system": 1.9321471999999997,
      "min": 4.87246071232,
      "max": 5.34151398732,
      "times": [
        4.91960232832,
        4.97187737032,
        5.28145176732,
        5.074037426319999,
        4.936771204319999,
        4.94139871032,
        5.34151398732,
        5.1753531403199995,
        4.87246071232,
        4.9490161623199995
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.297 ± 0.010 1.285 1.319 2.45 ± 0.11
pacquet@main 1.314 ± 0.022 1.293 1.360 2.49 ± 0.11
pnpr@HEAD 0.563 ± 0.119 0.516 0.902 1.06 ± 0.23
pnpr@main 0.529 ± 0.023 0.519 0.593 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.29732629894,
      "stddev": 0.010350214593976904,
      "median": 1.29598545454,
      "user": 1.6323625199999998,
      "system": 1.1611868,
      "min": 1.28508477604,
      "max": 1.31910819304,
      "times": [
        1.29116612304,
        1.29497259404,
        1.31910819304,
        1.30097481704,
        1.2876935860399998,
        1.3004360880399999,
        1.29699831504,
        1.28508477604,
        1.30785094604,
        1.28897755104
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.31378062814,
      "stddev": 0.021926274533387358,
      "median": 1.30471899704,
      "user": 1.6494324200000001,
      "system": 1.1691496000000001,
      "min": 1.29309969304,
      "max": 1.35970561604,
      "times": [
        1.35970561604,
        1.34466809104,
        1.3037972550399999,
        1.29334314104,
        1.31159727304,
        1.31924782204,
        1.30372865204,
        1.30564073904,
        1.30297799904,
        1.29309969304
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.56250993244,
      "stddev": 0.11947467090314702,
      "median": 0.5252473990400002,
      "user": 0.33716011999999995,
      "system": 0.8152322999999999,
      "min": 0.5164765920400001,
      "max": 0.9021927270400001,
      "times": [
        0.9021927270400001,
        0.5278786850400001,
        0.5266938820400001,
        0.5213132100400001,
        0.5226364530400001,
        0.5164765920400001,
        0.5205590590400001,
        0.53685391804,
        0.5253631740400001,
        0.5251316240400001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.5286786412400002,
      "stddev": 0.02263341838126304,
      "median": 0.52148523304,
      "user": 0.33307282,
      "system": 0.8066559999999999,
      "min": 0.51907977404,
      "max": 0.5929425130400001,
      "times": [
        0.5929425130400001,
        0.5202819770400001,
        0.51907977404,
        0.5217244990400001,
        0.5237301260400001,
        0.5205896250400001,
        0.52154668004,
        0.5243912840400001,
        0.5210761480400001,
        0.5214237860400001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.954 ± 0.033 4.897 5.026 9.35 ± 0.23
pacquet@main 5.007 ± 0.079 4.945 5.215 9.45 ± 0.27
pnpr@HEAD 0.535 ± 0.020 0.520 0.585 1.01 ± 0.05
pnpr@main 0.530 ± 0.012 0.515 0.561 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.954499059000001,
      "stddev": 0.03307983307643209,
      "median": 4.950349158,
      "user": 1.9027148,
      "system": 1.3134935800000003,
      "min": 4.8968434815,
      "max": 5.0263145265,
      "times": [
        4.9415016265,
        4.9750156385,
        4.8968434815,
        4.9663619705,
        4.9620215485,
        5.0263145265,
        4.9401740285,
        4.9503845715,
        4.9360594535,
        4.9503137445
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 5.0068464293,
      "stddev": 0.07877592169656093,
      "median": 4.995803369000001,
      "user": 1.9078920999999998,
      "system": 1.31892568,
      "min": 4.944910574500001,
      "max": 5.2152732665,
      "times": [
        4.9978886615,
        5.0357309985,
        5.0110326075,
        4.9952025215,
        5.2152732665,
        4.9964042165,
        4.944910574500001,
        4.954312084500001,
        4.9627301395000005,
        4.9549792225000004
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.5349985861000001,
      "stddev": 0.020307584834425144,
      "median": 0.5277470125,
      "user": 0.33668339999999997,
      "system": 0.81051538,
      "min": 0.5199181355,
      "max": 0.5851817775,
      "times": [
        0.5851817775,
        0.5211241055,
        0.5351579925000001,
        0.5253585595,
        0.5538570795000001,
        0.5246618235,
        0.5211499365000001,
        0.5334409855000001,
        0.5301354655,
        0.5199181355
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.5296481419000001,
      "stddev": 0.012465861549624348,
      "median": 0.5270410275,
      "user": 0.33065639999999996,
      "system": 0.8107985799999999,
      "min": 0.5147244475,
      "max": 0.5614695735,
      "times": [
        0.5614695735,
        0.5356626985,
        0.5262383485000001,
        0.5250876015,
        0.5286387535,
        0.5207518615000001,
        0.5277008155,
        0.5263812395,
        0.5298260795,
        0.5147244475
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12283
Testbedpacquet

🚨 1 Alert

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-storeLatency
seconds (s)
📈 plot
🚷 threshold
🚨 alert (🔔)
9.26 s
(+43.52%)Baseline: 6.45 s
7.74 s
(119.60%)

Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
🚨 view alert (🔔)
9,255.22 ms
(+43.52%)Baseline: 6,448.91 ms
7,738.69 ms
(119.60%)

isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
4,954.50 ms
(-1.19%)Baseline: 5,014.11 ms
6,016.93 ms
(82.34%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,297.33 ms
(-7.20%)Baseline: 1,397.96 ms
1,677.55 ms
(77.33%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
9,834.32 ms
(+14.06%)Baseline: 8,622.02 ms
10,346.42 ms
(95.05%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
499.01 ms
(-23.42%)Baseline: 651.60 ms
781.92 ms
(63.82%)
🐰 View full continuous benchmarking report in Bencher

@zkochan zkochan marked this pull request as ready for review June 9, 2026 12:06
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit b21b98d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants