Skip to content
This repository was archived by the owner on May 14, 2026. It is now read-only.

feat: --no-optional filter for frozen lockfile install (#434 slice 5)#485

Merged
zkochan merged 4 commits into
mainfrom
feat-no-optional-plumbing
May 13, 2026
Merged

feat: --no-optional filter for frozen lockfile install (#434 slice 5)#485
zkochan merged 4 commits into
mainfrom
feat-no-optional-plumbing

Conversation

@zkochan

@zkochan zkochan commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Slice 5 of the #434 umbrella (Proper optionalDependencies support). Slices 1–4 (#439, #456, #467, #474) handle installability + the fetch-failure swallow. This slice closes the user-facing --no-optional flag: the CLI flag already exists and threads through Install::dependency_groups, but only SymlinkDirectDependencies and the on-disk included field actually honored it. The rest of the install pipeline walked optional_dependencies unconditionally, so the optional subtree was still extracted, linked, and built.

Mirrors upstream's depNode filter at installing/deps-installer/src/install/link.ts:109-111:

if (!opts.include.optionalDependencies) {
  depNodes = depNodes.filter(({ optional }) => !optional)
}

Changes

  • SkippedSnapshots: gains a third disjoint subset optional_excluded alongside installability (slice 1) and fetch_failed (slice 4). The existing contains / iter already return the union, so every downstream consumer that filters by skip-set (CreateVirtualStore, SymlinkDirectDependencies, BuildModules, hoist, link_bins) now respects --no-optional through the same gate. iter_installability still returns only the persistent subset for .modules.yaml.skipped writes — --no-optional exclusions are transient (matching upstream's behavior of never updating opts.skipped at the filter site). The three subsets are kept truly disjoint by write-time no-op guards on the transient inserts: precedence is installability > fetch_failed > optional_excluded, and the guards are symmetric so call order can't corrupt the invariant.
  • InstallFrozenLockfile::run: iterates the lockfile snapshots once and inserts every snap.optional == true entry into the new subset when the dispatch list doesn't contain DependencyGroup::Optional. The SnapshotEntry::optional flag is set by the resolver only when the snapshot is reachable exclusively through optional edges, so a snapshot reachable through any non-optional edge carries optional: false and survives the filter — covers optionalDependencies.ts:712 (dependency that is both optional and non-optional is installed).
  • symlink_hoisted_dependencies (drive-by fix): now takes the skip set and short-circuits skipped node IDs. The hoist module records (child_id, alias) → kind in hoisted_dependencies_by_node_id before its existing skipped-guard, mirroring upstream's structure — and the symlink pass walked every entry. graph.get(node_id) wasn't enough of a guard because build_hoist_graph includes every snapshot whose metadata is present (skipped or not). Without the new filter, a prod dep with an optional transitive child would get a dangling hoist symlink on Unix (failing as a junction on Windows). The latent bug pre-dated slice 5 but only became reachable now that --no-optional populates the skip set with snapshots whose slot CreateVirtualStore doesn't create.

Test plan

  • install::tests::frozen_install_no_optional_drops_optional_only_snapshots — discriminating fixture: an optional: true snapshot whose metadata row is missing from packages:. Slice 4 (fetch-failure swallow) only covers DownloadTarball/GitFetch so MissingPackageMetadata propagates even on optional — meaning install success proves the snapshot was dropped before cache-key derivation by the slice 5 filter. Test-the-test verified by inverting the !include_optional guard.
  • install::tests::frozen_install_optional_included_surfaces_missing_metadata — pins the polarity: same fixture, Optional in the dispatch list, install must abort with MissingPackageMetadata.
  • install::tests::frozen_install_no_optional_keeps_shared_non_optional_snapshot — regression for optionalDependencies.ts:712: a snapshot with optional: false listed under optionalDependencies must NOT be dropped by --no-optional. Same MissingPackageMetadata discriminator.
  • installability::tests::disjoint_subsets_preserve_len_and_iter — pins the disjoint-subset invariant: a key added to multiple subsets ends up in only the highest-precedence one. Test-the-test verified by removing the guards.
  • installability::tests::fetch_failed_and_optional_excluded_are_symmetric — both insertion orders end up with the same state (only one subset carries the key). Test-the-test verified by removing the new symmetric guard.
  • hoist::tests::symlink_skips_dropped_nodes — exercises symlink_hoisted_dependencies directly with one skipped node; uses symlink_metadata (not exists) to catch dangling-symlink regressions. Test-the-test verified by removing the new filter.
  • Error assertions are typed matches! on InstallError::FrozenLockfile(InstallFrozenLockfileError::CreateVirtualStore(CreateVirtualStoreError::MissingPackageMetadata { .. })) rather than Debug substring checks — stable against rename/wrapping changes.
  • just ready (typos + fmt + check + nextest + clippy).
  • taplo format --check.
  • just dylint (perfectionist).
  • RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace.

Out of scope

  • installing only optional deps (deps-restorer:300) — pacquet's CLI doesn't expose --only=optional yet.
  • optional dependency has bigger priority than regular dependency (optionalDependencies.ts:419) — resolver-side priority rule, not relevant to the lockfile-driven path.
  • Current-lockfile pruning when the include set changes between installs — umbrella slice 6.

Closes #480.


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

Pacquet's CLI already exposes `--no-optional` and threads it as a
`DependencyGroup` filter through `Install::dependency_groups`. The
flag is honored by `SymlinkDirectDependencies` (which already
filters by group) and by the on-disk `.modules.yaml.included`
field, but the rest of the install pipeline walks the lockfile's
`optional_dependencies` maps unconditionally — so the optional
subtree was still extracted, linked, and built even when the user
asked it to be skipped.

This slice closes that gap by adding the same filter upstream
applies at
`installing/deps-installer/src/install/link.ts:109-111` to
pacquet's centralized skip-set plumbing:

- `SkippedSnapshots` gains a third disjoint subset
  `optional_excluded` alongside `installability` (slice 1) and
  `fetch_failed` (slice 4). `contains` / `iter` already return the
  union, so every downstream consumer that filters by skip-set
  (`CreateVirtualStore`, `SymlinkDirectDependencies`,
  `BuildModules`, hoist, link_bins) now respects `--no-optional`
  through the same gate.
- `iter_installability` still returns only the persistent subset
  for `.modules.yaml.skipped` writes — `--no-optional` exclusions
  are transient (matching upstream's behavior of never adding to
  `opts.skipped` at the filter site), so a later install without
  `--no-optional` brings the snapshots back into the install graph.
- `InstallFrozenLockfile::run` iterates the lockfile snapshots
  once after the installability + seed pass and inserts every
  `snap.optional == true` entry into the new subset when the
  dispatch list doesn't contain `DependencyGroup::Optional`. The
  `SnapshotEntry::optional` flag is set by the resolver only when
  the snapshot is reachable **exclusively** through optional
  edges, so a snapshot reachable both optionally and non-optionally
  carries `optional: false` and survives the filter — covers
  upstream's `optionalDependencies.ts:712` case (`dependency that
  is both optional and non-optional is installed`).

Tests:

- `install::tests::frozen_install_no_optional_drops_optional_only_snapshots`
  uses a discriminating fixture: an `optional: true` snapshot whose
  metadata row is missing from `packages:`. Slice 4 (fetch-failure
  swallow) only covers `DownloadTarball`/`GitFetch` so
  `MissingPackageMetadata` propagates even on optional — meaning
  install success proves the snapshot was dropped **before**
  cache-key derivation by the slice 5 filter. Test-the-test:
  inverting the `!include_optional` guard surfaces
  `MissingPackageMetadata`.
- `install::tests::frozen_install_optional_included_surfaces_missing_metadata`
  pins the polarity: same fixture, `Optional` in the dispatch list,
  install must abort with `MissingPackageMetadata`.

Out of scope:
- `installing only optional deps` — pacquet has no CLI flag for that yet.
- `optional dependency has bigger priority than regular dependency` — resolver-side priority rule.
- Current-lockfile pruning when the include set changes between installs — umbrella slice 6.

Closes #480.
Copilot AI review requested due to automatic review settings May 13, 2026 19:51
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1753cbad-04dd-4e4d-a62c-70c720c182f7

📥 Commits

Reviewing files that changed from the base of the PR and between a1658f9 and 7c74c71.

📒 Files selected for processing (6)
  • crates/package-manager/src/hoist.rs
  • crates/package-manager/src/hoist/tests.rs
  • crates/package-manager/src/install/tests.rs
  • crates/package-manager/src/install_frozen_lockfile.rs
  • crates/package-manager/src/installability.rs
  • crates/package-manager/src/installability/tests.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/package-manager/src/installability/tests.rs
  • crates/package-manager/src/install/tests.rs
  • crates/package-manager/src/installability.rs
  • crates/package-manager/src/install_frozen_lockfile.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Preserve existing method chains and pipe-trait chains; do not break them into intermediate let bindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Path over &PathBuf, &str over &String) when it does not force extra copies.
Prefer Arc::clone(&x) and Rc::clone(&x) over x.clone() for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*; for any glob whose target is a module you control. External-crate preludes (e.g., use rayon::prelude::*;) and root-of-module re-exports (e.g., pub use submodule::*; in lib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plain String or &str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only via TryFrom<String> and/or FromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallible From<String> and From<&str> constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bare as assertion), add a from_str_unchecked (or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...

Files:

  • crates/package-manager/src/hoist.rs
  • crates/package-manager/src/hoist/tests.rs
🧠 Learnings (4)
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.

Applied to files:

  • crates/package-manager/src/hoist.rs
  • crates/package-manager/src/hoist/tests.rs
📚 Learning: 2026-05-13T19:22:48.951Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 478
File: crates/package-manager/src/hoisted_dep_graph.rs:51-55
Timestamp: 2026-05-13T19:22:48.951Z
Learning: When reviewing this Rust codebase, avoid introducing/using a newtype like `PkgIdWithPatchHash` in only one module (e.g., `hoisted_dep_graph.rs`) if other related pacquet modules still represent the upstream pnpm “pkg id with patch hash” as a plain `String` (e.g., `virtual_store_layout`). For type consistency, either keep `pkg_id_with_patch_hash` as `String` here, or require a workspace-wide sweep that defines/extracts the `PkgIdWithPatchHash` newtype once and updates all call sites together (otherwise defer the refactor to a coordinated follow-up PR).

Applied to files:

  • crates/package-manager/src/hoist.rs
  • crates/package-manager/src/hoist/tests.rs
📚 Learning: 2026-05-13T20:09:22.171Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 486
File: crates/package-manager/src/hoisted_dep_graph.rs:472-476
Timestamp: 2026-05-13T20:09:22.171Z
Learning: In pnpm/pacquet, when generating serialized `hoisted_locations` strings (used for `.modules.yaml`), normalize path separators to forward slashes (e.g., replace `\\` with `/`). This preserves cross-platform portability: the output must not use OS-native separators because upstream pnpm’s `path.relative` can return platform-specific separators, which would break consistency with pnpm/pacquet’s other serialized formats on Windows.

Applied to files:

  • crates/package-manager/src/hoist.rs
  • crates/package-manager/src/hoist/tests.rs
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.

Applied to files:

  • crates/package-manager/src/hoist/tests.rs
🔇 Additional comments (2)
crates/package-manager/src/hoist.rs (1)

480-480: LGTM!

Also applies to: 506-519

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

331-338: LGTM!

Also applies to: 342-420


📝 Walkthrough

Walkthrough

This PR records lockfile snapshots with snapshot.optional == true into a transient optional_excluded subset when DependencyGroup::Optional is excluded during frozen installs, threads that skip set into hoisting/symlinking so skipped snapshots are not symlinked, and adds unit and e2e tests validating the behavior.

Changes

--no-optional frozen install support

Layer / File(s) Summary
SkippedSnapshots optional exclusion tracking
crates/package-manager/src/installability.rs
SkippedSnapshots gains an optional_excluded HashSet; docs, constructors, add_fetch_failed guards, new add_optional_excluded, and union semantics for contains/len/is_empty and iteration are updated.
Frozen install optional filtering
crates/package-manager/src/install_frozen_lockfile.rs
InstallFrozenLockfile::run checks dependency_groups for DependencyGroup::Optional; when excluded it records snapshots with optional: true into the in-memory skipped set via add_optional_excluded and passes the hoist skip set to downstream phases.
Hoist symlink filtering
crates/package-manager/src/hoist.rs
symlink_hoisted_dependencies now accepts a skipped: &HashSet<PackageKey> parameter and ignores nodes in that set during symlink work collection to avoid creating dangling virtual-store aliases.
Hoist tests
crates/package-manager/src/hoist/tests.rs
Assert skipped snapshot remains present in hoist structures and add symlink_skips_dropped_nodes to verify dropped nodes do not create symlinks.
Frozen-install e2e tests
crates/package-manager/src/install/tests.rs
Three e2e tests: dropping optional-only snapshots when Optional is excluded (install succeeds, no virtual-store slot, .modules.yaml.skipped empty), polarity test including Optional (install fails with MissingPackageMetadata), and regression ensuring a shared non-optional snapshot is not dropped by --no-optional (install fails).
Installability unit tests
crates/package-manager/src/installability/tests.rs
Two unit tests asserting disjoint-subset precedence and that add_optional_excluded and add_fetch_failed are symmetric/no-ops when the other subset already contains the key.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pacquet#467 — Modifies the same frozen-install skipped-snapshots plumbing and persistence behavior.
  • pnpm/pacquet#439 — Related work on skipped optional snapshots and installability gating.
  • pnpm/pacquet#474 — Adds complementary skipped-subset handling (fetch_failed) overlapping this PR.

Poem

🐰 I nibble code in moonlit hops,
I skip optional crumbs and dance on stops,
Frozen locks yield tidy rows,
No dangling links where soft wind blows,
🥕 a small hop, a tidy prune — success grows.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: implementing the --no-optional filter for frozen lockfile installation as part of slice 5 of the umbrella effort.
Description check ✅ Passed The PR description is comprehensive and follows the template structure with clear Summary, Linked issue, Upstream reference, and Checklist sections. It thoroughly explains the motivation, changes, test plan, and what is out of scope.
Linked Issues check ✅ Passed The code changes directly implement all requirements from #480: SkippedSnapshots gains optional_excluded subset; InstallFrozenLockfile filters optional snapshots when DependencyGroup::Optional is absent; downstream consumers (CreateVirtualStore, hoist, symlink) respect the skip-set; three e2e tests validate behavior with/without optional inclusion and verify shared deps are preserved.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to implementing --no-optional filtering for frozen lockfile installs. No unrelated modifications to unrelated subsystems or out-of-scope feature work are present.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-no-optional-plumbing

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/package-manager/src/installability.rs`:
- Around line 131-133: The add_optional_excluded method currently inserts into
optional_excluded unconditionally which can violate the disjoint-subset
invariant with the installability set and cause len()/iter() to
overcount/duplicate; update add_optional_excluded to first remove the given
PackageKey from any conflicting set(s) (at minimum remove from installability)
before inserting into optional_excluded so the sets remain disjoint and
downstream len()/iter() behave correctly; reference the add_optional_excluded
function and the optional_excluded and installability collections when making
the change.
🪄 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: 4b128e79-66ea-4334-98a3-2290cfc5a4ed

📥 Commits

Reviewing files that changed from the base of the PR and between 996eb08 and 9d9f5c7.

📒 Files selected for processing (3)
  • crates/package-manager/src/install/tests.rs
  • crates/package-manager/src/install_frozen_lockfile.rs
  • crates/package-manager/src/installability.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). (8)
  • GitHub Check: copilot-pull-request-reviewer
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Code Coverage
  • GitHub Check: Dylint
  • GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Preserve existing method chains and pipe-trait chains; do not break them into intermediate let bindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Path over &PathBuf, &str over &String) when it does not force extra copies.
Prefer Arc::clone(&x) and Rc::clone(&x) over x.clone() for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*; for any glob whose target is a module you control. External-crate preludes (e.g., use rayon::prelude::*;) and root-of-module re-exports (e.g., pub use submodule::*; in lib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plain String or &str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only via TryFrom<String> and/or FromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallible From<String> and From<&str> constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bare as assertion), add a from_str_unchecked (or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...

Files:

  • crates/package-manager/src/install_frozen_lockfile.rs
  • crates/package-manager/src/install/tests.rs
  • crates/package-manager/src/installability.rs
🧠 Learnings (3)
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.

Applied to files:

  • crates/package-manager/src/install_frozen_lockfile.rs
  • crates/package-manager/src/install/tests.rs
  • crates/package-manager/src/installability.rs
📚 Learning: 2026-05-13T19:22:48.951Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 478
File: crates/package-manager/src/hoisted_dep_graph.rs:51-55
Timestamp: 2026-05-13T19:22:48.951Z
Learning: When reviewing this Rust codebase, avoid introducing/using a newtype like `PkgIdWithPatchHash` in only one module (e.g., `hoisted_dep_graph.rs`) if other related pacquet modules still represent the upstream pnpm “pkg id with patch hash” as a plain `String` (e.g., `virtual_store_layout`). For type consistency, either keep `pkg_id_with_patch_hash` as `String` here, or require a workspace-wide sweep that defines/extracts the `PkgIdWithPatchHash` newtype once and updates all call sites together (otherwise defer the refactor to a coordinated follow-up PR).

Applied to files:

  • crates/package-manager/src/install_frozen_lockfile.rs
  • crates/package-manager/src/install/tests.rs
  • crates/package-manager/src/installability.rs
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.

Applied to files:

  • crates/package-manager/src/install/tests.rs
🔇 Additional comments (2)
crates/package-manager/src/install_frozen_lockfile.rs (1)

322-350: LGTM!

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

1892-2074: LGTM!

Comment thread crates/package-manager/src/installability.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds frozen-lockfile --no-optional enforcement by treating optional-only snapshots as transiently skipped, so the existing install pipeline filters them out consistently.

Changes:

  • Extends SkippedSnapshots with an optional_excluded subset.
  • Marks optional: true snapshots as skipped when DependencyGroup::Optional is not included.
  • Adds frozen install tests for excluding optional-only snapshots and preserving polarity when optional deps are included.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
crates/package-manager/src/installability.rs Adds transient optional-exclusion tracking to the skipped snapshot set.
crates/package-manager/src/install_frozen_lockfile.rs Applies the --no-optional snapshot filter before virtual-store creation and downstream phases.
crates/package-manager/src/install/tests.rs Adds integration coverage for optional-only frozen-install filtering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/package-manager/src/install_frozen_lockfile.rs
Comment thread crates/package-manager/src/installability.rs
Comment thread crates/package-manager/src/installability.rs
@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.89%. Comparing base (bd99486) to head (7c74c71).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #485      +/-   ##
==========================================
- Coverage   90.12%   88.89%   -1.23%     
==========================================
  Files         119      121       +2     
  Lines       11134    12625    +1491     
==========================================
+ Hits        10034    11223    +1189     
- Misses       1100     1402     +302     

☔ View full report in Codecov by Sentry.
📢 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 May 13, 2026

Copy link
Copy Markdown

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.02     16.0±0.36ms   270.5 KB/sec    1.00     15.8±0.14ms   275.3 KB/sec

@github-actions

github-actions Bot commented May 13, 2026

Copy link
Copy Markdown

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.647 ± 0.059 2.569 2.745 1.02 ± 0.04
pacquet@main 2.603 ± 0.080 2.496 2.703 1.00
pnpm 6.155 ± 0.103 6.033 6.344 2.36 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.6467504701600006,
      "stddev": 0.059488878785868546,
      "median": 2.63671775116,
      "user": 2.5540966000000003,
      "system": 3.6623954399999996,
      "min": 2.56875661316,
      "max": 2.7454782251600003,
      "times": [
        2.7454782251600003,
        2.6295028261600004,
        2.5692338041600005,
        2.7312844431600003,
        2.6416167121600003,
        2.6821862431600003,
        2.56875661316,
        2.6147335871600004,
        2.63181879016,
        2.6528934571600002
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.60283824436,
      "stddev": 0.07999795873212051,
      "median": 2.6163977376600003,
      "user": 2.5580432999999996,
      "system": 3.6329725399999995,
      "min": 2.49601713316,
      "max": 2.7032597861600003,
      "times": [
        2.6926166801600004,
        2.49760521516,
        2.53426000016,
        2.59626986216,
        2.49601713316,
        2.7032597861600003,
        2.54608300616,
        2.6365256131600003,
        2.64474372516,
        2.6810014221600005
      ]
    },
    {
      "command": "pnpm",
      "mean": 6.154724399359999,
      "stddev": 0.10283887438001102,
      "median": 6.11256276866,
      "user": 9.072979199999997,
      "system": 4.41276634,
      "min": 6.0327698861600005,
      "max": 6.34359733316,
      "times": [
        6.12191005016,
        6.07482710116,
        6.16816359316,
        6.09962990416,
        6.10321548716,
        6.09222152716,
        6.1958419001600005,
        6.34359733316,
        6.315067211160001,
        6.0327698861600005
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 741.1 ± 65.4 703.1 918.5 1.00
pacquet@main 769.4 ± 56.9 702.8 866.2 1.04 ± 0.12
pnpm 2614.0 ± 106.3 2513.1 2790.2 3.53 ± 0.34
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.74107942062,
      "stddev": 0.06537218684142339,
      "median": 0.71977120852,
      "user": 0.36788844,
      "system": 1.59371352,
      "min": 0.7030610725199999,
      "max": 0.91851155152,
      "times": [
        0.91851155152,
        0.71139937752,
        0.72808821252,
        0.73541848852,
        0.7030610725199999,
        0.72340133952,
        0.71614107752,
        0.70328536852,
        0.70386389252,
        0.76762382552
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.76936479692,
      "stddev": 0.05691142259762945,
      "median": 0.74955895552,
      "user": 0.36904184,
      "system": 1.59751052,
      "min": 0.70282586452,
      "max": 0.86616264152,
      "times": [
        0.8459666475200001,
        0.76512672852,
        0.86616264152,
        0.75018386152,
        0.70282586452,
        0.74893404952,
        0.8261648365200001,
        0.70894952452,
        0.73510452952,
        0.74422928552
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.6140374841200003,
      "stddev": 0.10625185536970244,
      "median": 2.5988382015200004,
      "user": 3.1505990400000004,
      "system": 2.1646604199999997,
      "min": 2.5130569345200002,
      "max": 2.79015714252,
      "times": [
        2.79015714252,
        2.64963807752,
        2.67272718352,
        2.51806401352,
        2.55418917852,
        2.5189245285200004,
        2.5143341335200002,
        2.6434872245200003,
        2.5130569345200002,
        2.7657964245200004
      ]
    }
  ]
}

…ared-dep regression test

Addresses PR #485 review:

- **CodeRabbit + Copilot (×2)**: `add_optional_excluded` and
  `add_fetch_failed` inserted unconditionally, which let the same
  key live in multiple subsets when overlaps occur (the realistic
  case: a platform-incompatible optional snapshot installed with
  `--no-optional` would land in both `installability` and
  `optional_excluded`). That made `len()` overcount and `iter()`
  emit duplicates, breaking the disjoint-subset invariant
  `contains` already promised.

  Fix: write-time guards. `add_fetch_failed` no-ops if
  `installability` already has the key; `add_optional_excluded`
  no-ops if either `installability` or `fetch_failed` does. The
  precedence is "installability wins" (it's the only persistent
  subset, so a snapshot recorded as installability-skipped should
  stay that way on the next install's seed). New unit tests
  `disjoint_subsets_preserve_len_and_iter` and
  `fetch_failed_takes_precedence_over_optional_excluded` pin the
  invariant — test-the-test verified by removing the guards.

- **Copilot**: missing regression test for the shared-dependency
  case from `optionalDependencies.ts:712`. Added
  `frozen_install_no_optional_keeps_shared_non_optional_snapshot`:
  fixture has a snapshot listed under `optionalDependencies` but
  with `optional: false` on the snapshot entry (the upstream
  resolver's marker for "reachable through any non-optional
  edge"). With `--no-optional`, the slice 5 filter must NOT drop
  this snapshot. Discriminating signal: metadata is missing from
  `packages:`, so a wrong drop would silently succeed; a correct
  install instead aborts with `MissingPackageMetadata`.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/package-manager/src/installability/tests.rs (1)

552-556: ⚡ Quick win

Add failure context to the bare membership assertion.

The assert!(skipped.contains(&key)); at Line 555 is hard to debug on failure. Please add context (message or eprintln!) with the collected keys.

Based on learnings: In Rust test code, add logging for non-assert_eq! assertions so failures are diagnosable without rerunning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/package-manager/src/installability/tests.rs` around lines 552 - 556,
The bare membership assertion fails without context; update the test to include
diagnostic output when the check fails by changing the assert to include a
message or printing the collected keys, e.g. use the variables skipped,
collected and key to produce context (for example:
assert!(skipped.contains(&key), "skipped keys: {:?}", collected) or eprintln!
before the assert) so failures show the collected keys for debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/package-manager/src/install/tests.rs`:
- Around line 2068-2072: The test currently asserts the error by stringifying
the Debug output and checking for "MissingPackageMetadata"; instead, directly
pattern-match the concrete InstallError variant to make the assertion robust:
replace the Debug-substring check on msg with a match or assert matches! against
InstallError::MissingPackageMetadata (or destructure to inspect inner fields)
for the failing result from the test function (the current test and the other
test frozen_install_no_optional_keeps_shared_non_optional_snapshot), referencing
the returned error value (err/result) and using the InstallError enum variant
names to locate and update the assertions.

---

Nitpick comments:
In `@crates/package-manager/src/installability/tests.rs`:
- Around line 552-556: The bare membership assertion fails without context;
update the test to include diagnostic output when the check fails by changing
the assert to include a message or printing the collected keys, e.g. use the
variables skipped, collected and key to produce context (for example:
assert!(skipped.contains(&key), "skipped keys: {:?}", collected) or eprintln!
before the assert) so failures show the collected keys for debugging.
🪄 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: b270cd92-dff6-486f-82d4-ff3c60765452

📥 Commits

Reviewing files that changed from the base of the PR and between 9d9f5c7 and 268ee2b.

📒 Files selected for processing (3)
  • crates/package-manager/src/install/tests.rs
  • crates/package-manager/src/installability.rs
  • crates/package-manager/src/installability/tests.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/package-manager/src/installability.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Code Coverage
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Dylint
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Preserve existing method chains and pipe-trait chains; do not break them into intermediate let bindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Path over &PathBuf, &str over &String) when it does not force extra copies.
Prefer Arc::clone(&x) and Rc::clone(&x) over x.clone() for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*; for any glob whose target is a module you control. External-crate preludes (e.g., use rayon::prelude::*;) and root-of-module re-exports (e.g., pub use submodule::*; in lib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plain String or &str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only via TryFrom<String> and/or FromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallible From<String> and From<&str> constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bare as assertion), add a from_str_unchecked (or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...

Files:

  • crates/package-manager/src/installability/tests.rs
  • crates/package-manager/src/install/tests.rs
🧠 Learnings (4)
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.

Applied to files:

  • crates/package-manager/src/installability/tests.rs
  • crates/package-manager/src/install/tests.rs
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.

Applied to files:

  • crates/package-manager/src/installability/tests.rs
  • crates/package-manager/src/install/tests.rs
📚 Learning: 2026-05-13T19:22:48.951Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 478
File: crates/package-manager/src/hoisted_dep_graph.rs:51-55
Timestamp: 2026-05-13T19:22:48.951Z
Learning: When reviewing this Rust codebase, avoid introducing/using a newtype like `PkgIdWithPatchHash` in only one module (e.g., `hoisted_dep_graph.rs`) if other related pacquet modules still represent the upstream pnpm “pkg id with patch hash” as a plain `String` (e.g., `virtual_store_layout`). For type consistency, either keep `pkg_id_with_patch_hash` as `String` here, or require a workspace-wide sweep that defines/extracts the `PkgIdWithPatchHash` newtype once and updates all call sites together (otherwise defer the refactor to a coordinated follow-up PR).

Applied to files:

  • crates/package-manager/src/installability/tests.rs
  • crates/package-manager/src/install/tests.rs
📚 Learning: 2026-05-13T20:09:22.171Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 486
File: crates/package-manager/src/hoisted_dep_graph.rs:472-476
Timestamp: 2026-05-13T20:09:22.171Z
Learning: In pnpm/pacquet, when generating serialized `hoisted_locations` strings (used for `.modules.yaml`), normalize path separators to forward slashes (e.g., replace `\\` with `/`). This preserves cross-platform portability: the output must not use OS-native separators because upstream pnpm’s `path.relative` can return platform-specific separators, which would break consistency with pnpm/pacquet’s other serialized formats on Windows.

Applied to files:

  • crates/package-manager/src/installability/tests.rs
  • crates/package-manager/src/install/tests.rs
🔇 Additional comments (1)
crates/package-manager/src/installability/tests.rs (1)

570-578: LGTM!

Comment thread crates/package-manager/src/install/tests.rs
… Debug substring

CodeRabbit review on PR #485 flagged that
`msg.contains("MissingPackageMetadata")` couples the test to
`Debug` formatting — a rename or surrounding-context shift would
silently break the assertion's discrimination. Replace both
sites in the slice 5 install tests with a typed `matches!` on
the full error chain:

  InstallError::FrozenLockfile(
    InstallFrozenLockfileError::CreateVirtualStore(
      CreateVirtualStoreError::MissingPackageMetadata { .. },
    ),
  )

Test intent now lives in the type system rather than in a debug
string; both tests still pass.
Copilot AI review requested due to automatic review settings May 13, 2026 20:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment thread crates/package-manager/src/install/tests.rs Outdated
Comment thread crates/package-manager/src/install_frozen_lockfile.rs
Comment thread crates/package-manager/src/installability.rs Outdated
…mmetric fetch_failed guard

PR #485 review (Copilot, 2026-05-13):

- **Hoist symlink pass missed the skip filter**:
  `get_hoisted_dependencies` records `(child_id, alias) → kind`
  in `hoisted_dependencies_by_node_id` before the skipped guard at
  hoist.rs:388, and the symlink pass walks every entry. So a prod
  dep with an optional transitive child could still get a hoisted
  symlink to a slot `CreateVirtualStore` skipped — dangling on
  Unix, failing as a junction on Windows. The
  `graph.get(node_id)` guard isn't enough because `build_hoist_graph`
  doesn't filter by skip (it includes every snapshot whose
  metadata is present). Fix: thread the skip set into
  `symlink_hoisted_dependencies` and short-circuit there. Updated
  the call site in `InstallFrozenLockfile::run` to pass the
  existing `hoist_skipped` set.

  New unit test `hoist::tests::symlink_skips_dropped_nodes`
  exercises the pass directly: two nodes, one in the skip set;
  the kept alias's symlink is created, the dropped alias has
  none. Uses `symlink_metadata` (not `exists`) to catch the
  dangling-symlink regression — `exists` follows the link so a
  dangling target would mask the bug. Test-the-test verified by
  removing the new filter.

  Also updates the misleading doc comment on
  `skipped_snapshot_is_excluded` that claimed `graph.get(node_id)`
  was the symlink-side guard.

- **`add_fetch_failed` lacked the symmetric guard against
  `optional_excluded`**: the public API now no-ops symmetrically
  with `add_optional_excluded` so call order can't corrupt the
  disjoint-subset invariant. The
  `fetch_failed_and_optional_excluded_are_symmetric` test now
  covers both insertion orders; renamed from the prior
  `_takes_precedence_over_` name to reflect the symmetric
  semantics. Test-the-test verified by removing the new guard.

- **Test comment typo**: `AddDependencyOptions::dependency_groups()`
  → `InstallDependencyOptions::dependency_groups()` in
  `frozen_install_no_optional_drops_optional_only_snapshots`. The
  install CLI uses the `Install` variant; `Add` lives in
  `cli_args/add.rs`.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plumb --no-optional through the install graph (#434 slice 5)

2 participants