Skip to content

feat(lockfile): emit patchedDependencies block in pacquet lockfile#12281

Merged
zkochan merged 3 commits into
mainfrom
pacquet-lockfile-patched-dependencies
Jun 9, 2026
Merged

feat(lockfile): emit patchedDependencies block in pacquet lockfile#12281
zkochan merged 3 commits into
mainfrom
pacquet-lockfile-patched-dependencies

Conversation

@zkochan

@zkochan zkochan commented Jun 9, 2026

Copy link
Copy Markdown
Member

PR Summary by Qodo

Emit patchedDependencies in pacquet-generated pnpm-lock.yaml
✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

Walkthroughs

User Description

What

Ports the top-level patchedDependencies: lockfile block to pacquet — item 6 of the pacquet lockfile-parity tracking issue, #12266.

Why

pacquet install --lockfile-only already resolved and hashed patches (for the depPath (patch_hash=…) suffix and at build time) but never wrote the top-level patchedDependencies: block into pnpm-lock.yaml. So a clean-state lockfile diverged from pnpm's, which records e.g.:

patchedDependencies:
  graceful-fs@4.2.11: 68ebc232025360cb3dcd3081f4067f4e9fc022ab6b6f71a3230e86c7a5b337d1

How

  • pacquet-lockfile — add patched_dependencies: Option<BTreeMap<String, String>> to the Lockfile struct, in its sortLockfileKeys slot (between pnpmfileChecksum and importers). yaml_emit already sorts this section's keys, so it serializes byte-faithfully with no dumper change.
  • pacquet-config — add Config::patched_dependency_hashes(), a port of pnpm's calcPatchHashes(opts.patchedDependencies): resolve each patch path against the workspace dir and hash it, keeping the user's verbatim keys. This is distinct from resolved_patched_dependencies() (which buckets by package name) so a bare foo and foo@* stay separate lockfile keys rather than collapsing into one group bucket.
  • pacquet-package-manager — compute the hashes once per install and thread them through GraphToLockfileOptionsdependencies_graph_to_lockfile; the current lockfile (lock.yaml) carries them through.

Testing

  • New tests at all three layers: config hashing, graph→lockfile flow (+ empty→None), and a byte-parity save test modeling the repo's own graceful-fs@4.2.11 entry.
  • Verified the hash pacquet computes (68ebc232…) matches the committed pnpm-lock.yaml exactly.
  • pacquet-lockfile (449) and pacquet-config suites pass; install::tests (103) pass; clippy / fmt / taplo / typos clean; full-workspace cargo check --all-targets clean.

This is a pacquet-only catch-up to the TS pnpm CLI (which already emits this block), so no TS-side change and no changeset are needed.


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

AI Description
• Add top-level patchedDependencies to pacquet lockfile model and YAML output for pnpm parity.
• Compute patch file SHA-256 hashes from config using verbatim user keys and workspace-relative
  paths.
• Thread patch-hash map through install and graph→lockfile generation; add byte-parity/round-trip
  tests.
Diagram
graph TD
  A["Config"] --> B["Patch hashing"] --> C["Install (fresh lockfile)"] --> D["Graph→Lockfile"] --> E["Lockfile struct"] --> F["pnpm-lock.yaml"]
  E --> G["Current lockfile filter"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Return hashes alongside grouped patch records
  • ➕ Single pass over patchedDependencies data; avoids duplicated path resolution logic.
  • ➕ Keeps patch-related behavior centralized in pacquet_patching.
  • ➖ API churn for existing callers of resolve_and_group; larger refactor.
  • ➖ Still must preserve verbatim keys for lockfile while grouping by package for resolver, complicating the return type.
2. Compute patchedDependencies hashes at lockfile serialization time
  • ➕ Avoids threading a new option through install/graph layers.
  • ➕ Keeps graph→lockfile API smaller.
  • ➖ Moves I/O (reading patch files) into serialization, making it harder to cache and harder to error-report in install flow.
  • ➖ Breaks separation of concerns: lockfile emission would depend on workspace filesystem context.
3. Reuse resolved_patched_dependencies output to derive lockfile map
  • ➕ No new Config method; fewer code paths.
  • ➖ resolved_patched_dependencies groups by package name and can collapse distinct user keys (e.g. foo vs foo@*), losing byte-faithful parity with pnpm.
  • ➖ Would require extra bookkeeping to reconstruct the original keys anyway.

Recommendation: Current approach is the best fit for pnpm parity: compute a verbatim key→hash map once per install (where filesystem context and error handling belong), keep it distinct from the grouped resolver view, and thread it into lockfile generation with empty-map normalization. The main thing to double-check in review is that all code paths that write/filter lockfiles consistently preserve/omit the field and that error surfaces are appropriately mapped (CalcPatchHashes).

Grey Divider

File Changes

Enhancement (5)
lib.rs Add Config::patched_dependency_hashes() for lockfile patchedDependencies +41/-1

Add Config::patched_dependency_hashes() for lockfile patchedDependencies

• Imports patch-hash calculation utilities and adds a new method that resolves patch paths against workspace_dir and returns a verbatim key→SHA-256 map for lockfile emission. Distinguishes this from the grouped resolver representation to preserve byte-faithful lockfile keys.

pacquet/crates/config/src/lib.rs


lib.rs Extend Lockfile model with patched_dependencies field in v9 key order +17/-0

Extend Lockfile model with patched_dependencies field in v9 key order

• Adds optional patched_dependencies (BTreeMap) to the lockfile struct with documentation tying ordering/behavior to pnpm’s sortLockfileKeys and calcPatchHashes. Uses BTreeMap to ensure stable lexical key ordering on serialization.

pacquet/crates/lockfile/src/lib.rs


current_lockfile.rs Preserve patchedDependencies when filtering wanted → current lockfile +4/-0

Preserve patchedDependencies when filtering wanted → current lockfile

• Clones patched_dependencies into the filtered current lockfile so patch hashes survive into node_modules/.pnpm/lock.yaml round-trips.

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


dependencies_graph_to_lockfile.rs Thread patchedDependencies hashes into GraphToLockfileOptions and lockfile output +8/-0

Thread patchedDependencies hashes into GraphToLockfileOptions and lockfile output

• Adds patched_dependencies to GraphToLockfileOptions and sets Lockfile.patched_dependencies from it, normalizing empty maps to None so the YAML key is omitted when not needed.

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


install_with_fresh_lockfile.rs Compute patch hashes once per install and write to lockfile +18/-0

Compute patch hashes once per install and write to lockfile

• Adds an install error variant for patch hashing, computes Config::patched_dependency_hashes() alongside grouped patch resolution, and threads the result into fresh lockfile building so wanted/per-project lockfiles record top-level patchedDependencies.

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


Tests (7)
tests.rs Test config patch hashing resolves relative paths and returns verbatim keys +25/-0

Test config patch hashing resolves relative paths and returns verbatim keys

• Adds a unit test creating a temporary patch file, hashing it, and asserting Config::patched_dependency_hashes returns None when unset and the expected digest when configured.

pacquet/crates/config/src/tests.rs


tests.rs Add byte-parity YAML save test for patchedDependencies block +46/-0

Add byte-parity YAML save test for patchedDependencies block

• Introduces a fixture asserting patchedDependencies parses, round-trips, and renders in the correct top-level position between settings and importers. Updates existing lockfile construction in tests to include patched_dependencies: None.

pacquet/crates/lockfile/src/save_lockfile/tests.rs


tests.rs Update current lockfile test fixtures for new patched_dependencies field +1/-0

Update current lockfile test fixtures for new patched_dependencies field

• Extends the empty Lockfile test helper to populate patched_dependencies: None to match the updated struct shape.

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


tests.rs Test patchedDependencies map flows into lockfile and empty is omitted +74/-0

Test patchedDependencies map flows into lockfile and empty is omitted

• Updates existing option builders to include patched_dependencies: None and adds a dedicated test verifying non-empty maps are preserved verbatim while empty maps are normalized away.

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


tests.rs Update hoisted dep graph lockfile fixtures for patched_dependencies field +5/-0

Update hoisted dep graph lockfile fixtures for patched_dependencies field

• Adds patched_dependencies: None to multiple Lockfile constructors used in hoisting-related tests to align with the new struct field.

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


tests.rs Update real-hoist lockfile fixtures for patched_dependencies field +19/-0

Update real-hoist lockfile fixtures for patched_dependencies field

• Adds patched_dependencies: None to Lockfile test fixtures across hoist scenarios to satisfy the updated lockfile struct.

pacquet/crates/real-hoist/src/tests.rs


tests.rs Update lockfile reuse test fixture for patched_dependencies field +1/-0

Update lockfile reuse test fixture for patched_dependencies field

• Extends the empty Lockfile helper to set patched_dependencies: None, keeping reuse tests compiling with the new field.

pacquet/crates/resolving-deps-resolver/src/lockfile_reuse/tests.rs


Grey Divider

Qodo Logo

Summary by CodeRabbit

  • New Features

    • Lockfiles now include a top-level patchedDependencies block with SHA-256 hashes of applied patch files.
    • Config can produce patch-file hashes for inclusion in lockfiles and freshness checks.
  • Behavior Changes

    • Freshness/frozen-lockfile reuse and optimistic fast paths now consider patch hashes and patch-file mtimes; mismatches or hash failures can abort reuse/install.
  • Tests

    • Added/updated tests for patch-hash computation, serialization/order, drift detection, and mtime-based fast-path invalidation.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Config resolves patch paths and computes SHA-256 digests via Config::patched_dependency_hashes; Lockfile gains a top-level patchedDependencies BTreeMap; plumbing threads these hashes into lockfile construction, freshness checks, optimistic fast paths, and install flows; tests and fixtures updated accordingly.

Changes

Patched Dependencies Hashing and Lockfile Integration

Layer / File(s) Summary
Config patch-hash computation
pacquet/crates/config/src/lib.rs, pacquet/crates/config/src/tests.rs
Config::patched_dependency_hashes resolves configured patch paths relative to workspace_dir and computes SHA-256 hex digests via calc_patch_hashes, returning a lockfile-ready mapping keyed by patched-dependency identifier, or Ok(None) when inputs are unset.
Lockfile schema and serialization
pacquet/crates/lockfile/src/lib.rs, pacquet/crates/lockfile/src/save_lockfile/tests.rs, pacquet/crates/package-manager/src/current_lockfile.rs
Lockfile gains optional patched_dependencies: Option<BTreeMap<String, String>>, omitted when None; save/parse tests validate round-trip rendering and current-lockfile preservation.
Lockfile construction via GraphToLockfileOptions
pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs, pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
GraphToLockfileOptions accepts patched_dependencies and dependencies_graph_to_lockfile threads it into the produced Lockfile, filtering empty maps so the field is omitted when absent; tests cover non-empty vs empty vs None behavior.
Freshness checks for patchedDependencies
pacquet/crates/lockfile/src/freshness.rs, pacquet/crates/lockfile/src/freshness/tests.rs
Add StalenessReason::PatchedDependenciesChanged and extend check_lockfile_settings signature/logic to compare lockfile vs config patchedDependencies (normalized BTreeMap); tests added for match, change, and removal cases and call sites updated.
Fresh-install flow computes and wires patches
pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
InstallWithFreshLockfile::run computes patched_dependency_hashes from config, maps errors to a new CalcPatchHashes variant, and threads the result into build_fresh_lockfile for both --lockfile-only and normal install paths.
Install dispatch handles patch-hash failures
pacquet/crates/package-manager/src/install.rs
Prefer-frozen decision treats patch-hash computation failures as fatal; added FreshnessCheckError::CalcPatchHashes and mapping into InstallError conversions to abort frozen-lockfile reuse on hash-compute errors.
pnpr resolver frozen-lockfile gating
pnpr/crates/pnpr/src/resolver/resolve.rs
fresh_frozen_input_lockfile disqualifies reuse when config.patched_dependencies is non-empty and forwards updated check inputs.
Optimistic repeat-install patch mtime check
pacquet/crates/package-manager/src/optimistic_repeat_install.rs, pacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs
Adds pre-manifest patch-file mtime check via patches_modified_since and tests covering both newer and older patch mtime scenarios relative to validation timestamp.
Test fixture updates for patched_dependencies field
pacquet/crates/package-manager/src/hoisted_dep_graph/tests.rs, pacquet/crates/real-hoist/src/tests.rs, pacquet/crates/resolving-deps-resolver/src/lockfile_reuse/tests.rs, many others
Numerous test helpers and inline Lockfile { ... } literals updated to include patched_dependencies: None for schema compatibility across test suites.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pnpm#11824: Overlaps install dispatch freshness/gating changes that interact with patchedDependencies handling.
  • pnpm/pnpm#11943: Related changes to optimistic repeat-install fast-path; both modify how patch state affects the UpToDate decision.
  • pnpm/pnpm#11905: Refactors fresh-resolve lockfile pipeline; this PR extends the same pipeline to thread patchedDependencies into the produced lockfile.

Poem

"I nibble bytes and hash each patch,
A floppy-eared auditor of every batch,
Lockfiles hum with sorted keys,
I hop to keep their round-trip ease,
Hooray for tidy hashes! 🐰"

🚥 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 clearly and concisely describes the main change: adding patchedDependencies block emission to the pacquet lockfile format.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pacquet-lockfile-patched-dependencies

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.

@zkochan zkochan force-pushed the pacquet-lockfile-patched-dependencies branch from 32b8961 to c8244f7 Compare June 9, 2026 05:29
@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.02      8.4±0.22ms   514.9 KB/sec    1.00      8.3±0.08ms   525.6 KB/sec

@codecov-commenter

codecov-commenter commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.61538% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.55%. Comparing base (b730835) to head (a88cc77).

Files with missing lines Patch % Lines
pacquet/crates/package-manager/src/install.rs 42.85% 4 Missing ⚠️
...s/package-manager/src/optimistic_repeat_install.rs 84.21% 3 Missing ⚠️
pnpr/crates/pnpr/src/resolver/resolve.rs 0.00% 2 Missing ⚠️
pacquet/crates/config/src/lib.rs 92.85% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #12281   +/-   ##
=======================================
  Coverage   87.54%   87.55%           
=======================================
  Files         280      280           
  Lines       33476    33540   +64     
=======================================
+ Hits        29307    29365   +58     
- Misses       4169     4175    +6     

☔ 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 06:02
@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 (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0)

Grey Divider


Action required

1. Fast-path misses patch edits 🐞 Bug ≡ Correctness
Description
Install::run can short-circuit via optimisticRepeatInstall before check_lockfile_freshness
runs, so patchedDependencies hashes are never recomputed/compared and a modified patch file may
not trigger reinstall/lockfile refresh. This can silently leave node_modules and/or
pnpm-lock.yaml inconsistent with the current patch contents.
Code

pacquet/crates/package-manager/src/install.rs[R1234-1246]

+    // `calcPatchHashes(opts.patchedDependencies)` — reading the patch
+    // files here lets `check_lockfile_settings` catch an edited patch
+    // whose hash (and thus its `(patch_hash=...)` depPath suffix) drifted
+    // from what the lockfile recorded.
+    let patched_dependency_hashes =
+        config.patched_dependency_hashes().map_err(FreshnessCheckError::CalcPatchHashes)?;
 pacquet_lockfile::check_lockfile_settings(
     lockfile,
     overrides_map.as_ref(),
     package_extensions_checksum.as_deref(),
     config.ignored_optional_dependencies.as_deref(),
+        patched_dependency_hashes.as_ref(),
     config.inject_workspace_packages,
Evidence
The install can return early on the optimistic repeat-install path before any lockfile reads/checks;
the patch-hash drift detection is performed only inside check_lockfile_freshness, and the
optimistic-repeat implementation documents that patch/hook modification checks are not ported. With
optimistic_repeat_install defaulting to true, patch edits can bypass the new drift detection
entirely.

pacquet/crates/package-manager/src/install.rs[453-504]
pacquet/crates/package-manager/src/install.rs[1219-1249]
pacquet/crates/package-manager/src/optimistic_repeat_install.rs[18-27]
pacquet/crates/config/src/lib.rs[516-546]

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

## Issue description
`optimisticRepeatInstall` can return `Already up to date` before the new `patchedDependencies` hash drift check executes. As a result, editing a patch file may not invalidate the fast path, leaving patched output and the lockfile’s recorded patch hashes stale.
### Issue Context
- The optimistic repeat-install path explicitly runs before any lockfile freshness checks.
- Patch-file content drift is only detected inside `check_lockfile_freshness` by hashing patch files and comparing against the lockfile.
- `optimistic_repeat_install` defaults to `true`, so this bypass is possible in typical installs.
### Fix Focus Areas
Implement one of:
1) **Conservative correctness:** disable the optimistic repeat-install fast path when `Config.patched_dependencies` is `Some` and non-empty.
2) **Full parity:** extend the optimistic repeat-install decision to detect patch-file edits (e.g., hash patch files and compare against a previously recorded value, or store patch hashes/mtimes in workspace state).
- pacquet/crates/package-manager/src/install.rs[453-504]
- pacquet/crates/package-manager/src/install.rs[1219-1249]
- pacquet/crates/package-manager/src/optimistic_repeat_install.rs[18-27]
- pacquet/crates/config/src/lib.rs[516-546]

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


2. Patch drift not checked ✓ Resolved 🐞 Bug ≡ Correctness
Description
patchedDependencies hashes are now written into the lockfile, but check_lockfile_settings (used
by the frozen-lockfile freshness gate) still does not compare them against the current patch-file
hashes. As a result, a patch file content change may not invalidate an existing lockfile, even
though (patch_hash=…) participates in downstream identity/paths.
Code

pacquet/crates/lockfile/src/lib.rs[R153-168]

+    /// `patchedDependencies` recorded by the install that wrote this
+    /// lockfile: each configured `patchedDependencies` key (e.g.
+    /// `graceful-fs@4.2.11`) mapped to the SHA-256 hex digest of its
+    /// patch file. Top-level in the v9 wire shape, sitting between
+    /// `pnpmfileChecksum` and `importers` in pnpm's
+    /// [`sortLockfileKeys`](https://github.com/pnpm/pnpm/blob/e7e99f04e4/lockfile/fs/src/sortLockfileKeys.ts#L34-L42)
+    /// root-key order. Mirrors upstream's
+    /// [`patchedDependencies`](https://github.com/pnpm/pnpm/blob/39101f5e37/lockfile/types/src/index.ts#L23)
+    /// field, which records
+    /// [`calcPatchHashes(opts.patchedDependencies)`](https://github.com/pnpm/pnpm/blob/39101f5e37/installing/deps-installer/src/install/index.ts#L547-L549).
+    /// A [`BTreeMap`] so the entries serialize sorted by key, matching
+    /// pnpm's `sortDirectKeys` pass over this map.
+    ///
+    /// [`BTreeMap`]: std::collections::BTreeMap
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub patched_dependencies: Option<std::collections::BTreeMap<String, String>>,
Evidence
The lockfile gained a patched_dependencies field, but the lockfile freshness gate does not compare
it, even though patch hashes affect package identity via (patch_hash=…) handling downstream.

pacquet/crates/lockfile/src/lib.rs[153-168]
pacquet/crates/lockfile/src/freshness.rs[214-238]
pacquet/crates/package-manager/src/install.rs[1218-1239]
pacquet/crates/package-manager/src/create_virtual_store.rs[660-670]

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 lockfile now records `patchedDependencies` (key -> SHA-256 hex digest), but lockfile freshness checking (`check_lockfile_settings`) does not validate this field. This can allow `--frozen-lockfile` installs to proceed while patch-file contents have changed, leaving the lockfile’s recorded patch hashes stale.
## Issue Context
- `Lockfile` now has `patched_dependencies`.
- The frozen-lockfile freshness gate calls `pacquet_lockfile::check_lockfile_settings(...)` but that function only checks overrides/packageExtensionsChecksum/ignoredOptionalDependencies/injectWorkspacePackages/peersSuffixMaxLength.
- Patch hashes are embedded into snapshot identity via `(patch_hash=...)` and are used when deriving `pkg_id_with_patch_hash` from snapshot keys.
## Fix Focus Areas
- pacquet/crates/lockfile/src/freshness.rs[214-320]
- pacquet/crates/package-manager/src/install.rs[1218-1239]
- pacquet/crates/config/src/lib.rs[1519-1566]
### Concrete fix
1. Extend `StalenessReason` with a `PatchedDependenciesChanged { lockfile: BTreeMap<String,String>, config: BTreeMap<String,String> }` variant.
2. Extend `check_lockfile_settings(...)` to accept `patched_dependency_hashes: Option<&BTreeMap<String,String>>` (or similar) and compare it against `lockfile.patched_dependencies`.
- Normalize `None` and empty maps as equivalent (consistent with other settings checks).
3. In `check_lockfile_freshness` (package-manager `install.rs`), compute `config.patched_dependency_hashes()` and pass it into `check_lockfile_settings`.
- Add/propagate an error variant if hashing fails (reading patch files can error).
4. Update any other call sites of `check_lockfile_settings` (e.g., pnpr) accordingly.

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



Remediation recommended

3. Patch files hashed twice 🐞 Bug ➹ Performance
Description
Fresh installs hash patch files once in resolve_and_group (via resolved_patched_dependencies)
and again in patched_dependency_hashes, duplicating disk reads and SHA-256 work. This is
unnecessary overhead and can be avoided by reusing the already-computed hashes.
Code

pacquet/crates/config/src/lib.rs[R1549-1566]

+    pub fn patched_dependency_hashes(
+        &self,
+    ) -> Result<Option<BTreeMap<String, String>>, CalcPatchHashError> {
+        let (Some(workspace_dir), Some(raw)) = (&self.workspace_dir, &self.patched_dependencies)
+        else {
+            return Ok(None);
+        };
+        let resolved = raw.iter().map(|(key, rel_or_abs)| {
+            let candidate = Path::new(rel_or_abs);
+            let path = if candidate.is_absolute() {
+                candidate.to_path_buf()
+            } else {
+                workspace_dir.join(candidate)
+            };
+            (key.clone(), path)
+        });
+        Ok(Some(calc_patch_hashes(resolved)?))
+    }
Evidence
resolve_and_group reads+hashes each patch file, and the new patched_dependency_hashes path
reads+hashes the same files again; install code calls both in sequence.

pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[827-845]
pacquet/crates/patching/src/resolve.rs[64-82]
pacquet/crates/config/src/lib.rs[1549-1566]
pacquet/crates/patching/src/hash.rs[57-68]

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

## Issue description
Patch files are currently read+hashed twice per install when patches are configured:
- `Config::resolved_patched_dependencies()` -> `pacquet_patching::resolve_and_group()` hashes each patch file.
- `Config::patched_dependency_hashes()` resolves the same paths and hashes each file again via `calc_patch_hashes()`.
## Issue Context
This is redundant: the resolver path already computed SHA-256 hashes for every configured key.
## Fix Focus Areas
- pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs[827-845]
- pacquet/crates/patching/src/resolve.rs[64-86]
- pacquet/crates/config/src/lib.rs[1519-1566]
- pacquet/crates/patching/src/hash.rs[57-68]
### Concrete fix options
Option A (preferred):
1. Add a helper in `pacquet_patching` to collect `BTreeMap<key, hash>` from a `PatchGroupRecord` (using each `ExtendedPatchInfo.key` -> `ExtendedPatchInfo.hash`).
2. In `install_with_fresh_lockfile`, after computing `patched_dependencies` once, derive `patched_dependency_hashes` from the grouped record without rereading files.
3. Remove or repoint `Config::patched_dependency_hashes()` to avoid a second hashing pass.
Option B:
Create a new `pacquet_patching` API that resolves paths once and returns both `(PatchGroupRecord, BTreeMap<String,String>)` in one pass, then use it from config/install.

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


Grey Divider

Qodo Logo

Comment thread pacquet/crates/lockfile/src/lib.rs
@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.974 ± 0.191 9.817 10.333 1.85 ± 0.06
pacquet@main 9.930 ± 0.135 9.815 10.277 1.84 ± 0.05
pnpr@HEAD 5.383 ± 0.129 5.281 5.721 1.00
pnpr@main 5.407 ± 0.137 5.314 5.730 1.00 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 9.97412217692,
      "stddev": 0.19122886089543223,
      "median": 9.89024299242,
      "user": 3.0562098399999997,
      "system": 3.2469278599999996,
      "min": 9.81712069642,
      "max": 10.33304234842,
      "times": [
        10.02066742442,
        9.96216677842,
        10.33304234842,
        10.299377419419999,
        9.90238017442,
        9.87353835842,
        9.82980909342,
        9.81712069642,
        9.82501366542,
        9.87810581042
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 9.930094471919997,
      "stddev": 0.13508613739909,
      "median": 9.891884136919998,
      "user": 3.0833564399999993,
      "system": 3.31841206,
      "min": 9.81479780642,
      "max": 10.277153886419999,
      "times": [
        9.85122031842,
        9.84499596742,
        9.925373879419999,
        9.81479780642,
        9.89610323342,
        10.03054823742,
        9.88884999342,
        9.87698311642,
        10.277153886419999,
        9.894918280419999
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 5.38302285542,
      "stddev": 0.12855450331427387,
      "median": 5.33106323192,
      "user": 2.5112710399999996,
      "system": 2.96809186,
      "min": 5.28086207742,
      "max": 5.72123388242,
      "times": [
        5.40996726042,
        5.35303376742,
        5.32893361242,
        5.72123388242,
        5.333192851420001,
        5.32570633942,
        5.31643308042,
        5.44805680242,
        5.28086207742,
        5.31280888042
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 5.40675920852,
      "stddev": 0.13735402194220353,
      "median": 5.34892380242,
      "user": 2.48444954,
      "system": 2.9621580599999997,
      "min": 5.31360033042,
      "max": 5.73044855942,
      "times": [
        5.31892165942,
        5.35704221942,
        5.31360033042,
        5.34163177942,
        5.3562158254200005,
        5.574811164420001,
        5.41040934842,
        5.333251858420001,
        5.33125934042,
        5.73044855942
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 656.5 ± 11.0 640.1 678.6 1.00
pacquet@main 675.8 ± 26.7 649.5 729.5 1.03 ± 0.04
pnpr@HEAD 778.2 ± 95.3 719.5 1042.6 1.19 ± 0.15
pnpr@main 771.9 ± 43.8 728.7 876.8 1.18 ± 0.07
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6565091374,
      "stddev": 0.010988727521776008,
      "median": 0.65687256,
      "user": 0.38811531999999993,
      "system": 1.3259450199999998,
      "min": 0.6401165305000001,
      "max": 0.6785752755000001,
      "times": [
        0.6785752755000001,
        0.6570444755,
        0.6567006445000001,
        0.6584717875,
        0.6401165305000001,
        0.6658185755,
        0.6611865955,
        0.6436224755000001,
        0.6498313615000001,
        0.6537236525000001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6758314305,
      "stddev": 0.026712482083816325,
      "median": 0.6688266140000001,
      "user": 0.39228502,
      "system": 1.33436192,
      "min": 0.6495445715,
      "max": 0.7295363275000001,
      "times": [
        0.6869546035,
        0.7295363275000001,
        0.7090356795,
        0.6495445715,
        0.6532599275000001,
        0.6556282695000001,
        0.6764849815,
        0.6611682465000001,
        0.6820540145,
        0.6546476835
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.7781698551,
      "stddev": 0.09528657947976128,
      "median": 0.7501312535,
      "user": 0.39609861999999996,
      "system": 1.3165503199999997,
      "min": 0.7194782605000001,
      "max": 1.0426208305,
      "times": [
        0.7523916495,
        0.7478708575,
        0.7694881235000001,
        0.7424034885,
        0.7337739915,
        0.7194782605000001,
        1.0426208305,
        0.7927057505,
        0.7267586505000001,
        0.7542069485
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7719287382,
      "stddev": 0.04377163502586479,
      "median": 0.7581983085000001,
      "user": 0.38821332,
      "system": 1.3165834200000002,
      "min": 0.7287439845,
      "max": 0.8767625865,
      "times": [
        0.7287439845,
        0.7934248945000001,
        0.8767625865,
        0.7637436965000001,
        0.7501290555000001,
        0.7402394545000001,
        0.7845824325,
        0.7939838985000001,
        0.7350244585000001,
        0.7526529205000001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 9.228 ± 0.039 9.175 9.304 1.81 ± 0.01
pacquet@main 9.260 ± 0.040 9.204 9.332 1.82 ± 0.01
pnpr@HEAD 5.093 ± 0.033 5.040 5.155 1.00
pnpr@main 5.149 ± 0.157 5.030 5.529 1.01 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 9.227795601939999,
      "stddev": 0.0392318016853357,
      "median": 9.21751972354,
      "user": 3.5917806,
      "system": 3.27838048,
      "min": 9.17483847804,
      "max": 9.30446261104,
      "times": [
        9.182860461039999,
        9.30446261104,
        9.21645935304,
        9.21701108704,
        9.24462781804,
        9.21802836004,
        9.23499329204,
        9.27437232204,
        9.17483847804,
        9.21030223704
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 9.259989389540001,
      "stddev": 0.04005112972133877,
      "median": 9.25446358154,
      "user": 3.6714076999999996,
      "system": 3.323112579999999,
      "min": 9.20377104104,
      "max": 9.331754071039999,
      "times": [
        9.331754071039999,
        9.24785498704,
        9.261072176039999,
        9.21228654904,
        9.24448965404,
        9.26504631404,
        9.27269634504,
        9.245556495039999,
        9.20377104104,
        9.31536626304
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 5.09282045224,
      "stddev": 0.03253103977183357,
      "median": 5.090568901539999,
      "user": 2.3624913,
      "system": 2.860212379999999,
      "min": 5.040061586039999,
      "max": 5.15453099104,
      "times": [
        5.11322254904,
        5.098778536039999,
        5.060063621039999,
        5.0859189450399995,
        5.09475005604,
        5.07198576604,
        5.040061586039999,
        5.08638774704,
        5.12250472504,
        5.15453099104
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 5.14899734634,
      "stddev": 0.157044569699118,
      "median": 5.088436894039999,
      "user": 2.3187606,
      "system": 2.881109379999999,
      "min": 5.03029194104,
      "max": 5.5287680320399994,
      "times": [
        5.11791554604,
        5.5287680320399994,
        5.083954290039999,
        5.06502205104,
        5.09401407904,
        5.09291949804,
        5.03029194104,
        5.33180039404,
        5.06377998504,
        5.08150764704
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.414 ± 0.018 1.392 1.449 2.11 ± 0.05
pacquet@main 1.428 ± 0.016 1.409 1.453 2.13 ± 0.05
pnpr@HEAD 0.670 ± 0.013 0.659 0.705 1.00
pnpr@main 0.688 ± 0.064 0.656 0.869 1.03 ± 0.10
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.4136930228600002,
      "stddev": 0.017994889234982908,
      "median": 1.41121714596,
      "user": 1.5722916599999999,
      "system": 1.76230298,
      "min": 1.39162775546,
      "max": 1.44855055646,
      "times": [
        1.44855055646,
        1.42610140446,
        1.4216784254600001,
        1.39162775546,
        1.39821977646,
        1.40858674446,
        1.41384754746,
        1.4014694754600001,
        1.3964006524600001,
        1.43044789046
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.42771714376,
      "stddev": 0.015810408441663322,
      "median": 1.42747882446,
      "user": 1.56412496,
      "system": 1.8130688799999999,
      "min": 1.40858857446,
      "max": 1.45300176546,
      "times": [
        1.42626879646,
        1.45054840746,
        1.40858857446,
        1.42877011646,
        1.41266585146,
        1.43817670346,
        1.40925843546,
        1.45300176546,
        1.42868885246,
        1.42120393446
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6704852725600001,
      "stddev": 0.013208644451633764,
      "median": 0.66806564846,
      "user": 0.34611896,
      "system": 1.25925788,
      "min": 0.6585060114600001,
      "max": 0.70524248046,
      "times": [
        0.70524248046,
        0.6676570244600001,
        0.6585060114600001,
        0.67227541546,
        0.6619455874600001,
        0.6690466404600001,
        0.66504201446,
        0.6750211634600001,
        0.66847427246,
        0.6616421154600001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.68778437326,
      "stddev": 0.06440367503159798,
      "median": 0.66734314896,
      "user": 0.34594006,
      "system": 1.2682856799999997,
      "min": 0.65621164546,
      "max": 0.8694590174600001,
      "times": [
        0.67710093646,
        0.6686736454600001,
        0.66791180746,
        0.8694590174600001,
        0.6650041184600001,
        0.65621164546,
        0.66677449046,
        0.6601421644600001,
        0.6857063764600001,
        0.66085953046
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.995 ± 0.028 4.939 5.026 7.40 ± 0.38
pacquet@main 5.022 ± 0.037 4.988 5.094 7.44 ± 0.38
pnpr@HEAD 0.675 ± 0.034 0.640 0.766 1.00
pnpr@main 0.679 ± 0.025 0.656 0.727 1.01 ± 0.06
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.994590336919999,
      "stddev": 0.027915958970823475,
      "median": 5.00115034122,
      "user": 1.7729421000000003,
      "system": 1.9021667800000004,
      "min": 4.93889329372,
      "max": 5.026225053719999,
      "times": [
        5.021929494719999,
        4.986101284719999,
        4.9845258677199995,
        4.95980292672,
        5.01116636872,
        5.026225053719999,
        5.01495839672,
        4.99970986372,
        5.00259081872,
        4.93889329372
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 5.021765816619999,
      "stddev": 0.0369224457604047,
      "median": 5.00833668872,
      "user": 1.7803071,
      "system": 1.91898788,
      "min": 4.9884126347199995,
      "max": 5.093919155719999,
      "times": [
        4.99410167572,
        4.99672051372,
        4.9886538077199996,
        4.99853325372,
        5.018140123719999,
        5.02751527372,
        5.093919155719999,
        4.9884126347199995,
        5.039171359719999,
        5.0724903677199995
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6746661433200001,
      "stddev": 0.03398111774761516,
      "median": 0.6663847457200001,
      "user": 0.3472266999999999,
      "system": 1.2563653800000003,
      "min": 0.64012304072,
      "max": 0.7660393217200001,
      "times": [
        0.7660393217200001,
        0.6659378717200001,
        0.6757109037200001,
        0.66683161972,
        0.65562074272,
        0.6814573767200001,
        0.6637750707200001,
        0.66866735472,
        0.64012304072,
        0.6624981307200001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6790304167200001,
      "stddev": 0.024763683208613125,
      "median": 0.6726585782200001,
      "user": 0.34082409999999996,
      "system": 1.27460178,
      "min": 0.65571603272,
      "max": 0.7268504597200001,
      "times": [
        0.6779450597200001,
        0.6716171267200001,
        0.7268504597200001,
        0.66269351472,
        0.6737000297200001,
        0.65619845872,
        0.65571603272,
        0.7187724547200001,
        0.66409439672,
        0.68271663372
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12281
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.23 s
(+48.55%)Baseline: 6.21 s
7.45 s
(123.79%)

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,227.80 ms
(+48.55%)Baseline: 6,212.05 ms
7,454.47 ms
(123.79%)

isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
4,994.59 ms
(-0.33%)Baseline: 5,011.36 ms
6,013.63 ms
(83.05%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,413.69 ms
(+1.46%)Baseline: 1,393.41 ms
1,672.09 ms
(84.55%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
9,974.12 ms
(+18.04%)Baseline: 8,450.06 ms
10,140.08 ms
(98.36%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
656.51 ms
(+0.72%)Baseline: 651.85 ms
782.22 ms
(83.93%)
🐰 View full continuous benchmarking report in Bencher

@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 65e5afb

@zkochan zkochan force-pushed the pacquet-lockfile-patched-dependencies branch from 65e5afb to ffaad73 Compare June 9, 2026 06:58
@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 ffaad73

Comment thread pacquet/crates/package-manager/src/install.rs
zkochan added 3 commits June 9, 2026 09:27
pacquet resolved and hashed patches for the depPath `(patch_hash=...)`
suffix and at build time, but never wrote the top-level
`patchedDependencies:` block into `pnpm-lock.yaml`, so a
`pacquet install --lockfile-only` diverged from pnpm (issue item 6 of
#12266).

Add a `patched_dependencies` field to the `Lockfile` struct in its
`sortLockfileKeys` slot (between `pnpmfileChecksum` and `importers`),
populated via a new `Config::patched_dependency_hashes()` that ports
pnpm's `calcPatchHashes(opts.patchedDependencies)`: resolve each patch
path against the workspace dir and hash it, keeping the user's verbatim
keys so a bare `foo` and `foo@*` stay separate lockfile keys rather than
collapsing into one group bucket. The hashes are computed once per
install and threaded through `GraphToLockfileOptions`; the current
lockfile (`lock.yaml`) carries them through.
Now that the lockfile records `patchedDependencies` hashes, the
frozen-lockfile freshness gate must reject an install when those hashes
drift — otherwise editing a patch file would not invalidate the
lockfile even though the patch hash participates in `(patch_hash=...)`
depPath identity.

Port pnpm's `getOutdatedLockfileSetting` patchedDependencies check:
`check_lockfile_settings` now takes the current install's
`patched_dependency_hashes()` and compares it (order-insensitively)
against `lockfile.patched_dependencies`, surfacing a new
`StalenessReason::PatchedDependenciesChanged` between the
`ignoredOptionalDependencies` and settings checks, matching upstream's
order. The pnpr fast-path bails when patches are configured.

Addresses review feedback on the patchedDependencies-block PR.
…all fast path

The optimistic repeat-install fast path skipped the whole pipeline
before `check_lockfile_freshness` ran, and its workspace-state
comparison only checks the `patchedDependencies` key→path map. A patch
file edited in place (same config entry, new contents) therefore slipped
through: `node_modules` and the lockfile's recorded patch hash could stay
stale behind an "Already up to date".

Port the patch-file branch of pnpm's `patchesOrHooksAreModified`: a
configured patch whose mtime is newer than the workspace state's
`lastValidatedTimestamp` invalidates the fast path, checked before the
manifest-mtime exit so the patch reason wins. The pnpmfile branch and
the `assertWantedLockfileUpToDate` re-verification remain unported.

Addresses review feedback on the patchedDependencies-block PR.
@zkochan zkochan force-pushed the pacquet-lockfile-patched-dependencies branch from ffaad73 to a88cc77 Compare June 9, 2026 08:00
@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 a88cc77

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pacquet/crates/package-manager/src/optimistic_repeat_install.rs (1)

452-488: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Doc comment for manifests_unchanged_since is separated from its function.

The doc comment at lines 452-457 describes statting package.json files and references statManifestFile, which clearly documents manifests_unchanged_since (line 490+), not patches_modified_since. Inserting the new function between the doc comment and the function it documents breaks Rust's doc comment attachment rules — doc comments must immediately precede the item they document.

📝 Proposed fix

Move the manifests_unchanged_since doc comment to immediately precede its function:

-/// Stat every project's `package.json` and check that no mtime is
-/// newer than `cutoff_ms`. Any stat failure is treated as "can't
-/// prove freshness, fall through" — matching pnpm's
-/// [`statManifestFile`](https://github.com/pnpm/pnpm/blob/cc4ff817aa/deps/status/src/statManifestFile.ts)
-/// behavior on missing files (it throws, which `checkDepsStatus`
-/// catches via the outer `try`).
 /// Whether any configured patch file's mtime is newer than the last
 /// validation. Mirrors the patch branch of upstream's
 /// [`patchesOrHooksAreModified`](https://github.com/pnpm/pnpm/blob/cc4ff817aa/deps/status/src/checkDepsStatus.ts#L604-L613):
 /// `allPatchStats.some(patch => patch && patch.mtime > lastValidatedTimestamp)`.
 /// A patch that can't be stat'd is treated as not-modified — pnpm's
 /// `safeStat` returns null and the `patch &&` guard drops it, leaving a
 /// genuinely missing patch to surface on the full install path. Patch
 /// paths are resolved against `workspace_root` (the `pnpm-workspace.yaml`
 /// dir, where `patchedDependencies` is declared), matching how
 /// [`Config::patched_dependency_hashes`] resolves them.
 fn patches_modified_since(workspace_root: &Path, config: &Config, cutoff_ms: i64) -> bool {
     // ... body ...
 }
 
+/// Stat every project's `package.json` and check that no mtime is
+/// newer than `cutoff_ms`. Any stat failure is treated as "can't
+/// prove freshness, fall through" — matching pnpm's
+/// [`statManifestFile`](https://github.com/pnpm/pnpm/blob/cc4ff817aa/deps/status/src/statManifestFile.ts)
+/// behavior on missing files (it throws, which `checkDepsStatus`
+/// catches via the outer `try`).
 fn manifests_unchanged_since(
     cutoff_ms: i64,
     project_manifests: &[(PathBuf, &PackageManifest)],
 ) -> bool {
🤖 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/package-manager/src/optimistic_repeat_install.rs` around lines
452 - 488, The doc comment describing statting package.json and referencing
statManifestFile belongs to manifests_unchanged_since, but it was left before
patches_modified_since; move that entire doc comment block so it immediately
precedes the manifests_unchanged_since function declaration (remove it from
above patches_modified_since), ensuring there are no blank lines or other items
between the doc comment and the manifests_unchanged_since function so Rust will
attach the docs correctly; keep the patches_modified_since doc comment (if any)
with its function.
🤖 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/lockfile/src/lib.rs`:
- Around line 167-182: The top-level struct field order is wrong:
`patched_dependencies` must be declared before `ignored_optional_dependencies`
so YAML produced by `serialize_yaml` matches pnpm's root-key order
(pnpmfileChecksum → patchedDependencies → importers); `serialize_yaml` only
canonicalizes map entries not struct field order, so fix this by moving the `pub
patched_dependencies: Option<std::collections::BTreeMap<String, String>>`
declaration to appear before `pub ignored_optional_dependencies` in the lockfile
struct (and update any comments/tests referencing field order accordingly).

---

Outside diff comments:
In `@pacquet/crates/package-manager/src/optimistic_repeat_install.rs`:
- Around line 452-488: The doc comment describing statting package.json and
referencing statManifestFile belongs to manifests_unchanged_since, but it was
left before patches_modified_since; move that entire doc comment block so it
immediately precedes the manifests_unchanged_since function declaration (remove
it from above patches_modified_since), ensuring there are no blank lines or
other items between the doc comment and the manifests_unchanged_since function
so Rust will attach the docs correctly; keep the patches_modified_since doc
comment (if any) with its function.
🪄 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: 8e38e778-3a2e-452c-8148-cd6189ff5a74

📥 Commits

Reviewing files that changed from the base of the PR and between ffaad73 and a88cc77.

📒 Files selected for processing (18)
  • pacquet/crates/config/src/lib.rs
  • pacquet/crates/config/src/tests.rs
  • pacquet/crates/lockfile/src/freshness.rs
  • pacquet/crates/lockfile/src/freshness/tests.rs
  • pacquet/crates/lockfile/src/lib.rs
  • pacquet/crates/lockfile/src/save_lockfile/tests.rs
  • pacquet/crates/package-manager/src/current_lockfile.rs
  • pacquet/crates/package-manager/src/current_lockfile/tests.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
  • pacquet/crates/package-manager/src/hoisted_dep_graph/tests.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/package-manager/src/optimistic_repeat_install.rs
  • pacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs
  • pacquet/crates/real-hoist/src/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse/tests.rs
  • pnpr/crates/pnpr/src/resolver/resolve.rs
✅ Files skipped from review due to trivial changes (2)
  • pacquet/crates/package-manager/src/hoisted_dep_graph/tests.rs
  • pacquet/crates/real-hoist/src/tests.rs
🚧 Files skipped from review as they are similar to previous changes (12)
  • pacquet/crates/resolving-deps-resolver/src/lockfile_reuse/tests.rs
  • pnpr/crates/pnpr/src/resolver/resolve.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile.rs
  • pacquet/crates/config/src/tests.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/dependencies_graph_to_lockfile/tests.rs
  • pacquet/crates/lockfile/src/save_lockfile/tests.rs
  • pacquet/crates/package-manager/src/current_lockfile.rs
  • pacquet/crates/lockfile/src/freshness.rs
  • pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs
  • pacquet/crates/lockfile/src/freshness/tests.rs
  • pacquet/crates/config/src/lib.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: ubuntu-latest / Node.js 24 / Test
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-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 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/current_lockfile/tests.rs
  • pacquet/crates/package-manager/src/optimistic_repeat_install.rs
  • pacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs
  • pacquet/crates/lockfile/src/lib.rs
🧠 Learnings (5)
📚 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/current_lockfile/tests.rs
  • pacquet/crates/package-manager/src/optimistic_repeat_install.rs
  • pacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs
  • pacquet/crates/lockfile/src/lib.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/current_lockfile/tests.rs
  • pacquet/crates/package-manager/src/optimistic_repeat_install.rs
  • pacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs
  • pacquet/crates/lockfile/src/lib.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/current_lockfile/tests.rs
  • pacquet/crates/package-manager/src/optimistic_repeat_install.rs
  • pacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs
  • pacquet/crates/lockfile/src/lib.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/current_lockfile/tests.rs
  • pacquet/crates/package-manager/src/optimistic_repeat_install.rs
  • pacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs
  • pacquet/crates/lockfile/src/lib.rs
📚 Learning: 2026-05-20T23:23:40.606Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11788
File: pacquet/crates/lockfile/src/snapshot_dep_ref.rs:52-56
Timestamp: 2026-05-20T23:23:40.606Z
Learning: In the pacquet lockfile crate, the link target is intentionally represented as a raw `Link(String)` (not a `LinkTarget` newtype) for both `SnapshotDepRef::Link` and `ImporterDepVersion::Link`. pnpm upstream preserves `link:` targets verbatim with only a prefix check (e.g., `refToRelative`), so do not suggest introducing a `LinkTarget` newtype in a bug-fix/targeted PR. If type branding is ever desired, it should be done together for both variants in a separate dedicated refactor PR rather than as part of a small fix.

Applied to files:

  • pacquet/crates/lockfile/src/lib.rs
🔇 Additional comments (7)
pacquet/crates/lockfile/src/lib.rs (1)

140-152: LGTM!

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

54-68: LGTM!

pacquet/crates/package-manager/src/optimistic_repeat_install.rs (3)

18-31: LGTM!


157-165: LGTM!


468-488: LGTM!

pacquet/crates/package-manager/src/optimistic_repeat_install/tests.rs (2)

628-674: LGTM!


676-720: LGTM!

Comment thread pacquet/crates/lockfile/src/lib.rs
@zkochan zkochan merged commit 8c9edf2 into main Jun 9, 2026
28 checks passed
@zkochan zkochan deleted the pacquet-lockfile-patched-dependencies branch June 9, 2026 10:01
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