Skip to content

perf: run lockfile verification concurrently with frozen install#12227

Merged
zkochan merged 13 commits into
mainfrom
lockfile-perf
Jun 12, 2026
Merged

perf: run lockfile verification concurrently with frozen install#12227
zkochan merged 13 commits into
mainfrom
lockfile-perf

Conversation

@zkochan

@zkochan zkochan commented Jun 5, 2026

Copy link
Copy Markdown
Member

Problem

pnpm install with a frozen lockfile got noticeably slower because lockfile verification blocks every later install stage. The verification gate (the minimumReleaseAge/trustPolicy policy revalidation plus the tarball-URL anti-tamper check) issues a registry round trip per lockfile entry, and the whole install waited for it to finish before any fetching or linking could begin.

Change (pnpm / TypeScript)

Run lockfile verification concurrently with fetching and linking instead of blocking on it, while keeping two guarantees intact:

  1. No lifecycle script runs on an unverified lockfile. A verifyLockfile gate is threaded into both buildModules call sites — headlessInstall (frozen path) and _installInContext (full-resolution path) — and awaited immediately before any dependency lifecycle script runs. The projects' own preinstall/postinstall hooks are held to the same gate at both runLifecycleHooksConcurrently call sites, covering the enableModulesDir: false path that skips the build phase. If verification failed, the gate throws before a single script executes.
  2. The verdict is always reconciled. settleInstall(_install(), verifyLockfilePromise) awaits the verification verdict first so it takes precedence and fails fast (even mid-install), then surfaces the install's result/error. This also covers paths that skip the build phase entirely (ignoreScripts, lockfileOnly, empty lockfile).

Verification's synchronous prologue (cache lookup, lockfile hash, candidate collection) still runs against the pristine lockfile before _install() mutates ctx.wantedLockfile, so the concurrent async fan-out reads a stable snapshot — no data race.

The verification verdict deliberately takes precedence over a concurrent install error: pnpm add's full-resolution path can throw its own generic "resolution-policy violations produced but no handler wired" for the same underlying violation, and settleInstall makes sure the specific minimumReleaseAge/trustPolicy error is what surfaces.

Change (pacquet / Rust)

Same optimization ported to pacquet/crates/package-manager/. Install::run builds the resolution verifiers up front but dispatches the verification fan-out per path:

  • Frozen materialization path: verification runs concurrently with CreateVirtualStore (the fetch), settled with a select! so the verdict takes precedence: a rejected lockfile aborts the fetch in flight (fail-fast), while a fetch failure waits for the verdict and only surfaces once the lockfile is known trusted — an unrelated fetch error can't mask a rejected lockfile. The verdict is always reached before linking and BuildModules, so no dependency lifecycle script runs on an unverified lockfile.
  • Lockfile-only / up-to-date short-circuits and the fresh-resolve path: keep an eager blocking gate — they have no fetch to overlap.

A verification failure surfaces as the same InstallError::LockfileVerification variant regardless of which path produced it.

On the pnpr client, a frozen restore now skips resolution entirely: tarball downloads start from the local lockfile at t=0 (filtered through one batched store-index existence probe, so a warm store prefetches nothing) while the server delivers only the trust verdict via the new POST /v1/verify-lockfile endpoint, concurrently with the fetch.

Tests

  • pnpm: test/install/trustLockfile.ts covers the rejection itself, the trustLockfile opt-out, and both script gates — a dependency postinstall never runs when verification fails, and the projects' own lifecycle hooks never run either, asserted on the enableModulesDir: false path with a slow-rejecting verifier (an instantly-rejecting one aborts the install before the hooks are attempted, which would hide a missing gate). Existing verification/lifecycle/minimumReleaseAge suites pass.
  • pacquet: existing frozen_lockfile_gate_rejects_under_huge_minimum_release_age (unit) and install_fails_under_huge_minimum_release_age (CLI) assert the frozen install aborts with no virtual-store materialization on verification failure — proving the fail-fast settle cancels the fetch. New: without_store_hits + StoreIndex::contains_many unit tests pin the warm-store prefetch filter, and the frozen pnpr CLI test swaps the registry for a zero-expectation server before the restore, proving a warm-store frozen restore makes no registry requests.
  • pnpr client/server: integration tests cover /v1/verify-lockfile accepting a clean lockfile, rejecting a policy violation, honoring trustLockfile, and forwarding the client's credential map (each verify call targets a fresh pnpr so no verdict/metadata cache can satisfy it without exercising the credential).
  • clippy / cargo doc -D warnings / rustfmt / eslint clean; package-manager, lockfile-verification, store-dir, pnpr-client, and CLI pnpr-install suites all pass.

Behavioral nuance

On a rejected lockfile, fetching/linking may now have partially populated the store/node_modules before the abort (previously nothing ran, since verification went first). The command still fails with the same error code and no lifecycle scripts run.


Written by agents (Claude Code: claude-opus-4-8, claude-fable-5).

Summary by CodeRabbit

  • New Features

    • Server-side lockfile verification endpoint for external verification
    • Lockfile-driven tarball prefetch to speed local-frozen installs
  • Improvements

    • Lockfile verification runs concurrently with fetching/linking
    • Dependency and project lifecycle scripts are delayed until verification succeeds
    • Frozen-lockfile installs can verify locally without re-resolving or redownloading
  • Tests

    • New end-to-end and unit tests covering verification gating, prefetching, and store-index batching

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR runs lockfile verification concurrently with fetch/link during frozen installs, gates lifecycle scripts and final success on verification passing, adds a pnpr verification-only endpoint and client method, and threads verification overrides and tarball prefetching into pacquet's frozen-install fast path.

Changes

Concurrent Lockfile Verification

Layer / File(s) Summary
Release Notes
.changeset/parallel-lockfile-verification.md
Changeset documents the patch bumps and behavioral change: frozen-lockfile verification runs concurrently with fetch/link while lifecycle scripts remain gated on verification success.
pnpm Core Verification Promise Infrastructure
installing/deps-installer/src/install/index.ts, installing/deps-restorer/src/index.ts, installing/deps-installer/test/install/trustLockfile.ts
Start verification after lockfile load as an in-flight promise, run install concurrently and reconcile via settleInstall, thread a verifyLockfile callback through install/headless flows, and await it before running dependency/project lifecycle scripts; tests assert verification failure prevents postinstall artifacts.
pnpr Verification-Only Endpoint & Client
pacquet/crates/pnpr-client/src/lib.rs, pnpr/crates/pnpr/src/resolver.rs, pnpr/crates/pnpr/src/server.rs, pacquet/crates/pnpr-client/tests/integration.rs
Adds server POST /v1/verify-lockfile that verifies an input lockfile without resolving packages; pnpr client gains VerifyLockfileOptions and verify_lockfile to stream NDJSON verdict frames; integration tests cover accept/reject and credential forwarding.
Pacquet Install Pipeline Refactoring
pacquet/crates/package-manager/src/install.rs
Introduces verify_lockfile_eagerly, run_with_lockfile_verification/run_inner, conditional verifier construction based on trust_lockfile, eager verification at early-return/no-op points, blocking re-verification before fresh resolution, and updated verification recording.
Frozen Install Concurrent Verification Execution
pacquet/crates/package-manager/src/install_frozen_lockfile.rs
InstallFrozenLockfile accepts verifier list, optional override future, and lockfile_path; it races verification with virtual-store creation (tokio select) ensuring verification failure aborts before linking/build phases and surfaces verification errors with new error variants.
Tarball Prefetching & Store Index Batch Probe
pacquet/crates/package-manager/src/tarball_prefetch.rs, pacquet/crates/package-manager/src/tarball_prefetch/tests.rs, pacquet/crates/store-dir/src/store_index.rs, pacquet/crates/store-dir/src/store_index/tests.rs
Adds TarballPrefetcher::prefetch_lockfile and batching helpers that filter out entries present in the store index via without_store_hits; StoreIndex::contains_many provides batched existence probes; tests validate filtering and chunked lookup behavior.
CLI Frozen + Local Lockfile Fast Path & Tests
pacquet/crates/cli/src/cli_args/install.rs, pacquet/crates/cli/tests/pnpr_install.rs
install_via_pnpr gains a fast path for local frozen-lockfile installs: prefetch tarballs, optionally call PnprClient::verify_lockfile to build an override, run local Install wired to the local lockfile, and return; E2E test validates verification is invoked and no tarball downloads occur.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as pacquet CLI
  participant PnprClient as PnprClient
  participant Server as pnpr Server
  participant Resolver as Resolver
  participant Installer as Install/FrozenInstall
  CLI->>PnprClient: request verify_lockfile (VerifyLockfileOptions)
  PnprClient->>Server: POST /v1/verify-lockfile
  Server->>Resolver: handle_verify_lockfile(body)
  Resolver-->>PnprClient: terminal NDJSON frame (done|violations|error)
  CLI->>Installer: run frozen Install (with optional override)
  Installer->>Verifier: await verifyLockfile callback before lifecycle
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pnpm#11734: Overlaps frozen-install fast-path and pnpr delegation changes.
  • pnpm/pnpm#12245: Touches InstallFrozenLockfile orchestration that this PR modifies.
  • pnpm/pnpm#11824: Modifies Install dispatch/verification decision points related to this work.

Poem

🐰 I started a race with a lockfile today,

verification hummed while downloads stayed away,
scripts sat quiet till the verdict came through,
if it failed we stopped — if it passed we grew,
modules linked snug — a hop, and we're new.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main performance improvement: running lockfile verification concurrently with frozen install operations instead of sequentially, which is the core objective of this changeset.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 lockfile-perf

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

❤️ Share

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

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results

# Scenario main HEAD
1 Isolated linker: fresh restore, hot cache + hot store 2.199s ± 0.092s 2.172s ± 0.044s
2 Isolated linker: fresh add new dep, hot cache + hot store 5.456s ± 0.051s 5.478s ± 0.048s
3 Isolated linker: fresh install, hot cache + hot store 5.501s ± 0.079s 5.511s ± 0.068s
4 Isolated linker: fresh restore, cold cache + cold store 6.708s ± 0.035s 6.753s ± 0.044s
5 Isolated linker: fresh install, cold cache + cold store 12.37s ± 0.865s 12.613s ± 0.999s
6 GVS linker: fresh restore, hot cache + hot store 1.074s ± 0.043s 1.078s ± 0.032s

Run 27396467220 · 10 runs per scenario · triggered by @zkochan

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      7.6±0.16ms   569.4 KB/sec    1.06      8.0±0.27ms   539.1 KB/sec

@codecov-commenter

codecov-commenter commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.78212% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.91%. Comparing base (615c669) to head (9c6981b).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/pnpr-client/src/lib.rs 82.35% 12 Missing ⚠️
...uet/crates/package-manager/src/tarball_prefetch.rs 80.95% 8 Missing ⚠️
pacquet/crates/cli/src/cli_args/install.rs 90.16% 6 Missing ⚠️
pnpr/crates/pnpr/src/resolver.rs 81.81% 4 Missing ⚠️
pacquet/crates/package-manager/src/install.rs 96.62% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12227      +/-   ##
==========================================
+ Coverage   87.74%   87.91%   +0.16%     
==========================================
  Files         291      291              
  Lines       36107    36559     +452     
==========================================
+ Hits        31683    32141     +458     
+ Misses       4424     4418       -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.

@github-actions

github-actions Bot commented Jun 5, 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 3.912 ± 0.188 3.746 4.244 1.83 ± 0.14
pacquet@main 4.119 ± 0.090 3.993 4.287 1.92 ± 0.13
pnpr@HEAD 2.143 ± 0.133 1.927 2.326 1.00
pnpr@main 2.309 ± 0.132 2.117 2.547 1.08 ± 0.09
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.911990438280001,
      "stddev": 0.18767666054223558,
      "median": 3.8381010202800003,
      "user": 3.7474253400000004,
      "system": 3.30302892,
      "min": 3.74577882278,
      "max": 4.2441488637800004,
      "times": [
        4.2441488637800004,
        3.7715134537800004,
        4.20095422378,
        3.8590943617800004,
        3.75876485578,
        4.05957187378,
        3.8171076787800002,
        3.8853000087800003,
        3.74577882278,
        3.7776702397800004
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.11945938128,
      "stddev": 0.09045897569978009,
      "median": 4.0993153832800004,
      "user": 3.7038308399999993,
      "system": 3.2972945200000003,
      "min": 3.9926448807800003,
      "max": 4.28682467578,
      "times": [
        4.08025576978,
        4.06499031778,
        4.10818236278,
        4.09044840378,
        4.252285590780001,
        4.15223519078,
        4.28682467578,
        3.9926448807800003,
        4.04623610378,
        4.12049051678
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.1427464862800005,
      "stddev": 0.1333137945741719,
      "median": 2.13263401628,
      "user": 2.7005783400000003,
      "system": 2.86437822,
      "min": 1.9272505797800001,
      "max": 2.32561372478,
      "times": [
        2.31185965278,
        2.2802547517800003,
        2.16813450878,
        2.09827518678,
        2.0098519777800004,
        2.32561372478,
        2.1033297757800002,
        2.16193825678,
        2.04095644778,
        1.9272505797800001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.30935213488,
      "stddev": 0.13155189722514338,
      "median": 2.31453569528,
      "user": 2.6920381399999997,
      "system": 2.8743460199999995,
      "min": 2.11705185878,
      "max": 2.54650143578,
      "times": [
        2.2601286117800004,
        2.16632793078,
        2.19150845378,
        2.42565695178,
        2.3492343887800002,
        2.3691905437800003,
        2.54650143578,
        2.38808417178,
        2.2798370017800003,
        2.11705185878
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 664.4 ± 9.4 645.5 673.7 1.00
pacquet@main 697.8 ± 93.9 643.6 957.0 1.05 ± 0.14
pnpr@HEAD 710.8 ± 15.6 684.7 729.9 1.07 ± 0.03
pnpr@main 786.4 ± 13.3 766.7 813.3 1.18 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6643652237400001,
      "stddev": 0.009394964484256994,
      "median": 0.6679630399400001,
      "user": 0.38805639999999997,
      "system": 1.3008908199999996,
      "min": 0.64554504794,
      "max": 0.67365483094,
      "times": [
        0.66866620094,
        0.67365483094,
        0.66021742794,
        0.66779373594,
        0.66294538294,
        0.65176920494,
        0.66813234394,
        0.64554504794,
        0.67290386094,
        0.67202420094
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6977805962400001,
      "stddev": 0.09390096237733639,
      "median": 0.66704153194,
      "user": 0.37071209999999993,
      "system": 1.3262577199999999,
      "min": 0.64360620194,
      "max": 0.95701882094,
      "times": [
        0.72215058294,
        0.67774156894,
        0.66302445494,
        0.64380606394,
        0.64360620194,
        0.68333642494,
        0.66668673994,
        0.66739632394,
        0.65303877994,
        0.95701882094
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.7107919254399999,
      "stddev": 0.015567164745545427,
      "median": 0.71052314994,
      "user": 0.38877229999999996,
      "system": 1.3350637199999997,
      "min": 0.68471965094,
      "max": 0.72993422494,
      "times": [
        0.71360762694,
        0.68906836694,
        0.70743867294,
        0.68471965094,
        0.72993422494,
        0.70318392494,
        0.72750708394,
        0.70732911794,
        0.71962765794,
        0.72550292694
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.78636427104,
      "stddev": 0.013323594361210531,
      "median": 0.78585798744,
      "user": 0.3939145,
      "system": 1.3306725199999998,
      "min": 0.76666792494,
      "max": 0.8133207289400001,
      "times": [
        0.79044621494,
        0.76666792494,
        0.78798847994,
        0.79299879694,
        0.78372749494,
        0.78204242794,
        0.79640324594,
        0.8133207289400001,
        0.77927342994,
        0.77077396594
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.823 ± 0.063 3.706 3.924 1.77 ± 0.13
pacquet@main 3.827 ± 0.041 3.763 3.908 1.78 ± 0.13
pnpr@HEAD 2.174 ± 0.160 1.970 2.455 1.01 ± 0.10
pnpr@main 2.155 ± 0.152 1.993 2.338 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.822628512,
      "stddev": 0.06254913747144179,
      "median": 3.8220302816,
      "user": 3.5792077,
      "system": 3.2267067799999998,
      "min": 3.7060613141,
      "max": 3.9243816871,
      "times": [
        3.9243816871,
        3.8815351401,
        3.8208426951,
        3.8059947761,
        3.8232178681,
        3.8271101141,
        3.8069343670999998,
        3.7553537571,
        3.8748534010999998,
        3.7060613141
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.8266757666,
      "stddev": 0.04117442524837456,
      "median": 3.8179892031,
      "user": 3.5508942999999995,
      "system": 3.2204318800000005,
      "min": 3.7631213421000003,
      "max": 3.9083962941,
      "times": [
        3.8587211661,
        3.8154246691,
        3.8205537371,
        3.8437803101,
        3.8066636811,
        3.7848963370999997,
        3.8531043541,
        3.9083962941,
        3.8120957751,
        3.7631213421000003
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.1742044055000003,
      "stddev": 0.16002853871739245,
      "median": 2.1359355071,
      "user": 2.5035109999999996,
      "system": 2.7535432799999997,
      "min": 1.9696593790999999,
      "max": 2.4545337231,
      "times": [
        2.3245884920999997,
        2.2336279311,
        2.1280450081,
        2.3382902171,
        2.1438260061,
        1.9696593790999999,
        2.0282355161,
        2.4545337231,
        2.1190334881,
        2.0022042941
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.1553271540999996,
      "stddev": 0.15182468466119797,
      "median": 2.1007015491,
      "user": 2.5111084999999997,
      "system": 2.7391932799999994,
      "min": 1.9927967501000001,
      "max": 2.3381442581,
      "times": [
        2.1506526741,
        2.2931836651,
        2.0507504241,
        2.3339329171,
        2.3320578811,
        2.0145222271,
        2.0234665070999998,
        2.3381442581,
        1.9927967501000001,
        2.0237642371
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.100 ± 0.007 1.091 1.111 1.58 ± 0.19
pacquet@main 1.107 ± 0.030 1.067 1.167 1.59 ± 0.20
pnpr@HEAD 0.696 ± 0.084 0.653 0.933 1.00
pnpr@main 0.723 ± 0.088 0.682 0.973 1.04 ± 0.18
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.0998141423,
      "stddev": 0.007463650068356752,
      "median": 1.0973841579,
      "user": 1.07256114,
      "system": 1.6776818800000002,
      "min": 1.0905164779,
      "max": 1.1109710739,
      "times": [
        1.1089615119,
        1.1047753989,
        1.1109710739,
        1.1068323029,
        1.0961164799,
        1.0960745719,
        1.0986518359000002,
        1.0921770409,
        1.0930647289000002,
        1.0905164779
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.1070197253,
      "stddev": 0.029718806211795526,
      "median": 1.1010465979000001,
      "user": 1.06594964,
      "system": 1.6913265799999997,
      "min": 1.0671559049000001,
      "max": 1.1672208829000001,
      "times": [
        1.0919893219,
        1.1425455459,
        1.0999689209,
        1.0899817939,
        1.1672208829000001,
        1.1167018559000002,
        1.1129586709000001,
        1.0795500809,
        1.0671559049000001,
        1.1021242749000002
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6961167569000001,
      "stddev": 0.08384877380664288,
      "median": 0.6715059644,
      "user": 0.33024654,
      "system": 1.2831906800000001,
      "min": 0.6525289869,
      "max": 0.9332046629,
      "times": [
        0.6709930619,
        0.6714183949,
        0.6575668469,
        0.6753294229,
        0.6525289869,
        0.9332046629,
        0.6767204219,
        0.6715935339,
        0.6861778979000001,
        0.6656343389
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7231288257,
      "stddev": 0.08842628343812448,
      "median": 0.6923104134,
      "user": 0.32998464,
      "system": 1.3013437800000003,
      "min": 0.6817244239,
      "max": 0.9730359369,
      "times": [
        0.7098701899000001,
        0.6918475459,
        0.6910534129,
        0.6862306469,
        0.6927732809,
        0.6817244239,
        0.6890731789,
        0.7003695939,
        0.9730359369,
        0.7153100469
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.599 ± 0.055 2.556 2.747 3.88 ± 0.12
pacquet@main 2.585 ± 0.052 2.551 2.729 3.86 ± 0.12
pnpr@HEAD 0.689 ± 0.015 0.665 0.711 1.03 ± 0.03
pnpr@main 0.669 ± 0.016 0.657 0.710 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.59880495146,
      "stddev": 0.054881240458828806,
      "median": 2.58906788976,
      "user": 1.50407192,
      "system": 1.9658132199999998,
      "min": 2.55556212076,
      "max": 2.74742531776,
      "times": [
        2.60320088076,
        2.5975914687599997,
        2.55618529176,
        2.56920335276,
        2.59153006576,
        2.59716086876,
        2.58660571376,
        2.58358443376,
        2.55556212076,
        2.74742531776
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.5854306151599995,
      "stddev": 0.05225447923327142,
      "median": 2.5684705327599997,
      "user": 1.4976663199999998,
      "system": 1.94977482,
      "min": 2.55121581376,
      "max": 2.7292508017599997,
      "times": [
        2.5700630317599997,
        2.56465510976,
        2.59336269676,
        2.58706077676,
        2.55173183976,
        2.55121581376,
        2.57411937476,
        2.56596867276,
        2.5668780337599997,
        2.7292508017599997
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6885936340600001,
      "stddev": 0.015042276011885677,
      "median": 0.6896673602600001,
      "user": 0.33782982,
      "system": 1.27363612,
      "min": 0.66504778076,
      "max": 0.7108406807600001,
      "times": [
        0.7108406807600001,
        0.69584909976,
        0.69299715876,
        0.68593039576,
        0.66504778076,
        0.68633756176,
        0.7065367757600001,
        0.69693890876,
        0.67074115776,
        0.6747168207600001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.66922436296,
      "stddev": 0.015779471364458075,
      "median": 0.66522356976,
      "user": 0.33423331999999994,
      "system": 1.2632276199999999,
      "min": 0.65682431176,
      "max": 0.70984428676,
      "times": [
        0.67373472176,
        0.6573606307600001,
        0.66778162376,
        0.66676466876,
        0.65914171276,
        0.66368247076,
        0.70984428676,
        0.65682431176,
        0.66034809976,
        0.67676110276
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12227
Testbedpacquet
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
3,822.63 ms
(-53.03%)Baseline: 8,137.86 ms
9,765.44 ms
(39.14%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
2,598.80 ms
(-42.45%)Baseline: 4,515.97 ms
5,419.16 ms
(47.96%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,099.81 ms
(-18.36%)Baseline: 1,347.15 ms
1,616.58 ms
(68.03%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
3,911.99 ms
(-56.54%)Baseline: 9,000.90 ms
10,801.08 ms
(36.22%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
664.37 ms
(-0.14%)Baseline: 665.32 ms
798.39 ms
(83.21%)
🐰 View full continuous benchmarking report in Bencher

@zkochan

zkochan commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

This doesn't help unfortunately

@zkochan zkochan closed this Jun 5, 2026
@zkochan zkochan deleted the lockfile-perf branch June 5, 2026 22:55
@zkochan zkochan restored the lockfile-perf branch June 7, 2026 09:49
@zkochan zkochan reopened this Jun 7, 2026
@zkochan zkochan force-pushed the lockfile-perf branch 4 times, most recently from f92498c to dcedd65 Compare June 7, 2026 21:34
@zkochan

zkochan commented Jun 7, 2026

Copy link
Copy Markdown
Member Author

Why the benchmark shows no speedup — investigation

Looked into why @HEAD@main across the board. Short version: the optimization is correctly implemented on both paths, but the benchmark's workload and harness keep the win off the critical path. Two distinct reasons for the two paths.

Direct pacquet: verification and fetch share one network-concurrency semaphore

The PR runs verification concurrently with the fetch via try_join!. But both sides draw HTTP permits from the same ThrottledClient semaphore:

  • ThrottledClient holds a single Semaphore sized to network_concurrency (16–64; pacquet/crates/network/src/lib.rs). Its own doc comment: "The semaphore is shared across both — bounding the total" in-flight requests.
  • Install::run hands the same client to the verifier (Arc::clone(&http_client_arc) in build_resolution_verifiers) and to the fetcher (CreateVirtualStoreinstall_package_by_snapshot).

With ~1264 tarballs to fetch, the fetch alone already saturates all N permits for the whole install. Verification's ~1264 metadata round trips just queue for the same permits they would have used serially. Wall-clock through an N-permit pool is total_requests / N × latency whether the two phases run serially (main: eager verify, then fetch) or interleaved (HEAD's try_join!). Same total requests, same pool → same time. (HEAD is in fact ~1% faster on cold-cache cold-store, consistent with interleaving avoiding the drain/refill gap between main's two serial phases.)

So on the direct path, "overlap verification with fetch" doesn't add network parallelism — it reorders work through a pipe that's already full.

pnpr: the early-start is implemented, but the benchmark masks it

The pnpr frozen path does exactly what's intended: prefetch_lockfile spawns every tarball download from the local lockfile at t=0 (tokio::spawn, no server wait), and materialization runs concurrently with the server verify_lockfile call (try_join! in install_frozen_lockfile.rs). So fetch + materialization genuinely start before the server confirms the lockfile is trusted.

It still doesn't beat main, for three compounding reasons:

  1. The benchmark's pnpr server is warm. On main, the pnpr frozen-restore goes through resolve_streaming: the client prefetches each tarball as the server streams its package frame. With a warm server, those frames arrive after ~one fast round trip, so main starts fetching almost as early as HEAD's lockfile-prefetch. The early-start only wins big when the server is slow to produce the resolved package list — which the warm-server setup specifically avoids.
  2. Cold-store is download-bound. The critical path is downloading ~1264 tarballs through the same N-wide semaphore (per-connection bandwidth cap). A ~one-round-trip head-start (~50 ms) is ~1% of a ~5 s download — inside the run-to-run noise.
  3. Hot-store has no download to hide behind. With the store warm there's nothing to overlap, and HEAD adds a dedicated verify_lockfile round trip whose server-side work (re-checking every entry against minimumReleaseAge) is more than main's cheap warm resolve — hence the small regression (599 → 648 ms on fresh-restore hot/hot).

How to actually realize the win

  • Direct path: give verification its own network budget — a separate ThrottledClient/semaphore (or a reserved permit slice) — so its latency-bound round trips run on spare capacity instead of competing with tarball downloads. Without that, the overlap can't add throughput.
  • pnpr path: the early-start pays off against a cold or slow pnpr server (or an expensive server resolve), where main blocks waiting for the resolve stream while HEAD's lockfile-prefetch races ahead. A benchmark variant with a cold server / added server-resolve latency would surface it.
  • More generally, the overlap only helps when the fetch leaves idle network capacity — i.e. few tarballs but many cold verification round trips. None of the current scenarios isolate that.

Happy to implement the separate-network-budget variant (direct) and/or a cold-server pnpr scenario and re-run, to confirm the diagnosis and give the PR a measurable improvement.


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

zkochan added 7 commits June 12, 2026 01:30
Lockfile verification (the minimumReleaseAge/trustPolicy policy gate plus the
tarball-URL anti-tamper check, which issues a registry round trip per lockfile
entry) blocked every later install stage. Kick it off without awaiting so it
overlaps fetching and linking, and reconcile the verdict in settleInstall:
a verification failure aborts the install even mid-flight, and an install that
finishes first is held back until the verdict arrives.

Dependency lifecycle scripts are still gated behind a successful verification
via a verifyLockfile callback awaited immediately before every buildModules
call, so no dependency script ever runs on an unverified lockfile.
…install error

The full-resolution `pnpm add` path can throw its own resolution-policy
error ("violations produced but no handleResolutionPolicyViolations callback
was wired") for the same underlying violation. Racing it against verification
let that generic error mask the specific minimumReleaseAge/trustPolicy error.
Await the verification verdict first so it takes precedence and still fails
fast, matching the original sequencing where verification gated the install.
…e frozen-install fetch

Ports the pnpm-side optimization to pacquet. Lockfile verification (the
minimumReleaseAge / trustPolicy='no-downgrade' gate plus the tarball-URL
anti-tamper check) used to block the whole install: it ran to completion
before any tarball was fetched, and each entry is a registry round trip.

Now the verifiers are built up front but the fan-out is dispatched per path.
On the frozen materialization path it runs concurrently with CreateVirtualStore
via tokio::try_join!, so the per-entry round trips overlap the downloads and a
rejected lockfile aborts the fetch in flight (fail-fast). The verdict is always
reached before linking and BuildModules, so no dependency lifecycle script runs
on an unverified lockfile. The lockfile-only / up-to-date short-circuits and the
fresh-resolve path keep an eager blocking gate (they have no fetch to overlap).
A verification failure surfaces as the same InstallError::LockfileVerification
variant regardless of which path produced it.
…erifier API

The rebase onto main picked up #12309, which threads an
ObservedDistStats sink through build_resolution_verifiers and
verify_input_lockfile and adds size-hint parameters to
TarballPrefetcher::prefetch. Pass an empty sink where the overlap
paths have no use for the stats, and prefetch lockfile tarballs
without a work estimate (the lockfile records no dist sizes).
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12227
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,174.20 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
688.59 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
696.12 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,142.75 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
710.79 ms
🐰 View full continuous benchmarking report in Bencher

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

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0)

Grey Divider


Remediation recommended

1. Prefetch downloads skipped deps 🐞 Bug ➹ Performance
Description
TarballPrefetcher::prefetch_lockfile prefetches every Registry resolution in lockfile.packages
without applying the frozen install’s dependency-group/skip filters, so it can download tarballs for
packages that InstallFrozenLockfile will never materialize (e.g. optional deps excluded by flags).
This creates avoidable network/store churn and can slow down or stress registries on large
lockfiles.
Code

pacquet/crates/package-manager/src/tarball_prefetch.rs[R294-320]

+    pub async fn prefetch_lockfile(&self, lockfile: &Lockfile, config: &Config) {
+        let Some(packages) = lockfile.packages.as_ref() else {
+            return;
+        };
+        let mut pending = Vec::with_capacity(packages.len());
+        for (package_key, metadata) in packages {
+            if !matches!(&metadata.resolution, LockfileResolution::Registry(_)) {
+                continue;
+            }
+            let (tarball_url, integrity) =
+                tarball_url_and_integrity(&metadata.resolution, package_key, config)
+                    .expect("registry resolutions always carry an integrity");
+            let package_id = package_key.without_peer().to_string();
+            let integrity = integrity.to_string();
+            pending.push(PendingPrefetch {
+                store_key: store_index_key(&integrity, &package_id),
+                package_id,
+                package_url: tarball_url.into_owned(),
+                integrity,
+            });
+        }
+        for entry in without_store_hits(self.store_index.clone(), pending).await {
+            let PendingPrefetch { package_id, package_url, integrity, .. } = entry;
+            // The lockfile records no dist size hints, so the downloads
+            // queue without a work estimate.
+            self.prefetch(package_id, package_url, &integrity, None, None);
+        }
Evidence
prefetch_lockfile walks lockfile.packages and schedules downloads for every registry resolution. The
frozen install path explicitly excludes optional snapshots (and other skipped categories) and passes
the skip set down into CreateVirtualStore, so some prefetched entries are guaranteed to never be
used.

pacquet/crates/package-manager/src/tarball_prefetch.rs[279-321]
pacquet/crates/package-manager/src/install_frozen_lockfile.rs[452-480]
pacquet/crates/package-manager/src/install_frozen_lockfile.rs[656-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
`TarballPrefetcher::prefetch_lockfile()` currently iterates over **all** `lockfile.packages` registry entries and schedules downloads, but the frozen install pipeline later filters the graph (e.g. `--no-optional`, runtime skipping, installability skips) via a `skipped` set. This means the prefetch phase can download tarballs for packages that will never be linked/built in this invocation.
### Issue Context
- Frozen installs compute a `skipped` set based on `dependency_groups` and other flags and pass it into `CreateVirtualStore`.
- The new prefetcher runs before that filtering and has no knowledge of which snapshots will be materialized.
### Fix Focus Areas
- pacquet/crates/package-manager/src/tarball_prefetch.rs[279-321]
- pacquet/crates/package-manager/src/install_frozen_lockfile.rs[452-480]
- pacquet/crates/package-manager/src/install_frozen_lockfile.rs[656-670]
### Suggested fix
Refactor `prefetch_lockfile` (or add a new method) to prefetch only the subset of packages that the frozen install will actually materialize:
1. Prefer driving prefetch from `lockfile.snapshots` + the already-computed `skipped` set (and dependency-group reachability), rather than `lockfile.packages`.
2. Alternatively, move the prefetch kickoff into `InstallFrozenLockfile::run` *after* `skipped` is computed, so it can prefetch only non-skipped snapshot keys.
3. Keep the existing store-index hit filtering to avoid redundant downloads.

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


2. No install cancellation 🐞 Bug ☼ Reliability
Description
In mutateModules, settleInstall() throws as soon as lockfile verification fails but does not
cancel the already-started _install() promise, so fetch/link work may continue mutating the
store/node_modules after the caller has already observed failure. This is especially risky for
programmatic/long-lived callers because the background install can overlap subsequent operations and
still perform filesystem/network I/O despite the reported failure.
Code

installing/deps-installer/src/install/index.ts[R540-555]

+  async function settleInstall (
+    install: Promise<InnerInstallResult>,
+    verification: Promise<void> | undefined
+  ): Promise<InnerInstallResult> {
+    if (verification == null) return install
+    // Handle the install's eventual rejection up front so a fail-fast
+    // verification throw below doesn't leave the still-running install
+    // unhandled.
+    install.catch(() => {})
+    try {
+      await verification
+      return await install
+    } catch (err) {
+      detachReporter()
+      throw err
+    }
Evidence
_install() is started immediately and settleInstall() only awaits the verification verdict
before throwing; there is no abort/cancel mechanism invoked, so the in-flight install continues. The
only explicit verification gate shown is immediately before buildModules, meaning earlier install
phases can run even after verification has failed.

installing/deps-installer/src/install/index.ts[404-412]
installing/deps-installer/src/install/index.ts[465-556]
installing/deps-restorer/src/index.ts[583-606]
installing/deps-installer/src/install/index.ts[1671-1674]

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

## Issue description
`settleInstall()` fails fast on lockfile verification failure, but it cannot actually stop the already-started `_install()` work. This means the command/API may return a verification error while `_install()` continues doing I/O and filesystem mutations in the background.
### Issue Context
- Verification is kicked off early and `_install()` is started immediately (`settleInstall(_install(), verifyLockfilePromise)`).
- Dependency lifecycle scripts are gated via `await opts.verifyLockfile?.()` right before `buildModules`, but fetch/link phases can still run after verification has failed, until they naturally reach a gate or complete.
### Fix Focus Areas
- installing/deps-installer/src/install/index.ts[404-412]
- installing/deps-installer/src/install/index.ts[465-556]
- installing/deps-restorer/src/index.ts[583-606]
- installing/deps-installer/src/install/index.ts[1671-1674]
### Suggested fix
Implement cooperative cancellation for the install pipeline:
1. Create an `AbortController`/cancellation token in `mutateModules` when starting concurrent verification.
2. Attach a handler to the verification promise that triggers cancellation on failure.
3. Thread the abort signal/token into the parts of `_install()` that perform long-running work (fetching/linking) and make them bail out early when cancelled.
4. Ensure cancellation is also triggered when `mutateModules` exits early (e.g., preResolution hooks throw) to avoid leaving verification/fetch work running in the background.

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


3. Prefetcher not shutdown 🐞 Bug ☼ Reliability
Description
In pacquet’s frozen pnpr restore fast-path, prefetcher.shutdown().await is only executed on
success; any install/verification error triggers an early return via ? and skips the
shutdown/flush. This can drop the chance to drain/flush the store-index writer task and may leave
background work unfinished when the command exits.
Code

pacquet/crates/cli/src/cli_args/install.rs[R581-593]

+        let result = match lockfile_verification_override {
+            Some(lockfile_verification_override) => {
+                install
+                    .run_with_lockfile_verification::<Reporter>(lockfile_verification_override)
+                    .await
+            }
+            None => install.run::<Reporter>().await,
+        };
+        result.wrap_err("restoring dependencies from the local lockfile via pnpr verification")?;
+
+        prefetcher.shutdown().await;
+
+        return Ok(());
Evidence
The CLI path uses result.wrap_err(...)?; before prefetcher.shutdown().await, so errors bypass
shutdown. The prefetcher shutdown docs and store-index writer docs indicate the writer task should
be awaited after dropping the last sender so the final batch flushes.

pacquet/crates/cli/src/cli_args/install.rs[521-593]
pacquet/crates/package-manager/src/tarball_prefetch.rs[264-286]
pacquet/crates/store-dir/src/store_index.rs[111-116]

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 frozen pnpr restore path creates a `TarballPrefetcher` but calls `prefetcher.shutdown().await` only after `result.wrap_err(...)?` succeeds. On any error, the `?` returns early and skips shutdown, even though the prefetcher’s writer task is documented as requiring an explicit drain/await to flush the final batch.
### Issue Context
`TarballPrefetcher::shutdown(self)` drops the writer handle and awaits the writer task to flush queued rows. Skipping it on failures makes behavior depend on Drop/runtime shutdown timing.
### Fix Focus Areas
- pacquet/crates/cli/src/cli_args/install.rs[521-593]
- pacquet/crates/package-manager/src/tarball_prefetch.rs[264-286]
- pacquet/crates/store-dir/src/store_index.rs[111-116]
### Suggested fix
Restructure to always run shutdown before returning:
- Capture `result` (success or error) without `?`.
- Call `prefetcher.shutdown().await` unconditionally.
- Then return `result.wrap_err(...)`.
Example shape:

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



Informational

4. Stale detachReporter comment 🐞 Bug ⚙ Maintainability
Description
A comment in tryFrozenInstall still claims it mirrors a removed verifyLockfileResolutions try/catch,
which is no longer true after verification was made concurrent; this misleads future maintainers
about why detachReporter is needed there.
Code

installing/deps-installer/src/install/index.ts[R404-412]

+    verifyLockfilePromise = verifyLockfileResolutions(ctx.wantedLockfile, opts.resolutionVerifiers, {
+      cacheDir: opts.cacheDir,
+      lockfilePath: wantedLockfilePath,
+    })
+    // Keep the rejection from going unhandled in the window before
+    // `settleInstall` awaits the verdict — a preResolution hook or the
+    // install kickoff below could throw and bail out before we get there.
+    verifyLockfilePromise.catch(() => {})
 }
Evidence
The PR replaced the earlier inline verification try/catch with a concurrently-started promise
(verifyLockfilePromise = verifyLockfileResolutions(...) plus a no-op catch), but the pacquet error
comment still references the old try/catch, which no longer exists in the updated flow.

installing/deps-installer/src/install/index.ts[395-412]
installing/deps-installer/src/install/index.ts[1051-1054]

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

## Issue description
`tryFrozenInstall` has a comment that says it uses the same reasoning as a `verifyLockfileResolutions` catch block “above”, but that catch block was removed when lockfile verification was changed to run concurrently (promise kick-off + later reconciliation). The comment is now stale and misleading.
### Issue Context
`detachReporter()` is still important for the pacquet failure path, but the justification should reference the current reconciliation/cleanup approach (e.g. `settleInstall`) or be phrased generically (“user-facing failure path; detach to avoid listener leaks”).
### Fix Focus Areas
- installing/deps-installer/src/install/index.ts[1047-1054]
- installing/deps-installer/src/install/index.ts[395-412]

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


5. Misleading verifier-empty comment 🐞 Bug ⚙ Maintainability
Description
A new comment in Install::run_inner claims the resolution-verifier list can be empty when there is
“no active resolution policy”, but build_resolution_verifiers documents that it always includes the
npm verifier (tarball-URL binding), making the list non-empty whenever verification is enabled. This
mismatch can mislead maintainers about when verification gates are active.
Code

pacquet/crates/package-manager/src/install.rs[R681-706]

+        // Resolution verifiers re-apply `minimumReleaseAge` /
+        // `trustPolicy='no-downgrade'` (plus the tarball-URL anti-tamper
+        // check) to every entry in the loaded `pnpm-lock.yaml`. They are
+        // built here — cheap, no I/O — but the verification fan-out itself
+        // is dispatched per path below: on the frozen materialization path
+        // it runs concurrently with the fetch (see [`InstallFrozenLockfile`])
+        // so the per-entry registry round trips overlap the download;
+        // every other path (fresh resolve, the lockfile-only / up-to-date
+        // short-circuits) verifies eagerly via [`verify_lockfile_eagerly`]
+        // before it proceeds. `trust_lockfile` (the OR of yaml's
+        // `trustLockfile` and the `--trust-lockfile` CLI flag, resolved in
+        // [`crate::cli_args::install::InstallArgs::run`]; the opt-out for
+        // environments that treat the on-disk lockfile as already-trusted,
+        // see [#11860]) or no active resolution policy leaves the list
+        // empty, making every gate a no-op — fresh local resolution is
+        // already filtered by the resolver's own per-version gate
+        // (`minimumReleaseAge` via `ResolveResult::policy_violation`,
+        // `trustPolicy='no-downgrade'` via the npm resolver's
+        // `fail_if_trust_downgraded_for_pick`). The list is built whenever
+        // a policy could apply, independent of whether a lockfile is loaded, so the
+        // fresh-resolve path can record the freshly written lockfile as
+        // already-verified (see `record_lockfile_verified` below). Mirrors
+        // pnpm's wiring at
+        // <https://github.com/pnpm/pnpm/blob/2a9bd897bf/installing/deps-installer/src/install/index.ts#L355-L383>
+        // and its concurrent verification + build gate.
+        //
Evidence
The install path comment asserts emptiness under “no active resolution policy”, but the verifier
builder’s own documentation states it always includes the npm verifier, implying the list is
non-empty whenever built.

pacquet/crates/package-manager/src/install.rs[681-720]
pacquet/crates/package-manager/src/build_resolution_verifiers.rs[61-64]

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

## Issue description
`Install::run_inner`’s new comment says the verifier list can be empty when there is “no active resolution policy”. However, `build_resolution_verifiers` is documented to always include the npm verifier (tarball-URL binding), so in practice the list is non-empty whenever verification is enabled (i.e. when `trust_lockfile` is false).
### Issue Context
This is a comment/documentation inconsistency: the code behavior is correct, but the comment describes a different runtime shape, which can cause incorrect future edits.
### Fix Focus Areas
- pacquet/crates/package-manager/src/install.rs[681-706]
- pacquet/crates/package-manager/src/build_resolution_verifiers.rs[61-64]
### Suggested fix
Update the comment to reflect actual behavior:
- Clarify that the list is empty only when `trust_lockfile` is enabled (or other explicit opt-out), and that the npm verifier is always present otherwise (even when policy knobs like `minimumReleaseAge` / `trustPolicy` are unset).
- If you intended a truly-empty list when policies are off, that would require changing `build_resolution_verifiers` and would weaken the always-on tarball URL anti-tamper binding; avoid that unless explicitly desired.

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


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

perf: Run lockfile verification concurrently with frozen installs
✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Run lockfile verification concurrently with fetch/link on frozen installs to reduce wall time.
• Gate dependency lifecycle scripts on verification verdict across headless and full install paths.
• Add pnpr /v1/verify-lockfile flow for pacquet and tests ensuring scripts never run unverified.
Diagram
graph TD
A["pnpm frozen install"] --> C["fetch/link phases"] --> D["lifecycle scripts (buildModules)"]
A --> B["verifyLockfileResolutions"]
B --> D
E["pacquet frozen install"] --> F["try_join verify+fetch"] --> D
F --> G["pnpr /v1/verify-lockfile"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Defer node_modules/linking until verification completes
  • ➕ Avoids partial node_modules/store population on rejected lockfiles
  • ➕ Simplifies rollback/cleanup expectations for users
  • ➖ Gives up most of the wall-time win (less overlap with expensive registry round trips)
  • ➖ Still blocks the install critical path under slow verification
2. Add cleanup/rollback on verification failure
  • ➕ Preserves concurrency while reducing observable partial artifacts
  • ➕ Could improve UX on failures (cleaner workspace state)
  • ➖ High complexity: needs safe deletion semantics across platforms and linkers
  • ➖ Risky to delete user-modified node_modules/store content
3. Batch verification metadata requests (reduce per-entry round trips)
  • ➕ Could speed up verification even without concurrency
  • ➕ Reduces load on registries/verification services
  • ➖ Requires new protocol/server support or new registry fetch strategy
  • ➖ More invasive than overlapping existing operations

Recommendation: Keep the PR’s approach: overlapping verification with fetch/link provides the performance win while maintaining the critical security guarantee via an explicit pre-script gate. The chosen reconciliation strategy (await verification first for error precedence + fail-fast) is appropriate, and the added tests specifically cover the highest-risk regression (scripts running before verification).

Grey Divider

File Changes

Enhancement (9)
index.ts Run lockfile verification concurrently and gate scripts via verifyLockfile +60/-17

Run lockfile verification concurrently and gate scripts via verifyLockfile

• Starts lockfile verification as an un-awaited promise and reconciles it with the install via 'settleInstall', ensuring verification failures fail fast and take precedence. Threads a 'verifyLockfile' callback into build entry points and awaits it immediately before 'buildModules' so dependency scripts never run on an unverified lockfile.

installing/deps-installer/src/install/index.ts


index.ts Add verifyLockfile callback to headless install options +9/-0

Add verifyLockfile callback to headless install options

• Extends 'HeadlessOptions' with an optional 'verifyLockfile' hook and awaits it right before 'buildModules', enabling overlap of verification with fetch/link while preserving the no-scripts-before-verdict guarantee.

installing/deps-restorer/src/index.ts


install.rs Wire pnpr lockfile verification override into frozen installs +80/-2

Wire pnpr lockfile verification override into frozen installs

• For frozen, non-lockfile-only pnpr installs, prefetches tarballs from the local lockfile and concurrently requests a verification verdict using the pnpr client. Passes a verification future into the package-manager via 'run_with_lockfile_verification', mapping failures into a consistent frozen-lockfile error variant.

pacquet/crates/cli/src/cli_args/install.rs


install.rs Dispatch verification per install path and normalize verification errors +144/-28

Dispatch verification per install path and normalize verification errors

• Refactors 'Install::run' into an inner runner supporting an optional verification override, verifies eagerly on paths without fetch to overlap, and passes verifier context into the frozen materialization path. Ensures verification failures surface as the same top-level 'InstallError::LockfileVerification' variant, including when produced by the frozen path.

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


install_frozen_lockfile.rs Run lockfile verification concurrently with CreateVirtualStore via try_join! +92/-28

Run lockfile verification concurrently with CreateVirtualStore via try_join!

• Adds verifier inputs and an optional verification override to the frozen install subroutine. Executes verification concurrently with the fetch/materialization phase using 'tokio::try_join!' to fail fast and ensures the verdict is reached before linking/build steps.

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


tarball_prefetch.rs Add prefetch_lockfile to queue tarball downloads from lockfile entries +29/-0

Add prefetch_lockfile to queue tarball downloads from lockfile entries

• Introduces 'prefetch_lockfile' to enqueue registry tarball downloads for all registry-resolved lockfile packages and adds URL construction helper logic.

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


lib.rs Add VerifyLockfileOptions and /v1/verify-lockfile client support +116/-0

Add VerifyLockfileOptions and /v1/verify-lockfile client support

• Adds a new request type derived from resolve options and implements a streaming NDJSON client for '/v1/verify-lockfile' that returns success on a terminal 'done' frame and maps 'violations' into a structured verification error.

pacquet/crates/pnpr-client/src/lib.rs


resolver.rs Implement POST /v1/verify-lockfile endpoint returning terminal verdict frame +45/-0

Implement POST /v1/verify-lockfile endpoint returning terminal verdict frame

• Adds a resolver handler that validates input, optionally short-circuits when 'trust_lockfile' is set, and otherwise verifies the provided lockfile under policy settings without resolving, returning a single NDJSON verdict frame.

pnpr/crates/pnpr/src/resolver.rs


server.rs Route /v1/verify-lockfile to resolver handler +8/-1

Route /v1/verify-lockfile to resolver handler

• Registers the new '/v1/verify-lockfile' route and wires it into the resolver runtime initialization path.

pnpr/crates/pnpr/src/server.rs


Tests (3)
trustLockfile.ts Test that postinstall scripts do not run when verification fails +28/-1

Test that postinstall scripts do not run when verification fails

• Adds an integration test that installs a package with a postinstall script under a rejecting verifier and asserts the install fails and the postinstall-generated file is absent.

installing/deps-installer/test/install/trustLockfile.ts


pnpr_install.rs Add CLI test asserting frozen pnpr install verifies lockfile without resolving +47/-0

Add CLI test asserting frozen pnpr install verifies lockfile without resolving

• Adds a test that performs an initial install to create a lockfile, removes node_modules, then performs a frozen install against a mocked '/v1/verify-lockfile' endpoint and asserts verification is called and node_modules links are created.

pacquet/crates/cli/tests/pnpr_install.rs


integration.rs Add integration tests for verify-lockfile endpoint success and rejection +50/-1

Add integration tests for verify-lockfile endpoint success and rejection

• Adds tests verifying that a clean lockfile is accepted and that a policy violation (huge minimumReleaseAge) is rejected with a verification error containing the expected breakdown.

pacquet/crates/pnpr-client/tests/integration.rs


Documentation (1)
parallel-lockfile-verification.md Add changeset describing concurrent lockfile verification speedup +7/-0

Add changeset describing concurrent lockfile verification speedup

• Introduces a changeset documenting the new behavior: verification runs concurrently with fetching/linking while still gating lifecycle scripts on a successful verdict.

.changeset/parallel-lockfile-verification.md


Grey Divider

Qodo Logo

@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: 5

Caution

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

⚠️ Outside diff range comments (1)
installing/deps-installer/src/install/index.ts (1)

404-432: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Start verification after preResolution has finished mutating the lockfile.

verifyLockfileResolutions() is kicked off on Lines 404-407 before the preResolution hooks run on Lines 421-432, but those hooks receive the live ctx.wantedLockfile object. A hook that rewrites importer/package snapshots can therefore change what _install() fetches and builds after the verifier has already snapshotted and approved the old contents, which reopens the resolver-policy bypass this gate is meant to prevent. verifyLockfilePromise needs to be created after the hooks finish, or from a post-hook clone of the lockfile.

🤖 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 `@installing/deps-installer/src/install/index.ts` around lines 404 - 432, The
verification promise is started too early: verifyLockfileResolutions is invoked
(creating verifyLockfilePromise) before opts.hooks.preResolution run and can
observe a lockfile that hooks will mutate; move the creation of
verifyLockfilePromise (and the derived verifyLockfile thunk) to after the
preResolution loop or alternatively call verifyLockfileResolutions with a deep
clone of ctx.wantedLockfile performed after the hooks complete so the verifier
always checks the final lockfile; update references to
verifyLockfilePromise/verifyLockfile accordingly (functions:
verifyLockfileResolutions, verifyLockfilePromise, verifyLockfile, and
opts.hooks.preResolution).
🧹 Nitpick comments (1)
pacquet/crates/pnpr-client/tests/integration.rs (1)

308-355: ⚡ Quick win

Add a /v1/verify-lockfile credential-forwarding test.

These new tests cover accept/reject semantics, but the new endpoint has its own request builder in PnprClient::verify_lockfile. A typo in the authorization header or authHeaders body shape would break frozen installs against private registries without failing this suite, because both cases use a public package and never assert the wire contract. Please mirror forwards_credentials_and_the_identity_header() for /v1/verify-lockfile.

🤖 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/pnpr-client/tests/integration.rs` around lines 308 - 355, Add
a credential-forwarding integration test for the /v1/verify-lockfile endpoint
similar to the existing forwards_credentials_and_the_identity_header test:
exercise PnprClient::verify_lockfile (using
VerifyLockfileOptions::from_resolve_options and a TestRegistry/start_pnpr setup)
and assert the outgoing HTTP request both sets the Authorization header and
includes the authHeaders body shape/identity header. Update or add the test in
the same integration.rs test module to mirror the semantics and assertions of
forwards_credentials_and_the_identity_header so any typo in
PnprClient::verify_lockfile's request builder (authorization header name/value
or authHeaders payload) will fail the suite.
🤖 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 `@installing/deps-restorer/src/index.ts`:
- Around line 583-585: The guard that ensures dependency lifecycle scripts don't
run on an unverified lockfile is only executed inside the buildModules() branch;
when enableModulesDir === false headlessInstall() still runs project lifecycle
hooks without awaiting opts.verifyLockfile. Move the await
opts.verifyLockfile?.() check so it executes before any script-execution path
(i.e., above both the buildModules(...) call and the call to
runLifecycleHooksConcurrently(...)), or alternatively add an explicit await
opts.verifyLockfile?.() immediately before runLifecycleHooksConcurrently() in
headlessInstall(); ensure references to enableModulesDir, buildModules,
headlessInstall, runLifecycleHooksConcurrently, and opts.verifyLockfile are
updated accordingly.

In `@pacquet/crates/package-manager/src/install_frozen_lockfile.rs`:
- Around line 627-687: The current tokio::try_join!(verify_fut,
create_virtual_store_fut) can drop verify_fut and let CreateVirtualStore errors
surface first; change this by awaiting both futures to completion but giving
precedence to verification errors: implement a small "settle" pattern (e.g.
spawn or join both futures and then inspect results) so that verify_fut's Err is
returned as InstallFrozenLockfileError::LockfileVerification whenever
verification fails, otherwise return CreateVirtualStoreOutput on success; update
the code around verify_fut, create_virtual_store_fut and the tokio::try_join!
call to use that helper so verification is never dropped and its error always
wins.

In `@pacquet/crates/package-manager/src/install.rs`:
- Around line 732-735: The verification cache write uses
workspace_root.join(Lockfile::FILE_NAME) instead of the computed
derived_lockfile_path, causing mismatch when callers pass lockfile_path; update
the fresh-resolve block that calls record_lockfile_verified(...) to pass the
derived_lockfile_path (or its Path) instead of reconstructing
workspace_root.join(Lockfile::FILE_NAME) so the same lockfile path used for
cache key/verification is recorded; locate references to derived_lockfile_path,
record_lockfile_verified, and any workspace_root.join(Lockfile::FILE_NAME) in
install.rs and replace the latter with the derived_lockfile_path value (handling
the Option/Path conversion consistently).
- Around line 719-730: The verification path is reusing the same ThrottledClient
(http_client_arc from state.http_client) so concurrent verification competes
with tarball downloads; change the caller to create and pass a dedicated HTTP
client (or a new ThrottledClient instance with its own semaphore/permit budget
or reserved permits) into build_resolution_verifiers rather than
Arc::clone(&http_client_arc) so verification traffic has an independent budget;
update the call site around build_resolution_verifiers and any type wiring
(http_client_arc, state.http_client, CreateVirtualStore usage) so the verifier
receives the new client while the frozen/install path continues to use the
original client.

In `@pacquet/crates/package-manager/src/tarball_prefetch.rs`:
- Around line 289-294: registry_tarball_url currently always uses
config.registry, causing mismatches for packages from scoped/named registries;
change registry_tarball_url to determine the package's actual registry using the
same per-package registry selection logic used by the install path (the client
registry map / per-package registry lookup) for the given PackageKey, and only
fall back to config.registry if no override exists, then build the URL from that
resolved registry, package_key.name, package_key.suffix.version(), and
name.bare.

---

Outside diff comments:
In `@installing/deps-installer/src/install/index.ts`:
- Around line 404-432: The verification promise is started too early:
verifyLockfileResolutions is invoked (creating verifyLockfilePromise) before
opts.hooks.preResolution run and can observe a lockfile that hooks will mutate;
move the creation of verifyLockfilePromise (and the derived verifyLockfile
thunk) to after the preResolution loop or alternatively call
verifyLockfileResolutions with a deep clone of ctx.wantedLockfile performed
after the hooks complete so the verifier always checks the final lockfile;
update references to verifyLockfilePromise/verifyLockfile accordingly
(functions: verifyLockfileResolutions, verifyLockfilePromise, verifyLockfile,
and opts.hooks.preResolution).

---

Nitpick comments:
In `@pacquet/crates/pnpr-client/tests/integration.rs`:
- Around line 308-355: Add a credential-forwarding integration test for the
/v1/verify-lockfile endpoint similar to the existing
forwards_credentials_and_the_identity_header test: exercise
PnprClient::verify_lockfile (using VerifyLockfileOptions::from_resolve_options
and a TestRegistry/start_pnpr setup) and assert the outgoing HTTP request both
sets the Authorization header and includes the authHeaders body shape/identity
header. Update or add the test in the same integration.rs test module to mirror
the semantics and assertions of forwards_credentials_and_the_identity_header so
any typo in PnprClient::verify_lockfile's request builder (authorization header
name/value or authHeaders payload) will fail the suite.
🪄 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: c64aa0b3-594a-434d-9ac6-9f9b93103281

📥 Commits

Reviewing files that changed from the base of the PR and between 01b3d45 and a8425b7.

📒 Files selected for processing (13)
  • .changeset/parallel-lockfile-verification.md
  • installing/deps-installer/src/install/index.ts
  • installing/deps-installer/test/install/trustLockfile.ts
  • installing/deps-restorer/src/index.ts
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/cli/tests/pnpr_install.rs
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/install_frozen_lockfile.rs
  • pacquet/crates/package-manager/src/tarball_prefetch.rs
  • pacquet/crates/pnpr-client/src/lib.rs
  • pacquet/crates/pnpr-client/tests/integration.rs
  • pnpr/crates/pnpr/src/resolver.rs
  • pnpr/crates/pnpr/src/server.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
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/pnpr-client/tests/integration.rs
  • pacquet/crates/cli/tests/pnpr_install.rs
  • pacquet/crates/package-manager/src/tarball_prefetch.rs
  • pacquet/crates/pnpr-client/src/lib.rs
  • pacquet/crates/package-manager/src/install_frozen_lockfile.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/package-manager/src/install.rs
pacquet/**/tests/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/tests/**/*.rs: Prefer real fixtures over dependency-injection seams — use tempfile::TempDir, the mocked registry, or integration tests spawning the actual binary for happy paths and most error paths; use the DI seam only for filesystem error kinds, deterministic time, shared process-global state, or external-service happy paths
Follow test-logging guidance — log before non-assert_eq! assertions, dbg! complex structures, skip logging for simple scalar assert_eq!
Tests must not be tolerant of missing build/runtime environment by silently returning early when a tool isn't found — let the test panic when a required tool is absent
Prefer #[cfg_attr(target_os = "windows", ignore = "...")] (or matching #[cfg(unix)] gates) over runtime probe-and-skip helpers for platform-locked tools
Port relevant pnpm tests to Rust tests whenever they translate when porting behavior from pnpm
Use snapshot tests with insta and carefully review diffs when intentional changes alter snapshots; accept with cargo insta review only after careful review
Tests that need the mocked registry should start pnpr through pacquet-testing-utils; cargo test / cargo nextest run should not require a separate just registry-mock launch step

Files:

  • pacquet/crates/pnpr-client/tests/integration.rs
  • pacquet/crates/cli/tests/pnpr_install.rs
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use Standard Style with trailing commas, prefer functions over classes, declare functions after they are used (relying on hoisting), limit functions to no more than two or three arguments, and use a single options object for functions needing more parameters
Follow import order: standard libraries first, then external dependencies (sorted alphabetically), then relative imports
Do not write comments that restate what the code already says; rename variables, split helpers, or move checks to more obvious places instead
Do not repeat documentation at call sites that already lives on the callee; update the JSDoc once and let every call site benefit
Use JSDoc for the function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the function body
Do not record past implementation shape, refactor history, or removed code in comments; use git log and git blame for that information instead
Write comments only when the reason for code is non-obvious, a hidden invariant exists, a workaround for a known bug is needed, or an exception to surrounding pattern is deliberate

Files:

  • installing/deps-installer/test/install/trustLockfile.ts
  • installing/deps-installer/src/install/index.ts
  • installing/deps-restorer/src/index.ts
pnpr/**/pnpr/**/*.rs

📄 CodeRabbit inference engine (pnpr/AGENTS.md)

pnpr/**/pnpr/**/*.rs: Follow the pacquet code-style guide (../pacquet/CODE_STYLE_GUIDE.md) for Rust-level conventions including imports, naming, ownership, and error handling
Follow the pacquet contributing guide (../pacquet/CONTRIBUTING.md) for test layout and Rust conventions

Files:

  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/src/resolver.rs
🧠 Learnings (8)
📚 Learning: 2026-05-26T21:01:06.666Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11966
File: .changeset/require-tarball-integrity.md:6-6
Timestamp: 2026-05-26T21:01:06.666Z
Learning: In pnpm lockfile-related release notes/docs (especially changeset markdown), preserve URL hostnames exactly as they appear in pnpm-lock.yaml tarball resolution entries—keep hosts like `codeload.github.com`, `bitbucket.org`, and `gitlab.com` in lowercase. Do not “correct” them to title-case/preserve brand capitalization (e.g., LanguageTool rules like `GITHUB` capitalization) because these are literal URL fragments, not platform brand names.

Applied to files:

  • .changeset/parallel-lockfile-verification.md
📚 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/pnpr-client/tests/integration.rs
  • pacquet/crates/cli/tests/pnpr_install.rs
  • pacquet/crates/package-manager/src/tarball_prefetch.rs
  • pacquet/crates/pnpr-client/src/lib.rs
  • pacquet/crates/package-manager/src/install_frozen_lockfile.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/package-manager/src/install.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/pnpr-client/tests/integration.rs
  • pacquet/crates/cli/tests/pnpr_install.rs
  • pacquet/crates/package-manager/src/tarball_prefetch.rs
  • pacquet/crates/pnpr-client/src/lib.rs
  • pacquet/crates/package-manager/src/install_frozen_lockfile.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/package-manager/src/install.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/pnpr-client/tests/integration.rs
  • pacquet/crates/cli/tests/pnpr_install.rs
  • pacquet/crates/package-manager/src/tarball_prefetch.rs
  • pacquet/crates/pnpr-client/src/lib.rs
  • pacquet/crates/package-manager/src/install_frozen_lockfile.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/package-manager/src/install.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/pnpr-client/tests/integration.rs
  • pacquet/crates/cli/tests/pnpr_install.rs
  • pacquet/crates/package-manager/src/tarball_prefetch.rs
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/src/resolver.rs
  • pacquet/crates/pnpr-client/src/lib.rs
  • pacquet/crates/package-manager/src/install_frozen_lockfile.rs
  • pacquet/crates/cli/src/cli_args/install.rs
  • pacquet/crates/package-manager/src/install.rs
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • installing/deps-installer/test/install/trustLockfile.ts
  • installing/deps-installer/src/install/index.ts
  • installing/deps-restorer/src/index.ts
📚 Learning: 2026-06-05T13:47:05.929Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/test/install/frozenStore.ts:2-17
Timestamp: 2026-06-05T13:47:05.929Z
Learning: In the pnpm/pnpm repository, the shared Jest preset keeps `injectGlobals` at its default (`true`), so `test` and `expect` are available as Jest globals. Therefore, reviewers should not flag (or treat as TypeScript/compilation errors) missing `import { test, expect } from 'jest/globals'` when a test file uses `test`/`expect` without importing them. Importing from `jest/globals` may still be used for consistency with sibling files, but it is not required for execution in this repo unless a Jest preset is explicitly configured with `injectGlobals: false`.

Applied to files:

  • installing/deps-installer/test/install/trustLockfile.ts
📚 Learning: 2026-06-05T13:47:26.046Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/src/install/index.ts:2337-2343
Timestamp: 2026-06-05T13:47:26.046Z
Learning: In the pnpm/pnpm codebase, `PnpmError` automatically prefixes `err.code` with `ERR_PNPM_` when you pass a code that does not already start with `ERR_PNPM_` (it normalizes `this.code` via `code.startsWith('ERR_PNPM_') ? code : `ERR_PNPM_${code}``). Therefore, during code review you should NOT flag `new PnpmError(...)` call sites for passing a bare error code (e.g., `new PnpmError('FROZEN_STORE_INCOMPATIBLE_WITH_PNPR', ...)`); the resulting `err.code` will still be `ERR_PNPM_FROZEN_STORE_INCOMPATIBLE_WITH_PNPR`.

Applied to files:

  • installing/deps-installer/test/install/trustLockfile.ts
  • installing/deps-installer/src/install/index.ts
  • installing/deps-restorer/src/index.ts
🔇 Additional comments (5)
.changeset/parallel-lockfile-verification.md (1)

1-8: LGTM!

pacquet/crates/pnpr-client/src/lib.rs (1)

85-123: LGTM!

Also applies to: 246-307, 409-454

pnpr/crates/pnpr/src/resolver.rs (1)

20-23: LGTM!

Also applies to: 300-334, 551-554

pnpr/crates/pnpr/src/server.rs (1)

163-165: LGTM!

Also applies to: 1781-1785

installing/deps-installer/test/install/trustLockfile.ts (1)

1-7: LGTM!

Also applies to: 60-82

Comment thread installing/deps-restorer/src/index.ts
Comment thread pacquet/crates/package-manager/src/install_frozen_lockfile.rs Outdated
Comment thread pacquet/crates/package-manager/src/install.rs
Comment thread pacquet/crates/package-manager/src/install.rs
Comment thread pacquet/crates/package-manager/src/tarball_prefetch.rs Outdated
…tore

On a warm store the frozen restore's prefetch_lockfile spawned one task
per lockfile entry, each doing its own SQLite lookup plus a per-file CAS
verification pass through a fresh verified-files cache — duplicating, in
un-batched form, the single batched verified pass the materializer runs
anyway. The /v1/resolve path never pays this on a verdict-cached restore
(bare done frame, no package frames), so the new frozen path regressed
the hot-cache/hot-store fresh-restore benchmark against pnpr.

Stage the candidates and filter them through one batched index.db
existence probe (StoreIndex::contains_many, keys-only SELECT) before
spawning: a fully warm store now prefetches nothing, a cold store is
unchanged, and a row whose CAS files vanished is still re-downloaded by
the materializer's per-snapshot fallback. The prefetch URLs are derived
via the shared tarball_url_and_integrity so they stay byte-identical
with the materializer's. The frozen pnpr CLI test now also swaps the
registry for a zero-expectation server before the restore, pinning that
a warm-store frozen restore makes no registry requests.
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8f19611

zkochan added 4 commits June 12, 2026 08:59
…gate

The gate only covered buildModules, but the projects' own
preinstall/postinstall hooks run outside that branch — always in the
headless installer, and in the full-resolution path whenever no new dep
paths made the build branch run. With enableModulesDir set to false the
build branch is skipped entirely, so those hooks were the one
script-execution path left ungated and could run while verification was
still in flight. Await the verdict before runLifecycleHooksConcurrently
at both call sites. The new test uses a slow-rejecting verifier: an
instantly-rejecting one aborts the install before the hooks are even
attempted, hiding a missing gate.
… error

tokio::try_join! surfaces whichever error lands first, so an unrelated
fetch failure could mask a rejected lockfile. Settle the two futures
with a select instead: a verification failure still aborts the fetch in
flight, while a fetch failure now waits for the verdict and only
surfaces once the lockfile is known trusted — mirroring pnpm's
settleInstall precedence.
…plied lockfile path

The eager gates key the lockfile-verified cache off
derived_lockfile_path, but the fresh-resolve record wrote against
workspace_root/pnpm-lock.yaml — a caller-supplied lockfile_path would
verify one path and record another, costing the next install the
verification round trip.
Each verify call targets a fresh pnpr so neither the whole-lockfile
verdict cache nor the metadata mirror warmed by an earlier call can
satisfy it without exercising the forwarded credential map.
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 9c6981b

@zkochan

zkochan commented Jun 12, 2026

Copy link
Copy Markdown
Member Author

Addressed the remaining review feedback (CodeRabbit review + Qodo report) in 8659fa3…9c6981b408:

Fixed

  • Project lifecycle hooks ran outside the verification gate (CodeRabbit, deps-restorer): the verdict is now awaited before runLifecycleHooksConcurrently in both headlessInstall and _installInContext, closing the enableModulesDir: false path and the no-new-dep-paths path. Regression test added with a slow-rejecting verifier (8659fa3).
  • try_join! could let a fetch error mask a rejected lockfile (CodeRabbit, pacquet): replaced with a select!-based settle — verification failure still cancels the fetch in flight; a fetch failure now waits for the verdict, which takes precedence. Mirrors the TS side's settleInstall (2dce29a).
  • Fresh-resolve verification recorded under the wrong path (CodeRabbit): now records under the caller-supplied lockfile_path (a37f5d5).
  • /v1/verify-lockfile credential forwarding untested (CodeRabbit nitpick): added a behavioral test — a gated package verifies only with the forwarded credential map and fails closed without it; each verify call targets a fresh pnpr so no cache can satisfy it without exercising the credential (9c6981b).

Not fixed, with rationale

  • Verification shares the fetch network budget (CodeRabbit): deliberate. Since the rebase onto perf(network): schedule tarball downloads by estimated pipeline work #12309, the ThrottledClient two-class policy gives the verifier's latency-class metadata fetches priority for freed slots beyond the downloads' reserved half — the "reserved permits" arrangement, arbitrated centrally. A separate client would defeat the global socket bound.
  • Verification starts before preResolution hooks can mutate the lockfile (CodeRabbit, outside diff): not a regression — verification ran before those hooks prior to this PR too, so a hook-mutated lockfile was never re-verified. Hooks are arbitrary local code (pnpmfile) and sit outside this gate's threat model, which targets lockfiles resolved elsewhere. Re-verifying post-hook would be a behavior change worth its own issue if desired.
  • settleInstall doesn't cancel the in-flight install (Qodo): the documented trade-off — scripts stay gated and the CLI exits on the error. True cancellation means threading an AbortSignal through the fetch/link pipeline; a sizable follow-up rather than part of this PR. The residual exposure is programmatic long-lived callers.
  • Prefetcher not shut down on the error path (Qodo): intentional. shutdown() waits for every in-flight prefetch download (each task holds a store-index writer handle), so running it on a verification failure would hold the fail-fast abort hostage to the remaining transfers. Index rows are best-effort; a dropped row only costs a later re-download. Made explicit with a comment at the early return (8f19611).

Written by an agent (Claude Code, claude-fable-5).

@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.

🧹 Nitpick comments (1)
pacquet/crates/pnpr-client/tests/integration.rs (1)

396-399: ⚡ Quick win

Narrow the failure assertion to the expected auth/verification path.

At Line 397, .is_err() is too broad and can pass on unrelated failures (transport/protocol/startup), which weakens this contract test.

Suggested tightening
-    assert!(
-        PnprClient::new(anonymous_pnpr_url).verify_lockfile(anonymous_verify_opts).await.is_err(),
-        "without the credential the gated entry's metadata fetch must fail closed",
-    );
+    let err = PnprClient::new(anonymous_pnpr_url)
+        .verify_lockfile(anonymous_verify_opts)
+        .await
+        .expect_err("without the credential, verification should fail closed");
+    assert!(
+        err.to_string().contains("401") || err.to_string().contains("unauthorized"),
+        "expected an auth-related verification failure, got: {err}",
+    );
🤖 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/pnpr-client/tests/integration.rs` around lines 396 - 399,
Capture the Result from
PnprClient::new(anonymous_pnpr_url).verify_lockfile(anonymous_verify_opts).await
into a variable and assert it specifically matches the
authentication/verification failure variant instead of using .is_err(); e.g.,
match on the error type returned by verify_lockfile (reference PnprClient and
verify_lockfile) and assert it is the expected auth/credential error variant (or
use a provided method like is_auth_error()/is_verification_failure on the
error), otherwise fail the test and include the actual error for diagnostics.
🤖 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 `@pacquet/crates/pnpr-client/tests/integration.rs`:
- Around line 396-399: Capture the Result from
PnprClient::new(anonymous_pnpr_url).verify_lockfile(anonymous_verify_opts).await
into a variable and assert it specifically matches the
authentication/verification failure variant instead of using .is_err(); e.g.,
match on the error type returned by verify_lockfile (reference PnprClient and
verify_lockfile) and assert it is the expected auth/credential error variant (or
use a provided method like is_auth_error()/is_verification_failure on the
error), otherwise fail the test and include the actual error for diagnostics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 445ab7b7-72f2-4f44-8d1e-306bda91da6c

📥 Commits

Reviewing files that changed from the base of the PR and between 8f19611 and 9c6981b.

📒 Files selected for processing (6)
  • installing/deps-installer/src/install/index.ts
  • installing/deps-installer/test/install/trustLockfile.ts
  • installing/deps-restorer/src/index.ts
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/install_frozen_lockfile.rs
  • pacquet/crates/pnpr-client/tests/integration.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • installing/deps-installer/src/install/index.ts
  • installing/deps-restorer/src/index.ts
  • pacquet/crates/package-manager/src/install.rs
  • pacquet/crates/package-manager/src/install_frozen_lockfile.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). (2)
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use Standard Style with trailing commas, prefer functions over classes, declare functions after they are used (relying on hoisting), limit functions to no more than two or three arguments, and use a single options object for functions needing more parameters
Follow import order: standard libraries first, then external dependencies (sorted alphabetically), then relative imports
Do not write comments that restate what the code already says; rename variables, split helpers, or move checks to more obvious places instead
Do not repeat documentation at call sites that already lives on the callee; update the JSDoc once and let every call site benefit
Use JSDoc for the function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the function body
Do not record past implementation shape, refactor history, or removed code in comments; use git log and git blame for that information instead
Write comments only when the reason for code is non-obvious, a hidden invariant exists, a workaround for a known bug is needed, or an exception to surrounding pattern is deliberate

Files:

  • installing/deps-installer/test/install/trustLockfile.ts
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/pnpr-client/tests/integration.rs
pacquet/**/tests/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/tests/**/*.rs: Prefer real fixtures over dependency-injection seams — use tempfile::TempDir, the mocked registry, or integration tests spawning the actual binary for happy paths and most error paths; use the DI seam only for filesystem error kinds, deterministic time, shared process-global state, or external-service happy paths
Follow test-logging guidance — log before non-assert_eq! assertions, dbg! complex structures, skip logging for simple scalar assert_eq!
Tests must not be tolerant of missing build/runtime environment by silently returning early when a tool isn't found — let the test panic when a required tool is absent
Prefer #[cfg_attr(target_os = "windows", ignore = "...")] (or matching #[cfg(unix)] gates) over runtime probe-and-skip helpers for platform-locked tools
Port relevant pnpm tests to Rust tests whenever they translate when porting behavior from pnpm
Use snapshot tests with insta and carefully review diffs when intentional changes alter snapshots; accept with cargo insta review only after careful review
Tests that need the mocked registry should start pnpr through pacquet-testing-utils; cargo test / cargo nextest run should not require a separate just registry-mock launch step

Files:

  • pacquet/crates/pnpr-client/tests/integration.rs
🧠 Learnings (7)
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • installing/deps-installer/test/install/trustLockfile.ts
📚 Learning: 2026-06-05T13:47:05.929Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/test/install/frozenStore.ts:2-17
Timestamp: 2026-06-05T13:47:05.929Z
Learning: In the pnpm/pnpm repository, the shared Jest preset keeps `injectGlobals` at its default (`true`), so `test` and `expect` are available as Jest globals. Therefore, reviewers should not flag (or treat as TypeScript/compilation errors) missing `import { test, expect } from 'jest/globals'` when a test file uses `test`/`expect` without importing them. Importing from `jest/globals` may still be used for consistency with sibling files, but it is not required for execution in this repo unless a Jest preset is explicitly configured with `injectGlobals: false`.

Applied to files:

  • installing/deps-installer/test/install/trustLockfile.ts
📚 Learning: 2026-06-05T13:47:26.046Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/src/install/index.ts:2337-2343
Timestamp: 2026-06-05T13:47:26.046Z
Learning: In the pnpm/pnpm codebase, `PnpmError` automatically prefixes `err.code` with `ERR_PNPM_` when you pass a code that does not already start with `ERR_PNPM_` (it normalizes `this.code` via `code.startsWith('ERR_PNPM_') ? code : `ERR_PNPM_${code}``). Therefore, during code review you should NOT flag `new PnpmError(...)` call sites for passing a bare error code (e.g., `new PnpmError('FROZEN_STORE_INCOMPATIBLE_WITH_PNPR', ...)`); the resulting `err.code` will still be `ERR_PNPM_FROZEN_STORE_INCOMPATIBLE_WITH_PNPR`.

Applied to files:

  • installing/deps-installer/test/install/trustLockfile.ts
📚 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/pnpr-client/tests/integration.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/pnpr-client/tests/integration.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/pnpr-client/tests/integration.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/pnpr-client/tests/integration.rs
🔇 Additional comments (1)
installing/deps-installer/test/install/trustLockfile.ts (1)

84-120: LGTM!

@zkochan zkochan merged commit c16eb0a into main Jun 12, 2026
25 checks passed
@zkochan zkochan deleted the lockfile-perf branch June 12, 2026 07:45
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