feat: apply patchedDependencies before postinstall (#397 item 9)#427
Conversation
Completes upstream's `patchedDependencies` support: pacquet now applies configured patches to extracted package directories before postinstall hooks run, includes patched-only packages in the build trigger, and surfaces the four upstream diagnostic codes. ## Pure-Rust patch applier New `pacquet_patching::apply_patch_to_dir` ports upstream's [`applyPatchToDir`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/patching/apply-patch/src/index.ts) using the [`diffy`](https://crates.io/crates/diffy) crate (MIT OR Apache-2.0, pure Rust, no_std-capable, no subprocess). Upstream's own comment notes that `patch` is unavailable on Windows and `git apply` is awkward inside an existing git repo — pnpm vendored `@pnpm/patch-package` for that reason; pacquet sidesteps it the same way with an in-process applier. Supported `FileOperation`s: `Modify`, `Create`, `Delete`. `Rename` and `Copy` fall through to `ERR_PNPM_PATCH_FAILED` with a descriptive message — they don't appear in `patch-package`-style patches in practice. New `PatchApplyError` enum surfaces the upstream diagnostic codes: - `ERR_PNPM_PATCH_NOT_FOUND` — patch file missing - `ERR_PNPM_INVALID_PATCH` — parse error - `ERR_PNPM_PATCH_FAILED` — hunk wouldn't apply, target missing, IO error on a target path ## Build pipeline - `build_sequence::get_subgraph_to_build` now considers `patch.is_some()` alongside `requires_build`. A patched-only package becomes a build candidate the same way upstream's `node.requiresBuild || node.patch != null` filter at [`buildSequence.ts:47,60`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/buildSequence.ts#L40-L67) treats them. - `BuildModules::run` per-snapshot loop refactored: - Inner build trigger: `requires_build || patch.is_some()`. - `allowBuilds` check only gates scripts (mirrors upstream's `if (node.requiresBuild)` at [`during-install/src/index.ts:88-101`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/index.ts#L82-L106)). Disallow / ignored sets `should_run_scripts = false` rather than `continue` — patches still apply. - `cache_key`'s `include_dep_graph_hash` is now `should_run_scripts` rather than unconditionally `true`, so a patched-only snapshot's cache key omits the deps-hash, matching upstream's `includeDepGraphHash: hasSideEffects` at [`during-install/src/index.ts:202`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/index.ts#L199-L204). - Patch application happens before `run_postinstall_hooks`, mirroring [`during-install/src/index.ts:171-178`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/index.ts#L168-L191). `is_patched` tracks whether the call ran. - Cache-write gate becomes `(is_patched || has_side_effects) && side_effects_cache_write`, mirroring upstream's `(isPatched || hasSideEffects) && opts.sideEffectsCacheWrite` at line 199. ## Diagnostics New `BuildModulesError::PatchFilePathMissing` surfaces `ERR_PNPM_PATCH_FILE_PATH_MISSING` when a snapshot's resolved `ExtendedPatchInfo.patch_file_path` is `None` (mirrors [`during-install/src/index.ts:172-176`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/index.ts#L172-L176)). New `BuildModulesError::PatchApply` wraps the four `PatchApplyError` variants from the patching crate. ## Dependency New workspace dep: `diffy = "0.5.0"` (MIT OR Apache-2.0). Used by `pacquet-patching` only. ## Tests - `pacquet-patching::apply::tests` (8 cases): happy-path Modify against the upstream `is-positive@1.0.0` body, Create, Delete, the three error variants, and a "missing target file errors PATCH_FAILED" case. - `patch_only_snapshot_gets_patched_via_build_modules` drives `BuildModules` with a patched no-scripts snapshot end-to-end and verifies the patch file lands on disk. - `missing_patch_file_path_errors_with_diagnostic` verifies `ERR_PNPM_PATCH_FILE_PATH_MISSING` propagates. - Updated `write_path_cache_key_includes_patch_hash` (from item 9 slices A+B) to provide a real patch file the applier can succeed against. `just ready` (575 tests), `just dylint`, `cargo doc -D warnings --document-private-items`, `taplo format --check` all clean. `cargo deny check licenses` ok. --- Written by an agent (Claude Code, claude-opus-4-7).
📝 WalkthroughWalkthroughAdds an in-process unified-diff applier (diffy), exposes apply_patch_to_dir/PatchApplyError, threads optional patch metadata into build_sequence, and makes build_modules apply patches before postinstall hooks while adjusting candidacy, cache-key composition, and side-effects-cache write gating. ChangesPatch application implementation and build integration
Sequence DiagramsequenceDiagram
participant Chunk as Build chunk
participant BM as BuildModules
participant Apply as apply_patch_to_dir
participant Hooks as Postinstall hooks
Chunk->>BM: package snapshot
BM->>BM: check requires_build OR has_patch
alt has_patch
BM->>Apply: apply patch
Apply-->>BM: is_patched=true
end
alt should_run_scripts
BM->>Hooks: run postinstall
Hooks-->>BM: has_side_effects
end
BM->>BM: update cache if is_patched OR has_side_effects
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #427 +/- ##
==========================================
+ Coverage 86.68% 86.71% +0.03%
==========================================
Files 91 92 +1
Lines 6344 6481 +137
==========================================
+ Hits 5499 5620 +121
- Misses 845 861 +16 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Micro-Benchmark ResultsLinux |
There was a problem hiding this comment.
Pull request overview
Implements the remaining patchedDependencies install pipeline behavior by applying patches onto extracted package directories before postinstall hooks run, and by treating “patched-only” packages as build candidates (matching pnpm’s during-install flow).
Changes:
- Add a pure-Rust unified-diff patch applier (
apply_patch_to_dir) usingdiffy, plus unit tests for modify/create/delete and error diagnostics. - Update build sequencing to include patched-only snapshots (
requires_build || patch.is_some()via the patches map). - Refactor
BuildModulesto (1) apply patches before scripts, (2) gate allowBuilds only for scripts, and (3) align side-effects cache key/write gating with upstream.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/patching/src/verify/tests.rs | Minor test doc comment cleanup. |
| crates/patching/src/resolve/tests.rs | Minor test doc comment cleanup. |
| crates/patching/src/resolve.rs | Update documentation comment reference. |
| crates/patching/src/lib.rs | Update crate docs and re-export new patch-apply API; add apply module. |
| crates/patching/src/apply.rs | New patch application implementation and diagnostics. |
| crates/patching/src/apply/tests.rs | New unit tests for patch application behavior and error cases. |
| crates/patching/Cargo.toml | Add diffy dependency for patch application. |
| crates/package-manager/src/build_sequence.rs | Treat patched snapshots as build candidates in subgraph selection. |
| crates/package-manager/src/build_sequence/tests.rs | Update tests for new build_sequence signature (patches parameter). |
| crates/package-manager/src/build_modules.rs | Apply patches before postinstall; adjust allowBuilds gating and cache-key/write gating. |
| crates/package-manager/src/build_modules/tests.rs | Update cache-key test to use a real patch file; add tests for patch-only snapshot + missing patch path diagnostic. |
| Cargo.toml | Add workspace dependency on diffy. |
| Cargo.lock | Lockfile updates for diffy and transitive dependency changes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
crates/patching/src/apply.rs (1)
10-10: ⚡ Quick winSwitch upstream references to
blob/mainlinks.The changed porting docs use SHA-permalink URLs; use
blob/mainURLs so references track current upstream behavior.As per coding guidelines: “open files from
https://github.com/pnpm/pnpm/blob/main/...rather than from a permalinked SHA.”Also applies to: 53-53
🤖 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 `@crates/patching/src/apply.rs` at line 10, The doc comment referencing the upstream pnpm file uses a SHA-permalink; update the URL to the non-permalink path so it tracks the main branch (use https://github.com/pnpm/pnpm/blob/main/...), replacing the current SHA-based link in the comment that references applyPatchToDir and any other similar occurrences (e.g., the second occurrence around the symbol or comment near line 53) so both links point to blob/main instead of the SHA.crates/package-manager/src/build_sequence/tests.rs (1)
64-64: ⚡ Quick winAdd one test that passes
Some(patches)to cover the new branch.Right now this file only exercises the
Nonepath; a patched-only node scenario would protect the new build trigger from regressions.Also applies to: 78-78, 95-95, 114-114, 139-139, 168-168
🤖 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 `@crates/package-manager/src/build_sequence/tests.rs` at line 64, Add tests that call build_sequence with Some(patches) instead of None to exercise the new patched-only branch: construct a patches HashMap (keys matching package/node ids used in the existing tests) and pass Some(patches) as the second argument to build_sequence in the same test cases that currently call build_sequence(&HashMap::new(), None, ...). Update the tests at the same locations (currently calling build_sequence with None) to include at least one scenario where patches contains entries for a node that has no other inputs so the patched-only build trigger path in build_sequence is executed; reuse existing test names and assertions but assert the expected behavior when patches are present. Ensure you use the same function name build_sequence to locate the code and mirror the existing test structure.crates/package-manager/src/build_sequence.rs (1)
25-30: ⚡ Quick winUse
blob/mainupstream links in the porting docs.These changed comments still point to SHA-permalink URLs; switch them to
https://github.com/pnpm/pnpm/blob/main/...so references stay aligned with upstream tip.As per coding guidelines: “When porting features... open files from
https://github.com/pnpm/pnpm/blob/main/...rather than from a permalinked SHA.”Also applies to: 146-147
🤖 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 `@crates/package-manager/src/build_sequence.rs` around lines 25 - 30, The comment in build_sequence.rs currently links upstream using a SHA-permalink; update the URL(s) that reference getSubgraphToBuild (and the other link at lines 146-147) to use the moving tip form (https://github.com/pnpm/pnpm/blob/main/...) instead of the SHA-based permalink so the porting docs follow the guideline; locate the comment block referencing getSubgraphToBuild and any other upstream links in this file and replace the permalinked paths with blob/main equivalents.crates/package-manager/src/build_modules/tests.rs (1)
1258-1336: ⚡ Quick winAdd a cache-enabled variant of the patch-only snapshot test.
This test currently runs with cache disabled and no engine name, so it doesn’t exercise the patched-only cache-key path. Adding one variant with cache inputs enabled would close that regression gap.
🤖 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 `@crates/package-manager/src/build_modules/tests.rs` around lines 1258 - 1336, Add a second test variant that exercises the patched-only cache-key path by duplicating patch_only_snapshot_gets_patched_via_build_modules but enabling cache inputs: set BuildModules.side_effects_cache = true and side_effects_cache_write = true and provide an engine_name (e.g., Some("test-engine")), while keeping store_dir/store_index_writer/patches the same; ensure the test uses the same setup (virtual_store_dir, pkg_dir, patch file, patches map) and asserts the patched file was created, so the cache-enabled code path is executed and covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/patching/src/apply.rs`:
- Around line 127-166: The code currently joins patch paths into patched_dir in
the FileOperation::Modify, Create and Delete arms (variables modified, path ->
target) without validation, so paths with absolute components or ".." can escape
patched_dir; add a path-sanitization check before constructing/using target:
reject any path where path.as_ref().is_absolute() or path.as_ref().components()
contains Component::ParentDir (and on Windows reject Component::Prefix),
returning an error (e.g. failed("invalid path")) and only proceed to build
patched_dir.join(...) and perform fs::read_to_string, diffy::apply, fs::write or
fs::remove_file after the check; apply the same validation for both modified (in
FileOperation::Modify) and path (in Create/Delete) so no patch operation can
reference files outside patched_dir.
---
Nitpick comments:
In `@crates/package-manager/src/build_modules/tests.rs`:
- Around line 1258-1336: Add a second test variant that exercises the
patched-only cache-key path by duplicating
patch_only_snapshot_gets_patched_via_build_modules but enabling cache inputs:
set BuildModules.side_effects_cache = true and side_effects_cache_write = true
and provide an engine_name (e.g., Some("test-engine")), while keeping
store_dir/store_index_writer/patches the same; ensure the test uses the same
setup (virtual_store_dir, pkg_dir, patch file, patches map) and asserts the
patched file was created, so the cache-enabled code path is executed and
covered.
In `@crates/package-manager/src/build_sequence.rs`:
- Around line 25-30: The comment in build_sequence.rs currently links upstream
using a SHA-permalink; update the URL(s) that reference getSubgraphToBuild (and
the other link at lines 146-147) to use the moving tip form
(https://github.com/pnpm/pnpm/blob/main/...) instead of the SHA-based permalink
so the porting docs follow the guideline; locate the comment block referencing
getSubgraphToBuild and any other upstream links in this file and replace the
permalinked paths with blob/main equivalents.
In `@crates/package-manager/src/build_sequence/tests.rs`:
- Line 64: Add tests that call build_sequence with Some(patches) instead of None
to exercise the new patched-only branch: construct a patches HashMap (keys
matching package/node ids used in the existing tests) and pass Some(patches) as
the second argument to build_sequence in the same test cases that currently call
build_sequence(&HashMap::new(), None, ...). Update the tests at the same
locations (currently calling build_sequence with None) to include at least one
scenario where patches contains entries for a node that has no other inputs so
the patched-only build trigger path in build_sequence is executed; reuse
existing test names and assertions but assert the expected behavior when patches
are present. Ensure you use the same function name build_sequence to locate the
code and mirror the existing test structure.
In `@crates/patching/src/apply.rs`:
- Line 10: The doc comment referencing the upstream pnpm file uses a
SHA-permalink; update the URL to the non-permalink path so it tracks the main
branch (use https://github.com/pnpm/pnpm/blob/main/...), replacing the current
SHA-based link in the comment that references applyPatchToDir and any other
similar occurrences (e.g., the second occurrence around the symbol or comment
near line 53) so both links point to blob/main instead of the SHA.
🪄 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: ec66e452-e31e-46aa-bf35-f0ba54da3cc8
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (12)
Cargo.tomlcrates/package-manager/src/build_modules.rscrates/package-manager/src/build_modules/tests.rscrates/package-manager/src/build_sequence.rscrates/package-manager/src/build_sequence/tests.rscrates/patching/Cargo.tomlcrates/patching/src/apply.rscrates/patching/src/apply/tests.rscrates/patching/src/lib.rscrates/patching/src/resolve.rscrates/patching/src/resolve/tests.rscrates/patching/src/verify/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Agent
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs
📄 CodeRabbit inference engine (AGENTS.md)
**/*.rs: Preserve existing method chains andpipe-traitchains; do not break them into intermediateletbindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Pathover&PathBuf,&strover&String) when it does not force extra copies.
PreferArc::clone(&x)andRc::clone(&x)overx.clone()for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;for any glob whose target is a module you control. External-crate preludes (e.g.,use rayon::prelude::*;) and root-of-module re-exports (e.g.,pub use submodule::*;inlib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plainStringor&str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only viaTryFrom<String>and/orFromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallibleFrom<String>andFrom<&str>constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bareasassertion), add afrom_str_unchecked(or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...
Files:
crates/patching/src/resolve.rscrates/patching/src/lib.rscrates/patching/src/resolve/tests.rscrates/package-manager/src/build_sequence.rscrates/patching/src/verify/tests.rscrates/patching/src/apply/tests.rscrates/package-manager/src/build_sequence/tests.rscrates/patching/src/apply.rscrates/package-manager/src/build_modules.rscrates/package-manager/src/build_modules/tests.rs
🧠 Learnings (2)
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.
Applied to files:
crates/patching/src/resolve.rscrates/patching/src/lib.rscrates/patching/src/resolve/tests.rscrates/package-manager/src/build_sequence.rscrates/patching/src/verify/tests.rscrates/patching/src/apply/tests.rscrates/package-manager/src/build_sequence/tests.rscrates/patching/src/apply.rscrates/package-manager/src/build_modules.rscrates/package-manager/src/build_modules/tests.rs
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.
Applied to files:
crates/patching/src/resolve/tests.rscrates/patching/src/verify/tests.rscrates/patching/src/apply/tests.rscrates/package-manager/src/build_sequence/tests.rscrates/package-manager/src/build_modules/tests.rs
🔇 Additional comments (11)
crates/patching/src/resolve.rs (1)
31-43: Looks good.This comment update is clear and keeps the ordering rationale explicit.
crates/patching/src/verify/tests.rs (1)
16-27: Looks good.The updated wording keeps the regression intent precise.
crates/patching/src/resolve/tests.rs (1)
103-107: Looks good.The contract note remains accurate and aligned with the test body.
Cargo.toml (1)
45-45: Looks good.Adding
diffyat the workspace level is consistent with centralized dependency management.crates/patching/Cargo.toml (1)
15-15: Looks good.Using the workspace-pinned
diffyhere is the right integration pattern.crates/package-manager/src/build_sequence.rs (1)
183-185: Patch-aware build candidacy is correctly integrated.Including
has_patchalongsiderequires_buildhere matches the intended behavior.crates/patching/src/lib.rs (1)
46-53: Public API exposure looks correct for downstream integration.Re-exporting
apply_patch_to_dirandPatchApplyErrorat the crate root cleanly matches the new build-modules wiring and diagnostics surface.crates/patching/src/apply/tests.rs (1)
58-177: Great test coverage for the new patch applier paths.This suite exercises happy-path apply plus the core failure diagnostics and file operations (modify/create/delete) in a way that should catch regressions early.
crates/package-manager/src/build_modules.rs (2)
392-415: Patch application ordering is correct.Applying the patch before postinstall hooks and surfacing
PatchFilePathMissing/PatchApplydirectly in this flow matches the intended upstream behavior.
248-257: The original concern does not represent an actual problem in the code.The design automatically prevents the hypothetical issue raised. At line 313,
should_run_scriptsis initialized torequires_buildand only updated withinif requires_build { ... }. For patched-only snapshots (whererequires_build=falseandhas_patch=true),should_run_scriptsremainsfalse.At line 349,
include_dep_graph_hashis set toshould_run_scripts. Wheninclude_dep_graph_hash=false(patched-only case),calc_dep_statenever invokescalc_dep_graph_hashand therefore never attempts to access the boundeddep_graph. The bounded graph (rooted atrequires_buildnodes only) is only consulted whenshould_run_scripts=true, which only occurs for nodes that are actually in the graph.> Likely an incorrect or invalid review comment.crates/package-manager/src/build_modules/tests.rs (1)
1346-1404: Nice diagnostic-path coverage for missing patch file path.This test cleanly validates the
PatchFilePathMissingbranch and keeps the new error contract pinned.
Integrated-Benchmark Report (Linux)Scenario: Frozen Lockfile
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 3.664882128880001,
"stddev": 0.9048358852973348,
"median": 3.4399575257799997,
"user": 1.9972860799999999,
"system": 2.6404005999999995,
"min": 2.7075113157799997,
"max": 5.67689092078,
"times": [
4.77583810478,
3.4613261137799998,
3.8032086327799997,
3.11998000478,
5.67689092078,
3.24254942578,
3.52132897978,
3.41858893778,
2.92159885278,
2.7075113157799997
]
},
{
"command": "pacquet@main",
"mean": 3.3664580158799993,
"stddev": 0.3500405740722681,
"median": 3.5008425597799997,
"user": 1.9815288800000002,
"system": 2.6223821,
"min": 2.7521231557799997,
"max": 3.79465069978,
"times": [
2.7521231557799997,
3.4942538007799997,
2.9216837277799996,
3.0339659427799996,
3.3305212847799996,
3.50843019478,
3.5074313187799997,
3.6978010217799997,
3.62371901178,
3.79465069978
]
},
{
"command": "pnpm",
"mean": 5.67480259078,
"stddev": 0.40364366293803244,
"median": 5.55619796478,
"user": 6.77015288,
"system": 3.3212460999999998,
"min": 5.2585748487799995,
"max": 6.59812904278,
"times": [
6.59812904278,
5.2585748487799995,
5.53248394078,
5.57991198878,
5.368530671779999,
5.52760202278,
5.86639575278,
5.62714671078,
5.33679704578,
6.05245388278
]
}
]
}Scenario: Frozen Lockfile (Hot Cache)
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.16234781366,
"stddev": 0.2575188742253623,
"median": 1.12173603186,
"user": 0.2715786,
"system": 1.15910562,
"min": 0.90410221036,
"max": 1.6679770443600002,
"times": [
1.6679770443600002,
1.1623153883600001,
0.91414503436,
1.13339924436,
0.95419895336,
0.9646421323600001,
1.11007281936,
1.3436008643600001,
1.46902444536,
0.90410221036
]
},
{
"command": "pacquet@main",
"mean": 1.59453764006,
"stddev": 0.9394568135637246,
"median": 1.1121770128600001,
"user": 0.26846509999999996,
"system": 1.17981522,
"min": 0.84442956536,
"max": 3.43526917036,
"times": [
3.43526917036,
2.5303406003599997,
2.7692879753599997,
1.1347524503600002,
1.13471090836,
0.9473143593600001,
0.84442956536,
1.08964311736,
1.0614916353600001,
0.99813661836
]
},
{
"command": "pnpm",
"mean": 3.64565535396,
"stddev": 1.206913936907984,
"median": 3.49074607836,
"user": 2.310307,
"system": 1.6488134200000002,
"min": 2.42710232436,
"max": 6.614476612360001,
"times": [
3.31757593536,
3.73579712936,
3.1173562263599996,
2.91748089136,
3.66391622136,
2.47150238636,
2.42710232436,
6.614476612360001,
4.32139419836,
3.86995161436
]
}
]
} |
…tale-delete Three review-flagged issues on #427: 1. **Path escape (CodeRabbit Critical + Copilot)** — patch-controlled paths were joined onto `patched_dir` without validation, so an `a/../../outside` header could read/write/delete outside the package directory. Added `resolve_target(rel)` that rejects absolute paths and any path with `ParentDir`, `RootDir`, or `Prefix` components, surfacing as `ERR_PNPM_PATCH_FAILED`. Applied to `Modify`, `Create`, and `Delete`. 2. **Create overwriting existing (Copilot)** — `FileOperation::Create` wrote unconditionally, silently clobbering an existing target. Matches `patch`'s and `git apply`'s footgun. Now checks `target.try_exists()` and fails with `ERR_PNPM_PATCH_FAILED` ("target already exists") before writing. 3. **Delete unlinking without hunk validation (Copilot)** — `FileOperation::Delete` just `fs::remove_file`-d the target, so a stale or wrong-target patch would silently delete the wrong file. Now reads the existing file, runs `diffy::apply`, and only unlinks when the result is empty (every hunk matched). Apply failures and non-empty results both surface as `ERR_PNPM_PATCH_FAILED`. Five new tests: - `parent_dir_segment_in_modify_errors` - `parent_dir_segment_in_create_errors` - `parent_dir_segment_in_delete_errors` - `create_on_existing_file_errors` — and verifies original contents are untouched. - `delete_with_mismatching_hunks_errors_without_unlinking` — and verifies the file remains on disk. 580 tests pass; `just ready`, `just dylint`, `cargo doc -D warnings --document-private-items`, `taplo format --check` all clean. --- Written by an agent (Claude Code, claude-opus-4-7).
…target `cargo fmt` collapsed my multi-line `format!(...)` call onto a single line in apply.rs:149 but left the trailing comma in place. `perfectionist::macro-trailing-comma` flags trailing commas on single-line macro calls, and CI's Dylint job ran the workspace under `-D warnings`, so the build failed on bd93ad8. Drop the comma.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
crates/patching/src/apply.rs:203
Deletealso usesfs::read_to_string, so it will fail with InvalidData on non-UTF8 files even if the patch would otherwise apply. If the intent is to mirror Node/patch-package behavior, decode file contents lossily (bytes +from_utf8_lossy) to keep patching robust and consistent with upstream semantics.
// diffy::apply on a delete patch produces the empty
// string when every hunk matches.
let original = fs::read_to_string(&target)
.map_err(|source| failed(format!("read {}: {source}", target.display())))?;
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/patching/src/apply/tests.rs (1)
160-223: ⚡ Quick winAssert that failed patch applications leave the filesystem untouched.
These tests prove the error path, but not the safety property. A regression could still create/modify an escaped
../...target or rewriteto-delete.txtbefore returningPatchFailed, and this suite would still pass. Please add postconditions that the escaped path is absent and that the stale-delete case preserves the original file contents.Suggested assertions
fn parent_dir_segment_in_create_errors() { let patched = tempdir().unwrap(); + let escaped = patched.path().parent().unwrap().join("planted.txt"); let patch_dir = tempdir().unwrap(); let patch = write_patch( patch_dir.path(), @@ let PatchApplyError::PatchFailed { message, .. } = err else { panic!("expected PatchFailed, got: {err:?}"); }; assert!(message.contains("escapes target dir"), "got: {message}"); + assert!(!escaped.exists(), "escape path must stay absent: {}", escaped.display()); } @@ fn delete_with_mismatching_hunks_errors_without_unlinking() { @@ let err = apply_patch_to_dir(patched.path(), &patch).expect_err("must refuse mismatch"); assert!(matches!(err, PatchApplyError::PatchFailed { .. }), "got: {err:?}"); assert!(target.exists(), "file must NOT be unlinked when the patch doesn't match"); + assert_eq!(fs::read_to_string(&target).unwrap(), "actually different content\n"); }Also applies to: 259-280
🤖 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 `@crates/patching/src/apply/tests.rs` around lines 160 - 223, Add postcondition assertions to each test (parent_dir_segment_in_modify_errors, parent_dir_segment_in_create_errors, parent_dir_segment_in_delete_errors) to ensure failed patch applications leave the filesystem untouched: after calling apply_patch_to_dir and asserting PatchFailed, verify that any escaped target path (e.g., patched.path().join("../escape.txt"), "../planted.txt", "../victim.txt" relative to patch_dir/patched) does not exist or was not created, and for the delete test confirm the original file contents remain unchanged (read the original file in patched before applying and assert its contents are equal afterwards). Use the existing tempdir(), patched.path(), patch_dir.path(), write_patch, and apply_patch_to_dir helpers to locate files and perform the existence/content checks.
🤖 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.
Nitpick comments:
In `@crates/patching/src/apply/tests.rs`:
- Around line 160-223: Add postcondition assertions to each test
(parent_dir_segment_in_modify_errors, parent_dir_segment_in_create_errors,
parent_dir_segment_in_delete_errors) to ensure failed patch applications leave
the filesystem untouched: after calling apply_patch_to_dir and asserting
PatchFailed, verify that any escaped target path (e.g.,
patched.path().join("../escape.txt"), "../planted.txt", "../victim.txt" relative
to patch_dir/patched) does not exist or was not created, and for the delete test
confirm the original file contents remain unchanged (read the original file in
patched before applying and assert its contents are equal afterwards). Use the
existing tempdir(), patched.path(), patch_dir.path(), write_patch, and
apply_patch_to_dir helpers to locate files and perform the existence/content
checks.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8f9613f8-8e94-4e57-8697-2d6711ed0d50
📒 Files selected for processing (2)
crates/patching/src/apply.rscrates/patching/src/apply/tests.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- crates/patching/src/apply.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Lint and Test (ubuntu-latest)
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Code Coverage
- GitHub Check: Agent
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs
📄 CodeRabbit inference engine (AGENTS.md)
**/*.rs: Preserve existing method chains andpipe-traitchains; do not break them into intermediateletbindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Pathover&PathBuf,&strover&String) when it does not force extra copies.
PreferArc::clone(&x)andRc::clone(&x)overx.clone()for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;for any glob whose target is a module you control. External-crate preludes (e.g.,use rayon::prelude::*;) and root-of-module re-exports (e.g.,pub use submodule::*;inlib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plainStringor&str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only viaTryFrom<String>and/orFromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallibleFrom<String>andFrom<&str>constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bareasassertion), add afrom_str_unchecked(or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...
Files:
crates/patching/src/apply/tests.rs
🧠 Learnings (2)
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.
Applied to files:
crates/patching/src/apply/tests.rs
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.
Applied to files:
crates/patching/src/apply/tests.rs
…ild_sequence patches doc Two Copilot comments on #427: 1. **apply.rs:158 and :200** — `Modify` and `Delete` were reading the existing target file with `fs::read_to_string`, which surfaces invalid UTF-8 as `InvalidData`. Upstream / Node uses `fs.readFile(..., 'utf8')` which replaces invalid bytes with U+FFFD and never throws, so the patch-file reader's lossy decoding already does the right thing; the target-file reads here didn't. Switched to `fs::read` + `String::from_utf8_lossy` for parity. New test `modify_target_with_invalid_utf8_bytes_does_not_error` pins the behavior — patch context is the three U+FFFD chars themselves, so the patch applies cleanly against the lossy-decoded target. 2. **build_sequence.rs:29** — Doc described `patches` as a resolved `PatchGroupRecord`, but the parameter is the per-snapshot lookup map (`Option<&HashMap<PackageKey, ExtendedPatchInfo>>`) built by `InstallFrozenLockfile::run` from `resolve_and_group` + `get_patch_info`. Rewrote the doc to match the actual shape. 581 tests pass; `just ready`, `just dylint`, `cargo doc -D warnings --document-private-items`, `taplo format --check` all clean. --- Written by an agent (Claude Code, claude-opus-4-7).
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/package-manager/src/build_sequence.rs (1)
35-35: ⚡ Quick winUse
blob/mainlinks in upstream references (not pinned SHAs).Line 35 and Line 151 currently point to permalinked SHAs. Please switch these to
https://github.com/pnpm/pnpm/blob/main/...to follow repo guidance for upstream-port references.Suggested doc-link update
-/// [`getSubgraphToBuild`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/buildSequence.ts#L40-L67). +/// [`getSubgraphToBuild`](https://github.com/pnpm/pnpm/blob/main/building/during-install/src/buildSequence.ts#L40-L67). ... -/// `https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/buildSequence.ts`. +/// `https://github.com/pnpm/pnpm/blob/main/building/during-install/src/buildSequence.ts`.As per coding guidelines: “If reading on GitHub, open files from
https://github.com/pnpm/pnpm/blob/main/...rather than from a permalinked SHA.”Also applies to: 151-151
🤖 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 `@crates/package-manager/src/build_sequence.rs` at line 35, Update the upstream GitHub links that currently use permalinked SHAs to use the blob/main form; specifically, in the doc comments referencing getSubgraphToBuild and the other upstream reference (the two occurrences in this file), replace URLs like https://github.com/pnpm/pnpm/blob/<sha>/... with https://github.com/pnpm/pnpm/blob/main/... so the links point to blob/main instead of a pinned commit.
🤖 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.
Nitpick comments:
In `@crates/package-manager/src/build_sequence.rs`:
- Line 35: Update the upstream GitHub links that currently use permalinked SHAs
to use the blob/main form; specifically, in the doc comments referencing
getSubgraphToBuild and the other upstream reference (the two occurrences in this
file), replace URLs like https://github.com/pnpm/pnpm/blob/<sha>/... with
https://github.com/pnpm/pnpm/blob/main/... so the links point to blob/main
instead of a pinned commit.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 451bf035-bdd7-43b0-8ca4-7b54be1b3827
📒 Files selected for processing (3)
crates/package-manager/src/build_sequence.rscrates/patching/src/apply.rscrates/patching/src/apply/tests.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- crates/patching/src/apply/tests.rs
- crates/patching/src/apply.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). (6)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Code Coverage
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (ubuntu-latest)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs
📄 CodeRabbit inference engine (AGENTS.md)
**/*.rs: Preserve existing method chains andpipe-traitchains; do not break them into intermediateletbindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Pathover&PathBuf,&strover&String) when it does not force extra copies.
PreferArc::clone(&x)andRc::clone(&x)overx.clone()for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;for any glob whose target is a module you control. External-crate preludes (e.g.,use rayon::prelude::*;) and root-of-module re-exports (e.g.,pub use submodule::*;inlib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plainStringor&str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only viaTryFrom<String>and/orFromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallibleFrom<String>andFrom<&str>constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bareasassertion), add afrom_str_unchecked(or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...
Files:
crates/package-manager/src/build_sequence.rs
🧠 Learnings (1)
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.
Applied to files:
crates/package-manager/src/build_sequence.rs
🔇 Additional comments (1)
crates/package-manager/src/build_sequence.rs (1)
38-53: Patch-aware build candidacy is implemented correctly.Nice change:
patchesis threaded cleanly and Line 188 + Line 190 correctly include patch-only snapshots in the build subgraph.Also applies to: 161-190
|
@coderabbitai re: the review-comment suggestion to switch CLAUDE.md is explicit that citations must pin a SHA (so the link stays meaningful when upstream files are renamed or deleted) and that the freshest-
The I'll leave the SHA links as they are. Thanks anyway! Written by an agent (Claude Code, claude-opus-4-7). |
|
Tip For best results, initiate chat on the files or code changes.
✏️ Learnings added
|
Pull in 28 commits from upstream main, including the `pacquet-npmrc` → `pacquet-config` rename (#420) plus features: - supportedArchitectures + --cpu/--os/--libc (#456) - frozen-lockfile (#442, #443, #447, #450) - git-fetcher (#436 / #446, #451, #454) - side-effects cache (#421 / #422, #423, #424) - real-hoist + global-virtual-store (#432 / #438 / #444, #449, #452) - patchedDependencies + allow-builds (#425, #427, #428) - engine/platform installability (#434 / #439) Conflicts resolved: - `crates/npmrc/` files migrated under the renamed `crates/config/` directory; `Npmrc` → `Config` everywhere except `NpmrcAuth` (which keeps the `.npmrc`-domain name). - `Config::current` reads the env-var DI generic `Api: EnvVar` for ${VAR}-substitution in `.npmrc`. Production turbofish in `cli_args.rs` is `Config::current::<RealApi, _, _, _, _>(...)`. - Two-phase `NpmrcAuth::apply_*` retained so default-registry creds key at the yaml-resolved registry URL. - New `Config::auth_headers` field plumbed through `install_package_by_snapshot`'s `DownloadTarballToStore`. - Tests under `crates/config/src/workspace_yaml/tests.rs` pick up the new ParseYaml unit test added on this branch.
Summary
Completes #397 item 9 (
patchedDependenciessupport). Patches now actually land on disk: pacquet applies configured patches to extracted package directories before postinstall hooks run, includes patched-only packages in the build trigger, and surfaces the four upstream diagnostic codes.Slices A + B (PR #425, merged) landed the foundation, Config plumbing, and the side-effects-cache key wiring. This PR wires the rest.
Pure-Rust patch applier
pacquet_patching::apply_patch_to_dirports upstream'sapplyPatchToDirusing thediffycrate (MIT OR Apache-2.0, pure Rust, no subprocess). Upstream's own comment notes thatpatchis unavailable on Windows andgit applyis awkward inside an existing git repo — pnpm vendored@pnpm/patch-packagefor that reason; pacquet sidesteps it the same way with an in-process applier.Supported
FileOperations:Modify,Create,Delete.RenameandCopyfall through toERR_PNPM_PATCH_FAILEDwith a descriptive message — they don't appear inpatch-package-style patches in practice.Build pipeline
build_sequence::get_subgraph_to_buildnow considerspatch.is_some()alongsiderequires_build. A patched-only package becomes a build candidate the same way upstream's filter atbuildSequence.ts:47,60treats them.BuildModules::runper-snapshot loop refactored to mirror upstream's flow:requires_build || patch.is_some().allowBuildscheck only gates scripts (upstream'sif (node.requiresBuild)atduring-install/src/index.ts:88-101). Disallow / ignored setsshould_run_scripts = falserather thancontinue— patches still apply.cache_key'sinclude_dep_graph_hashis nowshould_run_scripts(was unconditionallytrue), so a patched-only snapshot's cache key omits the deps-hash, matching upstream'sincludeDepGraphHash: hasSideEffectsat line 202.run_postinstall_hooks, mirroringduring-install/src/index.ts:171-178.(is_patched || has_side_effects) && side_effects_cache_write(was justside_effects_cache_write), matching upstream's(isPatched || hasSideEffects)at line 199.Diagnostics
Four upstream codes surface via
BuildModulesError:ERR_PNPM_PATCH_NOT_FOUND— patch file missing on diskERR_PNPM_INVALID_PATCH— unified-diff parse errorERR_PNPM_PATCH_FAILED— hunk wouldn't apply, target missing, IO error on a target pathERR_PNPM_PATCH_FILE_PATH_MISSING— resolved patch entry has a hash but no path (lockfile-only shape with no live config); mirrorsduring-install/src/index.ts:172-176Dependency
New workspace dep:
diffy = "0.5.0". MIT OR Apache-2.0, both in the deny.toml allow-list.cargo deny check licensesok.Test plan
pacquet_patching::apply::tests— 8 cases: happy-path Modify against the upstreamis-positive@1.0.0body, Create, Delete, missing patch file (PATCH_NOT_FOUND), malformed patch (INVALID_PATCH), unmatching hunk (PATCH_FAILED), missing target file (PATCH_FAILED).patch_only_snapshot_gets_patched_via_build_modulesdrivesBuildModuleswith a patched no-scripts snapshot end-to-end and verifies the patch file lands on disk.missing_patch_file_path_errors_with_diagnosticverifiesERR_PNPM_PATCH_FILE_PATH_MISSINGpropagates.write_path_cache_key_includes_patch_hash(from slices A+B) updated to provide a real patch file the applier can succeed against.just readyclean — 575 tests pass.just dylint(perfectionist) clean.RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace --document-private-itemsclean.taplo format --checkclean.cargo deny check licensesok.Out of scope
simple-with-patchfixture viaInstall::runthrough the live mock registry. Unit-level coverage of the same patched-package flow is in place above; the registry-mocked E2E mirrors the pattern from pacquet#419's optional-dep test and would fit a follow-up.RenameandCopyFileOperations — they don't appear inpatch-package-style patches; the applier surfaces them asERR_PNPM_PATCH_FAILEDwith a clear message.Written by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit
New Features
Bug Fixes
Tests & Docs