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

feat(package-is-installable): platform + engine check, skip optional incompatibles (#434 slice 1)#439

Merged
zkochan merged 13 commits into
mainfrom
feat-installability-check
May 13, 2026
Merged

feat(package-is-installable): platform + engine check, skip optional incompatibles (#434 slice 1)#439
zkochan merged 13 commits into
mainfrom
feat-installability-check

Conversation

@zkochan

@zkochan zkochan commented May 13, 2026

Copy link
Copy Markdown
Member

Summary

Slice 1 of #434 — foundational installability gate. Ports
@pnpm/config.package-is-installable
and threads the resulting skip-set through every phase of the
frozen-lockfile install pipeline.

Closes #266 once shipped (covers the "install respects every snapshot
— no os/cpu/libc filter" gap). Does not close #434 — that umbrella
has six more slices to follow.

Upstream reference: pnpm/pnpm@94240bc046.

What landed

New crate: pacquet-package-is-installable

Ports the upstream config/package-is-installable package's three
helpers:

  • check_platform (Option<&[String]> for each os/cpu/libc
    axis, plus a SupportedArchitectures override) — returns
    Option<UnsupportedPlatformError> matching upstream's
    ERR_PNPM_UNSUPPORTED_PLATFORM code, message shape, and JSON
    payload. Handles negation entries (!foo), the any sentinel,
    the current placeholder, and the currentLibc !== 'unknown'
    skip from checkPlatform.ts:38.
  • check_engine — evaluates engines.node / engines.pnpm via
    node-semver. Approximates npm-semver's includePrerelease: true
    via a strip-prerelease fallback; one over-acceptance edge case
    (>=X.Y.Z against X.Y.Z-rc1) is pinned in the known-failures
    integration test for follow-up.
  • package_is_installable — composes the two, returns the
    tri-state verdict matching upstream's boolean | null (Installable
    / SkipOptional / ProceedWithWarning), plus an Err arm for
    engine_strict aborts and ERR_PNPM_INVALID_NODE_VERSION.

InstallabilityOptions<'a> borrows its host strings so a caller
running through many snapshots in a row can build the host part
once and only toggle optional per snapshot. WantedPlatformRef<'a>
plays the same role for the manifest axes so check_platform runs
the happy path without any allocation.

New module: pacquet_package_manager::installability

compute_skipped_snapshots is the per-install entry point. For each
snapshot:

  1. Look up the matching PackageMetadata.
  2. Run check_package (cached per peer-stripped metadata_key so
    peer-resolved variants of the same package share one verdict).
  3. Dispatch on (verdict, snapshot.optional, host.engine_strict):
    • Installable: nothing to do.
    • SkipOptional + optional: add to SkippedSnapshots, emit
      pnpm:skipped-optional-dependency (deduped per metadata key,
      matching upstream's emit-per-pkgId).
    • Incompatible + non-optional + engine_strict: abort.
    • Incompatible + non-optional + non-strict: tracing::warn!
      and proceed. (Upstream's pnpm:install-check channel isn't
      wired into pacquet's reporter yet — slice 1 follow-up.)

any_installability_constraint(packages) is the caller-side fast
path: if no metadata row declares an engines.{node,pnpm} or a
non-empty / non-["any"] cpu/os/libc, the entire installability
pass is skipped. The probe runs synchronously in
install_frozen_lockfile before the tokio::task::spawn_blocking
that would invoke node --version — so the common no-constraints
lockfile pays nothing for the new pipeline, restoring main's overlap
of node-binary startup with extraction.

Install-pipeline plumbing

The SkippedSnapshots set is threaded into every downstream phase
of InstallFrozenLockfile::run:

  • CreateVirtualStore: installability-skipped snapshots are
    dropped from both survivors (no virtual-store slot extracted)
    and skipped_entries (no warm-cache row). Layered ahead of
    main's feat: partial install with --frozen-lockfile (#433) #442 already-installed-and-on-disk skip filter.
  • SymlinkDirectDependencies: a direct dep whose resolved
    snapshot is in the skip set is omitted from node_modules/<name>
    (no symlink, no pnpm:root added event, no bin link).
  • LinkVirtualStoreBins: per-slot bin link skips slots whose
    snapshot is installability-skipped (their virtual-store
    directories don't exist).
  • BuildModules via build_sequence: get_subgraph_to_build
    consults skipped before recursion, so a skipped snapshot's
    subtree doesn't contribute to the build graph via that edge.
    Descendants reachable from a non-skipped root still build
    normally.

Performance

CI integrated-benchmark on the 1352-package fixture, latest run:

Scenario pacquet@HEAD pacquet@main Relative
Frozen Lockfile (cold) 2.476 ± 0.083 s 2.442 ± 0.071 s 1.01 ± 0.05
Frozen Lockfile (Hot Cache) 685.8 ± 59.3 ms 700.2 ± 47.4 ms 1.00

Earlier iterations of this PR showed a ~5% cold-install regression
from the node --version spawn landing on the extraction critical
path. Closed by hoisting the no-constraints fast-path probe to the
caller (commit cf47ce51) so the spawn is gated on actual
constraint presence.

Other perf passes folded in:

  • compute_skipped_snapshots caches the per-metadata-row check
    verdict so peer-resolved variants share one check_package call.
  • check_platform borrows its three wanted axes through
    WantedPlatformRef<'a>; the owned WantedPlatform only
    materialises in the error path.

Tests

Suite Count What it covers
pacquet-package-is-installable::tests::check_platform 16 Port of upstream checkPlatform.tsany/current sentinels, negation, supportedArchitectures override, libc unknown-skip
pacquet-package-is-installable::tests::check_engine 7 Port of upstream checkEngine.ts — node/pnpm range checks, prerelease cases, ERR_PNPM_INVALID_NODE_VERSION
pacquet-package-is-installable::tests::package_is_installable 6 Tri-state verdict + optional/engine-strict dispatch
pacquet-package-is-installable::tests::known_failures 1 The >=X.Y.Z vs X.Y.Z-rc1 over-acceptance, picked up by just known-failures
pacquet_package_manager::installability::tests 11 Per-install skip-set computation: skip on bad OS, skip on bad node engine, dedup events across peer variants, fast-path triggers, constraint predicate's edge cases (engines.npm only, ["any"] sentinel, empty lists)
pacquet_package_manager::build_sequence::tests 3 (new) Skipped+patched doesn't enter build queue; skipped parent doesn't drag descendants in; descendant with non-skipped parent still builds

All ported tests verified to catch regressions by temporarily
breaking the subject under test, observing the failure, then
reverting. The "test the tests" workflow from CLAUDE.md.

Deferred to follow-up slices

  • .modules.yaml.skipped write/read + headless re-check
    (slice 3).
  • supportedArchitectures config + --cpu / --os / --libc
    CLI flags (slice 2).
  • pnpm:install-check warn channel on the reporter side
    (currently tracing::warn!).
  • Real libc detection — host_libc() returns "unknown" today;
    matches non-Linux host behavior, but on Alpine/musl this
    over-installs glibc-only optional packages. Slice 2.
  • engine_strict config wiring — defaults to false today, so
    the error path is unreachable from production. Wired through
    end-to-end so the slice that flips the config doesn't churn
    the error enum.

Test plan

  • just ready clean (typos, fmt, check, test, lint)
  • taplo format --check clean
  • just dylint clean
  • RUSTDOCFLAGS="-D warnings" cargo doc clean
  • cargo nextest run — 752 tests pass
  • CI Compare Benchmarks gate passes; no cold-install
    regression on the 1352-package fixture
  • All ported tests verified to detect a regression in the
    code under test

Cross-references


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

Copilot AI review requested due to automatic review settings May 13, 2026 09:43
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

New pacquet-package-is-installable crate implements pnpm-compatible engine and platform checks and tri-state installability verdicts; package-manager detects the host once per install, computes a SkippedSnapshots set, and threads it into CreateVirtualStore, SymlinkDirectDependencies, and BuildModules to filter optional+incompatible snapshots.

Changes

Package installability system

Layer / File(s) Summary
Workspace manifest & re-exports
Cargo.toml, crates/package-is-installable/Cargo.toml, crates/graph-hasher/src/lib.rs, crates/graph-hasher/src/engine_name.rs, crates/package-is-installable/src/lib.rs
Adds pacquet-package-is-installable to the workspace, crate manifest and root lib, and makes host-detection helpers (detect_node_version, host_arch, host_libc, host_platform) public and re-exported.
Engine constraint checking
crates/package-is-installable/src/check_engine.rs, crates/package-is-installable/src/tests/check_engine.rs, crates/package-is-installable/tests/known_failures.rs
Engine/pnpm models, diagnostics, check_engine with prerelease-aware semver satisfiability, unit tests, and a known prerelease edge-case marker.
Platform constraint checking
crates/package-is-installable/src/check_platform.rs, crates/package-is-installable/src/tests/check_platform.rs
OS/CPU/libc models, check_platform implementation with negation/any semantics and supported-architectures handling, plus unit tests.
Tri-state package verdict API
crates/package-is-installable/src/package_is_installable.rs, crates/package-is-installable/src/tests/package_is_installable.rs
Defines PackageInstallabilityManifest, InstallabilityOptions, InstallabilityVerdict, InstallabilityError, check_package, and package_is_installable with end-to-end tests.
Installability pass (package-manager)
crates/package-manager/src/installability.rs, crates/package-manager/src/installability/tests.rs, crates/package-manager/src/lib.rs
Adds InstallabilityHost::detect, SkippedSnapshots container, and compute_skipped_snapshots that runs package-level checks, records skipped snapshot keys, deduplicates and emits pnpm:skipped-optional-dependency events, and exposes the skip set.
Install pipeline wiring
crates/package-manager/src/install_frozen_lockfile.rs, crates/package-manager/src/create_virtual_store.rs, crates/package-manager/src/symlink_direct_dependencies.rs, crates/package-manager/src/build_modules.rs, plus many tests
Detects host once per install, computes SkippedSnapshots, threads &skipped into CreateVirtualStore (adjust stats, exclude warm-batch entries), SymlinkDirectDependencies (filter direct deps), and BuildModules (filter build requirements); adds InstallFrozenLockfileError::Installability variant and updates many unit tests to pass skipped.

Sequence Diagram(s)

sequenceDiagram
  participant Installer
  participant DetectHost
  participant ComputeSkipped
  participant CreateVS
  participant SymlinkDeps
  participant BuildModules

  Installer->>DetectHost: InstallabilityHost::detect()
  Installer->>ComputeSkipped: compute_skipped_snapshots(snapshots, packages, host)
  ComputeSkipped-->>CreateVS: skipped set
  ComputeSkipped-->>SymlinkDeps: skipped set
  ComputeSkipped-->>BuildModules: skipped set
  CreateVS->>SymlinkDeps: exclude skipped
  SymlinkDeps->>BuildModules: exclude skipped
  BuildModules-->>Installer: proceed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #266 — Addresses the same optional+platform skipping gap by filtering snapshots based on os/cpu/libc and optional flag.
  • #434 — Umbrella for optionalDependencies support; this PR implements core installability checks and skip propagation.
  • pnpm/pacquet#437 — Overlaps on host detection helpers and libc exposure used by installability.

Possibly related PRs

  • pnpm/pacquet#423 — Related to engine-name/side-effects-cache engine derivation in install flow.
  • pnpm/pacquet#333 — Overlaps on SymlinkDirectDependencies/bin-linking and install wiring.
  • pnpm/pacquet#375 — Related build_modules/install flow changes that intersect with skip propagation.

Suggested reviewers

  • KSXGitHub

"A rabbit hums a tiny tune,
Checks the engine, eyes the moon.
Optional hops that don't belong,
Are gently skipped, the log grows long.
Host detected, skips in store—hop on, install no more."

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main feature: platform and engine installability checks plus skipping of optional incompatible packages, with a reference to issue #434 slice 1.
Linked Issues check ✅ Passed The changes fully implement the primary objectives from #266 and #434 slice 1: platform/engine checks, skip-set threading, reporter events, and tri-state verdicts with comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are in-scope for slice 1: new crate, installability module, skip-set propagation, and test coverage; deferred items (persistence, CLI flags, libc detection) correctly noted as follow-up slices.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed The PR description comprehensively covers the changes, linking to upstream, explaining the design rationale, and including test coverage and deferred work.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-installability-check

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

@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.13187% with 54 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.21%. Comparing base (43e5db9) to head (ef18514).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...tes/package-manager/src/install_frozen_lockfile.rs 52.08% 23 Missing ⚠️
crates/package-manager/src/installability.rs 85.38% 19 Missing ⚠️
crates/graph-hasher/src/engine_name.rs 50.00% 7 Missing ⚠️
...ckage-is-installable/src/package_is_installable.rs 94.64% 3 Missing ⚠️
crates/package-is-installable/src/check_engine.rs 96.96% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main     #439    +/-   ##
========================================
  Coverage   87.20%   87.21%            
========================================
  Files         101      105     +4     
  Lines        8075     8493   +418     
========================================
+ Hits         7042     7407   +365     
- Misses       1033     1086    +53     

☔ 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.01     16.2±0.51ms   268.1 KB/sec    1.00     16.0±0.07ms   270.8 KB/sec

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

This PR introduces a foundational “installability gate” to match pnpm’s optional-dependency behavior on incompatible hosts by evaluating per-snapshot platform (os/cpu/libc) and engine (engines.*) constraints, emitting pnpm:skipped-optional-dependency, and threading a computed skip-set through the install pipeline so skipped snapshots are not materialized, linked, or built.

Changes:

  • Added new pacquet-package-is-installable crate porting pnpm’s checkPlatform, checkEngine, and packageIsInstallable tri-state verdict (with ported upstream unit tests).
  • Added package-manager::installability to compute a per-install SkippedSnapshots set (deduped reporter emits per metadata row) and integrated it into CreateVirtualStore, SymlinkDirectDependencies, and BuildModules.
  • Extended pacquet-graph-hasher to expose host triple helpers and provide detect_node_version to avoid re-spawning node --version.

Reviewed changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
crates/package-manager/src/symlink_direct_dependencies/tests.rs Updates test harness to pass an empty SkippedSnapshots set.
crates/package-manager/src/symlink_direct_dependencies.rs Skips linking direct deps whose resolved snapshot key is in SkippedSnapshots.
crates/package-manager/src/lib.rs Registers and re-exports the new installability module.
crates/package-manager/src/installability.rs New module computing SkippedSnapshots and emitting skipped-optional reporter events.
crates/package-manager/src/installability/tests.rs Unit tests for skip-set computation and reporter dedupe behavior.
crates/package-manager/src/install_frozen_lockfile.rs Detects host once, computes skip-set, threads it into install phases, and derives engine_name from detected node version.
crates/package-manager/src/create_virtual_store.rs Excludes skipped snapshots from virtual-store creation and adjusts “added” stats accordingly.
crates/package-manager/src/build_modules.rs Adds SkippedSnapshots filter to avoid build work for skipped snapshots.
crates/package-manager/src/build_modules/tests.rs Updates build tests to pass an empty SkippedSnapshots set.
crates/package-manager/Cargo.toml Adds dependency on pacquet-package-is-installable.
crates/package-is-installable/src/lib.rs New crate entry point exporting checkers and composer.
crates/package-is-installable/src/check_platform.rs Port of pnpm checkPlatform logic (os/cpu/libc + supportedArchitectures).
crates/package-is-installable/src/check_engine.rs Port of pnpm checkEngine logic with prerelease-handling approximation + tests.
crates/package-is-installable/src/package_is_installable.rs Port of pnpm packageIsInstallable tri-state verdict + error mapping.
crates/package-is-installable/src/tests/*, src/tests.rs Ported upstream unit tests + additional end-to-end verdict tests.
crates/package-is-installable/Cargo.toml New crate manifest.
crates/graph-hasher/src/lib.rs Re-exports new engine/host helpers.
crates/graph-hasher/src/engine_name.rs Adds detect_node_version and exports host_platform/arch/libc.
Cargo.toml Adds new crate to [workspace.dependencies].
Cargo.lock Adds workspace lock entries for the new crate.

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

Comment thread crates/package-manager/src/installability.rs
Comment thread crates/package-manager/src/installability.rs
Comment thread crates/package-manager/src/install_frozen_lockfile.rs Outdated
Comment thread crates/package-is-installable/src/check_engine.rs Outdated
Comment thread crates/package-is-installable/src/package_is_installable.rs

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

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

194-216: ⚡ Quick win

Reuse the existing package-key parser for skipped-event name/version extraction.

Using a second local parser here risks payload drift versus other install paths when key formatting evolves (scopes/peers/edge forms). Prefer the shared parser already used in-package.

Proposed refactor
-fn emit_skipped<R: Reporter>(pkg_id: &str, reason: SkipReason, details: String, prefix: &str) {
-    let (name, version) = split_name_version(pkg_id);
+fn emit_skipped<R: Reporter>(pkg_id: &str, reason: SkipReason, details: String, prefix: &str) {
+    let (name, version) = crate::build_modules::parse_name_version_from_key(pkg_id);
@@
-/// Split a `name@version` (with possible leading `@` for scoped
-/// packages) into `(name, version)`. Mirrors the `lastIndexOf('@')`
-/// rule pacquet's manifest parser already uses.
-fn split_name_version(pkg_id: &str) -> (String, String) {
-    match pkg_id.rfind('@') {
-        Some(idx) if idx > 0 => (pkg_id[..idx].to_string(), pkg_id[idx + 1..].to_string()),
-        _ => (pkg_id.to_string(), String::new()),
-    }
-}

As per coding guidelines: "Before implementing any non-trivial helper function, search the workspace for existing functions or utilities that do the same or similar thing."

🤖 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.rs` around lines 194 - 216, The
split_name_version helper duplicates package-key parsing and risks drift;
replace its usage in emit_skipped by calling the shared package-key parser
already used elsewhere in this crate (e.g. the PackageKey/parse_package_key
utility) to extract name and version, falling back to (pkg_id.to_string(),
String::new()) on parse errors, then remove the local split_name_version
function; update emit_skipped to build SkippedOptionalPackage from the parser's
name and version fields so formatting is consistent with other install paths.
🤖 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/graph-hasher/src/engine_name.rs`:
- Around line 141-143: host_libc currently returns the constant "unknown", which
disables libc filtering; replace it with runtime detection in host_libc() so it
returns a concrete identifier like "musl" or "glibc". Implement detection by
running a system probe (e.g., invoke "ldd --version" or read /lib*/libc*
metadata) and parse output for "musl" to return "musl", otherwise return
"glibc"; ensure failures fall back to "unknown" and propagate no-panics. Update
the host_libc function to perform this probe and return the detected string.

In `@crates/package-is-installable/src/check_engine.rs`:
- Around line 171-179: The current satisfies_with_prerelease fallback in
check_engine.rs incorrectly rejects prerelease versions for strict upper bounds
like `<X.Y.Z`; fix by either (A) adding the nodejs-semver fork to Cargo.toml and
calling its satisfies_with_prerelease(true) from the existing
satisfies_with_prerelease call to match pnpm semantics, or (B) implement a
targeted special-case in the satisfies_with_prerelease function: if the range is
a strict upper-bound of the form `<X.Y.Z` and the candidate version is a
prerelease whose major.minor.patch equals X.Y.Z, return true (treat as
included), otherwise keep the current strip-prerelease behavior—use these
identifiers (satisfies_with_prerelease, check_engine.rs) to locate and update
the logic.

In `@crates/package-is-installable/src/tests/check_engine.rs`:
- Around line 100-102: Replace the #[ignore] approach for the test
pnpm_is_a_prerelease_version_strict_ge_full_version_does_not_satisfy by moving
it into the repo’s known-failures harness: remove the #[ignore] attribute, place
the test under the known_failures module (or annotate it accordingly), and
invoke pacquet_testing_utils::allow_known_failure! for that test so it remains
runnable and flagged as a known failure; ensure the test function name
pnpm_is_a_prerelease_version_strict_ge_full_version_does_not_satisfy is
preserved and that the allow_known_failure! macro wraps or is called at the
start of the test to mark the boundary.

---

Nitpick comments:
In `@crates/package-manager/src/installability.rs`:
- Around line 194-216: The split_name_version helper duplicates package-key
parsing and risks drift; replace its usage in emit_skipped by calling the shared
package-key parser already used elsewhere in this crate (e.g. the
PackageKey/parse_package_key utility) to extract name and version, falling back
to (pkg_id.to_string(), String::new()) on parse errors, then remove the local
split_name_version function; update emit_skipped to build SkippedOptionalPackage
from the parser's name and version fields so formatting is consistent with other
install paths.
🪄 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: 254f001b-8e08-4e26-abdc-06caa8ef81fd

📥 Commits

Reviewing files that changed from the base of the PR and between b514929 and f3d6e05.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (22)
  • Cargo.toml
  • crates/graph-hasher/src/engine_name.rs
  • crates/graph-hasher/src/lib.rs
  • crates/package-is-installable/Cargo.toml
  • crates/package-is-installable/src/check_engine.rs
  • crates/package-is-installable/src/check_platform.rs
  • crates/package-is-installable/src/lib.rs
  • crates/package-is-installable/src/package_is_installable.rs
  • crates/package-is-installable/src/tests.rs
  • crates/package-is-installable/src/tests/check_engine.rs
  • crates/package-is-installable/src/tests/check_platform.rs
  • crates/package-is-installable/src/tests/package_is_installable.rs
  • crates/package-manager/Cargo.toml
  • crates/package-manager/src/build_modules.rs
  • crates/package-manager/src/build_modules/tests.rs
  • crates/package-manager/src/create_virtual_store.rs
  • crates/package-manager/src/install_frozen_lockfile.rs
  • crates/package-manager/src/installability.rs
  • crates/package-manager/src/installability/tests.rs
  • crates/package-manager/src/lib.rs
  • crates/package-manager/src/symlink_direct_dependencies.rs
  • crates/package-manager/src/symlink_direct_dependencies/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (2)
**/*.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-is-installable/src/tests/package_is_installable.rs
  • crates/package-manager/src/lib.rs
  • crates/graph-hasher/src/lib.rs
  • crates/package-manager/src/symlink_direct_dependencies.rs
  • crates/package-is-installable/src/tests.rs
  • crates/package-manager/src/build_modules.rs
  • crates/package-is-installable/src/check_platform.rs
  • crates/package-manager/src/installability.rs
  • crates/package-is-installable/src/lib.rs
  • crates/package-manager/src/installability/tests.rs
  • crates/package-manager/src/symlink_direct_dependencies/tests.rs
  • crates/package-manager/src/create_virtual_store.rs
  • crates/package-is-installable/src/tests/check_engine.rs
  • crates/package-is-installable/src/tests/check_platform.rs
  • crates/graph-hasher/src/engine_name.rs
  • crates/package-manager/src/install_frozen_lockfile.rs
  • crates/package-is-installable/src/check_engine.rs
  • crates/package-is-installable/src/package_is_installable.rs
  • crates/package-manager/src/build_modules/tests.rs
**/tests/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/tests/**/*.rs: When porting behavior from pnpm, port the relevant pnpm tests to Rust tests whenever they translate. Matching test coverage is the easiest way to prove behavioral parity.
Consult the test-porting plan in plans/TEST_PORTING.md before adding ported tests. Follow the conventions expected for ports: use known_failures modules, use pacquet_testing_utils::allow_known_failure! at the not-yet-implemented boundary, and temporarily break the subject under test to verify the ported test actually catches the regression. Update TEST_PORTING.md checkboxes as items land.
Follow the test-logging guidance in CODE_STYLE_GUIDE.md: log before non-assert_eq! assertions, use dbg! for complex structures, skip logging for simple scalar assert_eq! assertions.

Files:

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

Applied to files:

  • crates/package-is-installable/src/tests.rs
  • crates/package-manager/src/installability/tests.rs
  • crates/package-manager/src/symlink_direct_dependencies/tests.rs
  • crates/package-manager/src/build_modules/tests.rs
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In this repo’s Rust tests (run via `cargo nextest`), stdout/stderr from passing tests is captured and is only shown when a test fails. As a result, `eprintln!` (and similar stderr logging) in these tests should generally not be flagged as CI “noise” on the happy path; CI output should only appear for failing tests.

Applied to files:

  • crates/package-is-installable/src/tests.rs
🔇 Additional comments (20)
crates/package-is-installable/Cargo.toml (1)

1-21: LGTM!

Cargo.toml (1)

16-34: LGTM!

crates/package-manager/Cargo.toml (1)

14-28: LGTM!

crates/graph-hasher/src/lib.rs (1)

21-23: LGTM!

crates/package-is-installable/src/check_platform.rs (1)

117-207: LGTM!

crates/package-is-installable/src/tests/check_platform.rs (1)

34-157: LGTM!

crates/package-is-installable/src/package_is_installable.rs (1)

1-203: LGTM!

crates/package-is-installable/src/tests.rs (1)

1-10: LGTM!

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

2-2: LGTM!

Also applies to: 89-89, 203-203, 243-243

crates/package-manager/src/lib.rs (1)

16-16: LGTM!

Also applies to: 40-40

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

2-2: LGTM!

Also applies to: 308-308, 375-375, 430-430, 509-509, 636-636, 698-698, 753-753, 985-985, 1092-1092, 1208-1208, 1434-1434, 1538-1538, 1613-1613

crates/package-manager/src/build_modules.rs (1)

1-1: LGTM!

Also applies to: 251-257, 286-286, 304-309

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

1-249: LGTM!

crates/package-manager/src/symlink_direct_dependencies.rs (1)

1-1: LGTM!

Also applies to: 31-37, 59-60, 110-119

crates/package-manager/src/install_frozen_lockfile.rs (1)

3-5: LGTM!

Also applies to: 80-92, 125-159, 170-170, 176-182, 183-191, 306-306, 345-353

crates/package-is-installable/src/tests/check_engine.rs (1)

16-98: LGTM!

Also applies to: 119-130

crates/package-manager/src/create_virtual_store.rs (1)

2-3: LGTM!

Also applies to: 83-90, 128-128, 160-170, 286-299

crates/package-is-installable/src/tests/package_is_installable.rs (1)

29-119: LGTM!

crates/package-is-installable/src/lib.rs (1)

19-35: LGTM!

crates/package-manager/src/installability.rs (1)

36-180: LGTM!

Comment thread crates/graph-hasher/src/engine_name.rs
Comment thread crates/package-is-installable/src/check_engine.rs Outdated
Comment thread crates/package-is-installable/src/tests/check_engine.rs Outdated
@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.521 ± 0.090 2.388 2.647 1.00
pacquet@main 2.533 ± 0.073 2.442 2.645 1.00 ± 0.05
pnpm 6.022 ± 0.084 5.917 6.198 2.39 ± 0.09
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.5207950711199993,
      "stddev": 0.09047546383440337,
      "median": 2.52023782302,
      "user": 2.4923601599999996,
      "system": 3.53799068,
      "min": 2.38760493202,
      "max": 2.64718381302,
      "times": [
        2.64718381302,
        2.62394144802,
        2.51558165502,
        2.3930701270199997,
        2.52489399102,
        2.56215924302,
        2.48693040202,
        2.60326239002,
        2.46332271002,
        2.38760493202
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.53327262082,
      "stddev": 0.0727320430875739,
      "median": 2.53691443902,
      "user": 2.50874616,
      "system": 3.4906317799999997,
      "min": 2.44190387302,
      "max": 2.64470230402,
      "times": [
        2.44882229202,
        2.60664173802,
        2.47778349102,
        2.57411072702,
        2.54617471002,
        2.44190387302,
        2.5276541680199998,
        2.64470230402,
        2.4639693070199997,
        2.60096359802
      ]
    },
    {
      "command": "pnpm",
      "mean": 6.02236690582,
      "stddev": 0.08440919018326186,
      "median": 6.00859045802,
      "user": 8.81789876,
      "system": 4.392308979999999,
      "min": 5.91730197202,
      "max": 6.19799319202,
      "times": [
        5.98665217402,
        6.02166550402,
        6.0952855670199995,
        6.07589849702,
        5.99551541202,
        5.91730197202,
        5.9455654960199995,
        5.94624330802,
        6.19799319202,
        6.04154793602
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 703.9 ± 71.9 668.4 905.8 1.02 ± 0.12
pacquet@main 688.6 ± 44.2 650.5 760.1 1.00
pnpm 2668.0 ± 96.9 2513.4 2820.8 3.87 ± 0.29
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.7039470408800002,
      "stddev": 0.07187437315238897,
      "median": 0.6781522334800001,
      "user": 0.35408602000000006,
      "system": 1.4752617199999998,
      "min": 0.66838989548,
      "max": 0.9057613094800001,
      "times": [
        0.9057613094800001,
        0.67931045548,
        0.6758797604800001,
        0.67326300348,
        0.67699401148,
        0.6752031524800001,
        0.66838989548,
        0.6884465644800001,
        0.71041395048,
        0.68580830548
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.68861161948,
      "stddev": 0.04423649222548709,
      "median": 0.66889923498,
      "user": 0.35356692,
      "system": 1.47052312,
      "min": 0.6505246544800001,
      "max": 0.7601003914800001,
      "times": [
        0.7601003914800001,
        0.6563395334800001,
        0.66559958748,
        0.7361165014800001,
        0.6606435094800001,
        0.65475103448,
        0.67219888248,
        0.7575841734800001,
        0.67225792648,
        0.6505246544800001
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.66795867258,
      "stddev": 0.09691184761818279,
      "median": 2.66351564248,
      "user": 3.10966582,
      "system": 2.1947448199999995,
      "min": 2.51338479848,
      "max": 2.8208296654800002,
      "times": [
        2.7646161404800003,
        2.51338479848,
        2.65965596248,
        2.8208296654800002,
        2.5420245714800003,
        2.7063053364800003,
        2.64479664048,
        2.61072302448,
        2.7498752634800003,
        2.6673753224800003
      ]
    }
  ]
}

zkochan added a commit that referenced this pull request May 13, 2026
Address Doc + Format CI failures and CodeRabbit review feedback on
PR #439:

- Doc: clear ambiguous fn-vs-module intra-doc links in `lib.rs`,
  replace links to private items (`satisfies_with_prerelease`,
  `dedupe_current`) with plain references, and remove the
  cross-crate link from `graph-hasher` to
  `pacquet_package_is_installable`.
- Format: re-flow `installability.rs` after the previous edit
  broke the `node_version` line shape `cargo fmt` expects.
- `InstallabilityError`: add an `InvalidNodeVersion(InvalidNodeVersionError)`
  variant so callers keep the upstream `ERR_PNPM_INVALID_NODE_VERSION`
  error code instead of seeing it collapsed into a synthetic engine
  mismatch.
- `InstallabilityHost`: add a `node_detected: bool` flag that
  `install_frozen_lockfile` consults when deriving `engine_name`.
  Restores the prior "None when no `node` on PATH" semantics so a
  detection failure can't poison the side-effects cache with rows
  keyed on the `99999.0.0` synthetic fallback.
- `InstallabilityOptions`: switch the string fields to `&'a str` and
  derive `Copy` so `compute_skipped_snapshots` can build the
  host-derived part once and toggle only `optional` per snapshot.
  Removes four `String` allocations per snapshot.
- `satisfies_with_prerelease` doc comment now lists both known
  divergences from upstream's `includePrerelease: true` (the
  `>=X.Y.Z` over-acceptance the ported test pins, plus the
  `<X.Y.Z` under-acceptance), not just one.
- Move the prerelease known-failure case from `#[ignore]` to
  `tests/known_failures.rs` so `just known-failures` picks it up
  alongside the existing lifecycle-script entries.
- Dedupe the doubled "Resolve the host context from the running
  process." doc summary on `InstallabilityHost::detect` after the
  earlier rustfmt run.

@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)
crates/package-is-installable/tests/known_failures.rs (1)

23-35: ⚡ Quick win

Fix bound terminology in known-failure helper/message (>= is lower-bound).

This known-failure case is a strict lower-bound divergence, but helper/text currently call it upper-bound. Renaming avoids sending follow-up work in the wrong direction.

Suggested patch
-    fn semver_strict_upper_bound_prerelease_handled() -> KnownResult<()> {
+    fn semver_strict_lower_bound_prerelease_handled() -> KnownResult<()> {
         Err(KnownFailure::new(
             "pacquet's strip-prerelease fallback approximates npm-semver's \
-             `includePrerelease: true`, but over-accepts on a strict upper-bounded \
+             `includePrerelease: true`, but over-accepts on a strict lower-bounded \
              range. Upstream `>=9.0.0` rejects `9.0.0-alpha.1` (alpha.1 < 9.0.0 in \
              semver order, no implicit `-0` floor when fully specified); pacquet's \
              strip turns the version into `9.0.0` which then satisfies `>=9.0.0`. \
              Fix path: either add the `nodejs-semver` fork (which exposes \
              `satisfies_with_prerelease(include_prerelease: bool)`) or open-code the \
-             strict-upper-bound carve-out. Engine ranges of this shape are vanishingly \
+             strict-lower-bound carve-out. Engine ranges of this shape are vanishingly \
              rare in real package.json files.",
         ))
     }
@@
-        allow_known_failure!(semver_strict_upper_bound_prerelease_handled());
+        allow_known_failure!(semver_strict_lower_bound_prerelease_handled());

Also applies to: 50-50

🤖 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-is-installable/tests/known_failures.rs` around lines 23 - 35,
The helper name and message mislabel a strict lower-bound issue as an
"upper-bound"; rename the function semver_strict_upper_bound_prerelease_handled
to semver_strict_lower_bound_prerelease_handled and update the KnownFailure::new
message text (and any other occurrences) to replace "upper-bound" with
"lower-bound" so the helper and description correctly reflect that the
divergence is for a strict lower-bound range (e.g., change the comment "Boundary
helper for the strict-upper-bound prerelease semantics" and the embedded message
text accordingly).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/package-is-installable/tests/known_failures.rs`:
- Around line 23-35: The helper name and message mislabel a strict lower-bound
issue as an "upper-bound"; rename the function
semver_strict_upper_bound_prerelease_handled to
semver_strict_lower_bound_prerelease_handled and update the KnownFailure::new
message text (and any other occurrences) to replace "upper-bound" with
"lower-bound" so the helper and description correctly reflect that the
divergence is for a strict lower-bound range (e.g., change the comment "Boundary
helper for the strict-upper-bound prerelease semantics" and the embedded message
text accordingly).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 88998c67-22b8-4c7e-8a92-0d9b5af9aff9

📥 Commits

Reviewing files that changed from the base of the PR and between f3d6e05 and 0bb9d03.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • crates/graph-hasher/src/engine_name.rs
  • crates/package-is-installable/Cargo.toml
  • crates/package-is-installable/src/check_engine.rs
  • crates/package-is-installable/src/check_platform.rs
  • crates/package-is-installable/src/lib.rs
  • crates/package-is-installable/src/package_is_installable.rs
  • crates/package-is-installable/src/tests/check_engine.rs
  • crates/package-is-installable/src/tests/package_is_installable.rs
  • crates/package-is-installable/tests/known_failures.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 (9)
  • crates/package-is-installable/src/tests/package_is_installable.rs
  • crates/package-is-installable/Cargo.toml
  • crates/graph-hasher/src/engine_name.rs
  • crates/package-manager/src/install_frozen_lockfile.rs
  • crates/package-is-installable/src/check_platform.rs
  • crates/package-manager/src/installability.rs
  • crates/package-manager/src/installability/tests.rs
  • crates/package-is-installable/src/package_is_installable.rs
  • crates/package-is-installable/src/lib.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (2)
**/*.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-is-installable/tests/known_failures.rs
  • crates/package-is-installable/src/tests/check_engine.rs
  • crates/package-is-installable/src/check_engine.rs
**/tests/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/tests/**/*.rs: When porting behavior from pnpm, port the relevant pnpm tests to Rust tests whenever they translate. Matching test coverage is the easiest way to prove behavioral parity.
Consult the test-porting plan in plans/TEST_PORTING.md before adding ported tests. Follow the conventions expected for ports: use known_failures modules, use pacquet_testing_utils::allow_known_failure! at the not-yet-implemented boundary, and temporarily break the subject under test to verify the ported test actually catches the regression. Update TEST_PORTING.md checkboxes as items land.
Follow the test-logging guidance in CODE_STYLE_GUIDE.md: log before non-assert_eq! assertions, use dbg! for complex structures, skip logging for simple scalar assert_eq! assertions.

Files:

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

Applied to files:

  • crates/package-is-installable/tests/known_failures.rs
  • crates/package-is-installable/src/tests/check_engine.rs
  • crates/package-is-installable/src/check_engine.rs
🔇 Additional comments (2)
crates/package-is-installable/src/check_engine.rs (1)

1-200: LGTM!

crates/package-is-installable/src/tests/check_engine.rs (1)

1-120: LGTM!

zkochan added a commit that referenced this pull request May 13, 2026
…ed (#434 slice 1)

The cold-install CI benchmark on PR #439 showed a ~3% regression
(2.075 ± 0.072 s vs 2.009 ± 0.055 s on a 1352-package fixture).
The dominant per-snapshot cost in the new
`compute_skipped_snapshots` pass is the `snapshot_key.without_peer()`
+ `metadata_key.to_string()` + `package_is_installable` chain.
Skipping that entirely whenever the lockfile contains no metadata
row with an `engines` / `cpu` / `os` / `libc` constraint avoids
the per-snapshot work for the common case (most real lockfiles
have only a handful of platform-tagged native packages, and many
have none).

`any_installability_constraint` is a linear scan over `packages`
that short-circuits on the first declared constraint, so the
fast-path probe is O(1) amortized on lockfiles that do have
constraints (it finds one quickly) and O(N) on those that don't
— but with a much smaller per-row cost than the loop it replaces.

Also drops the redundant `or_else(|| Some(vec!["any".to_string()]))`
allocations in `check_package`. Upstream's `index.ts:82-86` defaults
absent platform axes to `["any"]` before passing them to
`checkPlatform`, but pacquet's `check_platform` already skips an
axis when its wanted field is `None`, so the default was
allocating one Vec + String per axis per snapshot for nothing.

Adds a unit test that exercises the fast path against a synthetic
constraint-free lockfile.
Copilot AI review requested due to automatic review settings May 13, 2026 10:32

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 23 out of 24 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

crates/package-manager/src/build_modules.rs:314

  • BuildModules filters requires_build_map by skipped, but the build graph/chunk computation still traverses the full snapshots map (including skipped roots/edges). That means dependencies that are only reachable through a skipped optional+incompatible snapshot can still end up in the build sequence and have their scripts run, even though the parent snapshot never landed on disk. Consider threading skipped into build_sequence (or pre-filtering the graph/roots/edges it walks) so skipped depPaths are removed from the traversal, matching the post-skip graph pnpm builds.
        let requires_build_map: HashMap<PackageKey, bool> = snapshots
            .keys()
            // Skip snapshots that never landed on disk. `pkg_requires_build`
            // would just return `false` for a missing dir, but the
            // walk would still spend a syscall per skipped key — the
            // filter short-circuits that on installs with large
            // optional fan-out.
            .filter(|key| !skipped.contains(key))
            .map(|key| {
                let pkg_dir = virtual_store_dir_for_key(virtual_store_dir, key);
                (key.clone(), pkg_requires_build(&pkg_dir))
            })
            .collect();

Comment thread crates/package-manager/src/installability.rs Outdated
Comment thread crates/package-is-installable/src/check_engine.rs Outdated
Copilot AI review requested due to automatic review settings May 13, 2026 10:56
zkochan added a commit that referenced this pull request May 13, 2026
Address Doc + Format CI failures and CodeRabbit review feedback on
PR #439:

- Doc: clear ambiguous fn-vs-module intra-doc links in `lib.rs`,
  replace links to private items (`satisfies_with_prerelease`,
  `dedupe_current`) with plain references, and remove the
  cross-crate link from `graph-hasher` to
  `pacquet_package_is_installable`.
- Format: re-flow `installability.rs` after the previous edit
  broke the `node_version` line shape `cargo fmt` expects.
- `InstallabilityError`: add an `InvalidNodeVersion(InvalidNodeVersionError)`
  variant so callers keep the upstream `ERR_PNPM_INVALID_NODE_VERSION`
  error code instead of seeing it collapsed into a synthetic engine
  mismatch.
- `InstallabilityHost`: add a `node_detected: bool` flag that
  `install_frozen_lockfile` consults when deriving `engine_name`.
  Restores the prior "None when no `node` on PATH" semantics so a
  detection failure can't poison the side-effects cache with rows
  keyed on the `99999.0.0` synthetic fallback.
- `InstallabilityOptions`: switch the string fields to `&'a str` and
  derive `Copy` so `compute_skipped_snapshots` can build the
  host-derived part once and toggle only `optional` per snapshot.
  Removes four `String` allocations per snapshot.
- `satisfies_with_prerelease` doc comment now lists both known
  divergences from upstream's `includePrerelease: true` (the
  `>=X.Y.Z` over-acceptance the ported test pins, plus the
  `<X.Y.Z` under-acceptance), not just one.
- Move the prerelease known-failure case from `#[ignore]` to
  `tests/known_failures.rs` so `just known-failures` picks it up
  alongside the existing lifecycle-script entries.
- Dedupe the doubled "Resolve the host context from the running
  process." doc summary on `InstallabilityHost::detect` after the
  earlier rustfmt run.
@zkochan zkochan force-pushed the feat-installability-check branch from 86acbf2 to ae93da0 Compare May 13, 2026 10:56
zkochan added a commit that referenced this pull request May 13, 2026
…ed (#434 slice 1)

The cold-install CI benchmark on PR #439 showed a ~3% regression
(2.075 ± 0.072 s vs 2.009 ± 0.055 s on a 1352-package fixture).
The dominant per-snapshot cost in the new
`compute_skipped_snapshots` pass is the `snapshot_key.without_peer()`
+ `metadata_key.to_string()` + `package_is_installable` chain.
Skipping that entirely whenever the lockfile contains no metadata
row with an `engines` / `cpu` / `os` / `libc` constraint avoids
the per-snapshot work for the common case (most real lockfiles
have only a handful of platform-tagged native packages, and many
have none).

`any_installability_constraint` is a linear scan over `packages`
that short-circuits on the first declared constraint, so the
fast-path probe is O(1) amortized on lockfiles that do have
constraints (it finds one quickly) and O(N) on those that don't
— but with a much smaller per-row cost than the loop it replaces.

Also drops the redundant `or_else(|| Some(vec!["any".to_string()]))`
allocations in `check_package`. Upstream's `index.ts:82-86` defaults
absent platform axes to `["any"]` before passing them to
`checkPlatform`, but pacquet's `check_platform` already skips an
axis when its wanted field is `None`, so the default was
allocating one Vec + String per axis per snapshot for nothing.

Adds a unit test that exercises the fast path against a synthetic
constraint-free lockfile.

@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 (2)
crates/package-manager/src/installability.rs (2)

278-300: ⚡ Quick win

Avoid re-parsing PackageKey through Display.

This path turns a typed key into a string and then hand-parses it back into name and version, so the reporter payload now depends on the current serialization format of PackageKey. Prefer reusing an existing package-id parser/accessor from the lockfile layer, or move this split logic there and call it directly.

As per coding guidelines: "Before implementing any non-trivial helper function, search the workspace for existing functions or utilities that do the same or similar thing." "If logic you need already exists in another crate but is not exported, refactor it into a shared crate or move it to a utility crate rather than copy-pasting."

🤖 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.rs` around lines 278 - 300, The
code is re-parsing a PackageKey by calling Display and hand-splitting in
split_name_version used by emit_skipped; instead, stop relying on string
round-tripping — either use an existing package-id parser/accessor from the
lockfile crate (export it and call it here) or move the split logic into a
shared utility crate and call that from emit_skipped; alternatively change
emit_skipped's signature to accept (name, version) or a typed PackageKey
accessor so you don't call pkg_id.to_string() and rfind('@') inside
split_name_version (update references to split_name_version and emit_skipped
accordingly).

10-14: ⚡ Quick win

Point these upstream breadcrumbs at pnpm/main.

These comments are the maintenance trail for future ports; pinning them to 94240bc046 bakes in stale upstream behavior. Use blob/main/... links instead.

As per coding guidelines: "When porting features, bug fixes, or behavior changes from pnpm, always confirm you are looking at the latest, most up-to-date version of pnpm's main branch before reading. If reading on GitHub, open files from https://github.com/pnpm/pnpm/blob/main/... rather than from a permalinked SHA."

🤖 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.rs` around lines 10 - 14, Update
the upstream breadcrumb links in the module doc comment at the top of
installability.rs to point to pnpm's main branch instead of the pinned commit
SHA; replace the two URLs currently referencing commit 94240bc046 with the
equivalent blob/main paths (e.g., change .../blob/94240bc046/... to
.../blob/main/...) so the comments in this file (the module-level doc comment in
crates/package-manager/src/installability.rs) always reference the latest
pnpm/main sources.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/package-manager/src/installability.rs`:
- Around line 278-300: The code is re-parsing a PackageKey by calling Display
and hand-splitting in split_name_version used by emit_skipped; instead, stop
relying on string round-tripping — either use an existing package-id
parser/accessor from the lockfile crate (export it and call it here) or move the
split logic into a shared utility crate and call that from emit_skipped;
alternatively change emit_skipped's signature to accept (name, version) or a
typed PackageKey accessor so you don't call pkg_id.to_string() and rfind('@')
inside split_name_version (update references to split_name_version and
emit_skipped accordingly).
- Around line 10-14: Update the upstream breadcrumb links in the module doc
comment at the top of installability.rs to point to pnpm's main branch instead
of the pinned commit SHA; replace the two URLs currently referencing commit
94240bc046 with the equivalent blob/main paths (e.g., change
.../blob/94240bc046/... to .../blob/main/...) so the comments in this file (the
module-level doc comment in crates/package-manager/src/installability.rs) always
reference the latest pnpm/main sources.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 029aaf55-ea2e-43cf-b799-882453cef514

📥 Commits

Reviewing files that changed from the base of the PR and between 57aa274 and 86acbf2.

📒 Files selected for processing (2)
  • crates/package-is-installable/src/check_engine.rs
  • crates/package-manager/src/installability.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/package-is-installable/src/check_engine.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). (1)
  • 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/installability.rs
🧠 Learnings (1)
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.

Applied to files:

  • crates/package-manager/src/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

Copilot reviewed 23 out of 24 changed files in this pull request and generated 1 comment.

Comment thread crates/package-manager/src/installability.rs Outdated
Copilot AI review requested due to automatic review settings May 13, 2026 11:31

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 23 out of 24 changed files in this pull request and generated 2 comments.

Comment thread crates/package-manager/src/build_modules.rs
Comment thread crates/package-manager/src/installability.rs Outdated
zkochan added a commit that referenced this pull request May 13, 2026
Address Doc + Format CI failures and CodeRabbit review feedback on
PR #439:

- Doc: clear ambiguous fn-vs-module intra-doc links in `lib.rs`,
  replace links to private items (`satisfies_with_prerelease`,
  `dedupe_current`) with plain references, and remove the
  cross-crate link from `graph-hasher` to
  `pacquet_package_is_installable`.
- Format: re-flow `installability.rs` after the previous edit
  broke the `node_version` line shape `cargo fmt` expects.
- `InstallabilityError`: add an `InvalidNodeVersion(InvalidNodeVersionError)`
  variant so callers keep the upstream `ERR_PNPM_INVALID_NODE_VERSION`
  error code instead of seeing it collapsed into a synthetic engine
  mismatch.
- `InstallabilityHost`: add a `node_detected: bool` flag that
  `install_frozen_lockfile` consults when deriving `engine_name`.
  Restores the prior "None when no `node` on PATH" semantics so a
  detection failure can't poison the side-effects cache with rows
  keyed on the `99999.0.0` synthetic fallback.
- `InstallabilityOptions`: switch the string fields to `&'a str` and
  derive `Copy` so `compute_skipped_snapshots` can build the
  host-derived part once and toggle only `optional` per snapshot.
  Removes four `String` allocations per snapshot.
- `satisfies_with_prerelease` doc comment now lists both known
  divergences from upstream's `includePrerelease: true` (the
  `>=X.Y.Z` over-acceptance the ported test pins, plus the
  `<X.Y.Z` under-acceptance), not just one.
- Move the prerelease known-failure case from `#[ignore]` to
  `tests/known_failures.rs` so `just known-failures` picks it up
  alongside the existing lifecycle-script entries.
- Dedupe the doubled "Resolve the host context from the running
  process." doc summary on `InstallabilityHost::detect` after the
  earlier rustfmt run.
zkochan added a commit that referenced this pull request May 13, 2026
…ed (#434 slice 1)

The cold-install CI benchmark on PR #439 showed a ~3% regression
(2.075 ± 0.072 s vs 2.009 ± 0.055 s on a 1352-package fixture).
The dominant per-snapshot cost in the new
`compute_skipped_snapshots` pass is the `snapshot_key.without_peer()`
+ `metadata_key.to_string()` + `package_is_installable` chain.
Skipping that entirely whenever the lockfile contains no metadata
row with an `engines` / `cpu` / `os` / `libc` constraint avoids
the per-snapshot work for the common case (most real lockfiles
have only a handful of platform-tagged native packages, and many
have none).

`any_installability_constraint` is a linear scan over `packages`
that short-circuits on the first declared constraint, so the
fast-path probe is O(1) amortized on lockfiles that do have
constraints (it finds one quickly) and O(N) on those that don't
— but with a much smaller per-row cost than the loop it replaces.

Also drops the redundant `or_else(|| Some(vec!["any".to_string()]))`
allocations in `check_package`. Upstream's `index.ts:82-86` defaults
absent platform axes to `["any"]` before passing them to
`checkPlatform`, but pacquet's `check_platform` already skips an
axis when its wanted field is `None`, so the default was
allocating one Vec + String per axis per snapshot for nothing.

Adds a unit test that exercises the fast path against a synthetic
constraint-free lockfile.
@zkochan zkochan force-pushed the feat-installability-check branch from a735e36 to d723f22 Compare May 13, 2026 11:42
Copilot AI review requested due to automatic review settings May 13, 2026 11:54
@zkochan zkochan deleted the feat-installability-check branch May 13, 2026 13:13
zkochan added a commit that referenced this pull request May 13, 2026
…l tests

After rebasing on top of #439 (`pacquet-package-is-installable` + the
`SkippedSnapshots` set), the new test sites this PR added —
`per_importer_prefix_in_pnpm_root_events`,
`unsafe_importer_keys_error_before_filesystem_writes`,
`cross_importer_link_dep_symlinks_to_sibling_rootdir`, and
`custom_modules_dir_propagates_to_each_importer` — needed the new
`skipped` field on their `SymlinkDirectDependencies` builders. Pass
`&SkippedSnapshots::default()` everywhere; the per-importer install
behaviour these tests cover is orthogonal to the installability
filter, so an empty skipped set keeps the assertion subject the
same.

---
Written by an agent (Claude Code, claude-opus-4-7).
zkochan added a commit that referenced this pull request May 13, 2026
…l tests

After rebasing on top of #439 (`pacquet-package-is-installable` + the
`SkippedSnapshots` set), the new test sites this PR added —
`per_importer_prefix_in_pnpm_root_events`,
`unsafe_importer_keys_error_before_filesystem_writes`,
`cross_importer_link_dep_symlinks_to_sibling_rootdir`, and
`custom_modules_dir_propagates_to_each_importer` — needed the new
`skipped` field on their `SymlinkDirectDependencies` builders. Pass
`&SkippedSnapshots::default()` everywhere; the per-importer install
behaviour these tests cover is orthogonal to the installability
filter, so an empty skipped set keeps the assertion subject the
same.

---
Written by an agent (Claude Code, claude-opus-4-7).
zkochan added a commit that referenced this pull request May 13, 2026
…l tests

After rebasing on top of #439 (`pacquet-package-is-installable` + the
`SkippedSnapshots` set), the new test sites this PR added —
`per_importer_prefix_in_pnpm_root_events`,
`unsafe_importer_keys_error_before_filesystem_writes`,
`cross_importer_link_dep_symlinks_to_sibling_rootdir`, and
`custom_modules_dir_propagates_to_each_importer` — needed the new
`skipped` field on their `SymlinkDirectDependencies` builders. Pass
`&SkippedSnapshots::default()` everywhere; the per-importer install
behaviour these tests cover is orthogonal to the installability
filter, so an empty skipped set keeps the assertion subject the
same.

---
Written by an agent (Claude Code, claude-opus-4-7).
zkochan added a commit that referenced this pull request May 13, 2026
… fixture

`crates/package-manager/src/installability/tests.rs` initialises a
`TarballResolution` without the `path` field that #451 added (git-
hosted tarball sub-directory selection). The omission landed when
#439 (installability check, #434 slice 1) merged just before #451 —
neither PR touched the other's file, so the bug shipped on
`origin/main`.

CI was failing with:

```
error[E0063]: missing field `path` in initializer of `TarballResolution`
  --> crates/package-manager/src/installability/tests.rs:56:49
```

Fix is one line: `path: None`. The installability check ignores the
resolution shape entirely (see the comment a few lines above the
init site), so `None` is correct.
zkochan added a commit that referenced this pull request May 13, 2026
Origin/main grew a `path: Option<String>` field on
`TarballResolution` (PR #451, `feat(git-fetcher): install
git-hosted tarballs via preparePackage + packlist`). That PR
landed after #439 (`feat(package-is-installable): platform +
engine check`) and missed updating the `TarballResolution`
literal in `crates/package-manager/src/installability/tests.rs`,
so any PR opened against current main inherits a build break in
the `package-is-installable` test build (#445 hit the same
failure). My own `peer_dependency_in_lockfile_surfaces_unsupported`
test in `crates/real-hoist/src/tests.rs` had the same gap.

Add `path: None` to both literals so the test builds compile
against the post-#451 `TarballResolution` shape.

Includes the rebase of feat/438-slice-3b onto current origin/main.
zkochan added a commit that referenced this pull request May 13, 2026
… fixture (#455)

Landrace between #439 (installability tests fixture) and #451 (added
`path: Option<String>` to `TarballResolution`). Both merged green
against their own bases but main's tip won't compile:

    error[E0063]: missing field `path` in initializer of `TarballResolution`
      --> crates/package-manager/src/installability/tests.rs:56:49

Add the missing field with `path: None` so the struct literal matches
the current `TarballResolution` shape.
zkochan added a commit that referenced this pull request May 13, 2026
…bc detection (#434 slice 2)

Slice 1 (#439) wired `pacquet-package-is-installable` into the install
pipeline but the `SupportedArchitectures` input was always `None`, so
every install behaved as if the user had opted into "host triple only".
This slice closes that gap by wiring the three upstream input sources
pinned at pnpm/pnpm@94240bc046:

- `Config.supported_architectures` deserialized from
  `pnpm-workspace.yaml`, mirroring
  `getOptionsFromRootManifest.ts`.
- `--cpu` / `--os` / `--libc` flags on `install` / `add`,
  multi-valued, comma-separated. Per-axis CLI override replaces the yaml
  axis wholesale, matching `overrideSupportedArchitecturesWithCLI.ts`.
- Real Linux libc detection — replaces the slice 1 `"unknown"` stub
  with a `/lib*/ld-musl-*` probe cached via `OnceLock`. Mirrors
  `detect-libc.familySync()` semantics: `"musl"` / `"glibc"` on
  Linux, `"unknown"` everywhere else. No new dep — open-coded
  `read_dir` is cheaper than spawning `ldd --version` and works in
  slim containers without `ldd` on PATH.

Threaded through `Install` / `InstallFrozenLockfile` / `Add` as an
explicit field instead of mutating `State.config`, because
`State.config` is `&'static Config` — the CLI override merge happens
at the CLI layer and the fully-resolved value lands on the install
pipeline.

Tests:

- `workspace_yaml/tests.rs` — yaml round-trip across `os` / `cpu` /
  `libc`, omission, and partial-axis cases.
- `installability/tests.rs` — `supportedArchitectures` widens the
  accept set so a darwin-only package stays on a linux host when the
  user lists `darwin`; conversely, listing `linux` keeps the
  darwin-only package skipped (no implicit host include).

Out of scope (umbrella slice 3+): `.modules.yaml.skipped` persistence,
current-lockfile diffing for `removeOptionalDependenciesThatAreNotUsed`,
and `--no-optional` plumbing.

Closes #453.
zkochan added a commit that referenced this pull request May 13, 2026
Origin/main grew a `path: Option<String>` field on
`TarballResolution` (PR #451, `feat(git-fetcher): install
git-hosted tarballs via preparePackage + packlist`). That PR
landed after #439 (`feat(package-is-installable): platform +
engine check`) and missed updating the `TarballResolution`
literal in `crates/package-manager/src/installability/tests.rs`,
so any PR opened against current main inherits a build break in
the `package-is-installable` test build (#445 hit the same
failure). My own `peer_dependency_in_lockfile_surfaces_unsupported`
test in `crates/real-hoist/src/tests.rs` had the same gap.

Add `path: None` to both literals so the test builds compile
against the post-#451 `TarballResolution` shape.

Includes the rebase of feat/438-slice-3b onto current origin/main.
zkochan added a commit that referenced this pull request May 13, 2026
…bc detection (#434 slice 2)

Slice 1 (#439) wired `pacquet-package-is-installable` into the install
pipeline but the `SupportedArchitectures` input was always `None`, so
every install behaved as if the user had opted into "host triple only".
This slice closes that gap by wiring the three upstream input sources
pinned at pnpm/pnpm@94240bc046:

- `Config.supported_architectures` deserialized from
  `pnpm-workspace.yaml`, mirroring
  `getOptionsFromRootManifest.ts`.
- `--cpu` / `--os` / `--libc` flags on `install` / `add`,
  multi-valued, comma-separated. Per-axis CLI override replaces the yaml
  axis wholesale, matching `overrideSupportedArchitecturesWithCLI.ts`.
- Real Linux libc detection — replaces the slice 1 `"unknown"` stub
  with a `/lib*/ld-musl-*` probe cached via `OnceLock`. Mirrors
  `detect-libc.familySync()` semantics: `"musl"` / `"glibc"` on
  Linux, `"unknown"` everywhere else. No new dep — open-coded
  `read_dir` is cheaper than spawning `ldd --version` and works in
  slim containers without `ldd` on PATH.

Threaded through `Install` / `InstallFrozenLockfile` / `Add` as an
explicit field instead of mutating `State.config`, because
`State.config` is `&'static Config` — the CLI override merge happens
at the CLI layer and the fully-resolved value lands on the install
pipeline.

Tests:

- `workspace_yaml/tests.rs` — yaml round-trip across `os` / `cpu` /
  `libc`, omission, and partial-axis cases.
- `installability/tests.rs` — `supportedArchitectures` widens the
  accept set so a darwin-only package stays on a linux host when the
  user lists `darwin`; conversely, listing `linux` keeps the
  darwin-only package skipped (no implicit host include).

Out of scope (umbrella slice 3+): `.modules.yaml.skipped` persistence,
current-lockfile diffing for `removeOptionalDependenciesThatAreNotUsed`,
and `--no-optional` plumbing.

Closes #453.
zkochan added a commit that referenced this pull request May 13, 2026
…bc detection (#434 slice 2) (#456)

Slice 2 of the [#434 umbrella](#434) (`Proper optionalDependencies support`). Slice 1 (#439) wired `pacquet-package-is-installable` into the install pipeline but the `SupportedArchitectures` input was always `None`, so every install behaved as if the user had opted into \"host triple only\". This PR closes that gap and replaces the slice 1 libc stub with a real Linux probe.

Mirrors the three upstream input sources pinned at [pnpm/pnpm@94240bc046](https://github.com/pnpm/pnpm/tree/94240bc046):

- **`Config.supported_architectures` from `pnpm-workspace.yaml`** — same shape as upstream's `getOptionsFromRootManifest.ts` ([`config/reader/src/getOptionsFromRootManifest.ts#L23`](https://github.com/pnpm/pnpm/blob/94240bc046/config/reader/src/getOptionsFromRootManifest.ts#L23)).
- **`--cpu` / `--os` / `--libc` flags on `install` / `add`** — multi-valued, comma-separated. Per-axis CLI override replaces the yaml axis wholesale, matching upstream's [`overrideSupportedArchitecturesWithCLI.ts`](https://github.com/pnpm/pnpm/blob/94240bc046/config/reader/src/overrideSupportedArchitecturesWithCLI.ts).
- **Real Linux libc detection** — replaces the slice 1 `\"unknown\"` stub with a `/lib*/ld-musl-*` probe cached via `OnceLock`. Mirrors `detect-libc.familySync()` semantics: `\"musl\"` / `\"glibc\"` on Linux, `\"unknown\"` everywhere else. No new dep — open-coded `read_dir` is cheaper than spawning `ldd --version` and works in slim containers without `ldd` on PATH.

Threaded through `Install` / `InstallFrozenLockfile` / `Add` as an explicit field instead of mutating `State.config`, because `State.config` is `&'static Config` — the CLI override merge happens at the CLI layer and the fully-resolved value lands on the install pipeline.

## Test plan

Unit tests added; full suite ran via `just ready` + `cargo doc --no-deps` + `taplo format --check` + `just dylint`.

- [x] `cargo test -p pacquet-config workspace_yaml::tests::*supported_architectures*` — yaml round-trip across `os` / `cpu` / `libc`, omission, partial-axis (3 tests).
- [x] `cargo test -p pacquet-package-manager --lib installability::tests::supported_architectures*` — `supportedArchitectures` widens the accept set so a darwin-only package stays on a linux host when the user lists `darwin`; conversely, listing `linux` keeps the darwin-only package skipped (no implicit host include).
- [x] `just ready` — typos + fmt + check + nextest (792 tests pass) + clippy.
- [x] `taplo format --check`.
- [x] `just dylint` (perfectionist).
- [x] `RUSTDOCFLAGS=\"-D warnings\" cargo doc --no-deps --workspace`.

## Out of scope

- `.modules.yaml.skipped` persistence (umbrella slice 3).
- Current-lockfile diffing for `removeOptionalDependenciesThatAreNotUsed` (umbrella slice 6).
- `--no-optional` plumbing (umbrella slice 5).

Closes #453.
zkochan added a commit that referenced this pull request May 13, 2026
Address PR #445 review feedback after the rebase pulled in #439
(installability check / `SkippedSnapshots`) + #443 (workspace
install).

- **Honor `SkippedSnapshots` in the hoist pass** (Copilot, real
  bug). `install_frozen_lockfile.rs` was passing
  `HashSet::new()` to `HoistInputs.skipped` even though
  `compute_skipped_snapshots` already produces the
  optional+platform-incompatible skip set earlier in the same
  function. Effect of the fix: hoist no longer creates symlinks
  to slots that were never extracted, and an alias that would
  have been claimed by a skipped snapshot can now be claimed by
  a non-skipped sibling at a deeper level. The `HoistInputs`
  shape stays `&HashSet<PackageKey>` so `hoist.rs` doesn't
  depend on `installability`; the cheap conversion happens at
  the call site.

- **Refresh `build_direct_deps_by_importer` doc comment**
  (Copilot). Doc said "currently only installs the root importer"
  — wrong since #443 (workspace install) and the follow-up that
  switched the call site to pass the full `importers` map.
  Updated to describe the current per-importer walk and the
  `link:` filter inside the loop.

- **Refresh `skipped_optional_deps` known-failure reason**
  (Copilot). The reason claimed pacquet doesn't compute skip
  sets — wrong since #439. The actual gap is the test fixture:
  upstream's `@pnpm.e2e/not-compatible-with-any-os` isn't in
  pacquet's mocked registry, so the end-to-end skip-then-hoist
  path can't be exercised yet. Reason updated to point at the
  fixture gap, not the (already-implemented) skip computation.

Tests: `just ready` 902/902 pass; `cargo doc --workspace --no-deps`
with `RUSTDOCFLAGS="-D warnings"` clean; `just dylint` clean.
The 36 CLI hoist tests + 12 unit tests still pass with the real
skip set threaded through.

Out of scope: Copilot's comment about `satisfies_package_manifest`
only validating the root importer is about main's #447 freshness
check (not this PR's hoist work). Worth a follow-up issue but
not addressed here.
KSXGitHub pushed a commit that referenced this pull request May 13, 2026
Pull in 28 commits from upstream main, including the
`pacquet-npmrc` → `pacquet-config` rename (#420) plus features:
- supportedArchitectures + --cpu/--os/--libc (#456)
- frozen-lockfile (#442, #443, #447, #450)
- git-fetcher (#436 / #446, #451, #454)
- side-effects cache (#421 / #422, #423, #424)
- real-hoist + global-virtual-store (#432 / #438 / #444, #449, #452)
- patchedDependencies + allow-builds (#425, #427, #428)
- engine/platform installability (#434 / #439)

Conflicts resolved:
- `crates/npmrc/` files migrated under the renamed
  `crates/config/` directory; `Npmrc` → `Config` everywhere
  except `NpmrcAuth` (which keeps the `.npmrc`-domain name).
- `Config::current` reads the env-var DI generic `Api: EnvVar`
  for ${VAR}-substitution in `.npmrc`. Production turbofish in
  `cli_args.rs` is `Config::current::<RealApi, _, _, _, _>(...)`.
- Two-phase `NpmrcAuth::apply_*` retained so default-registry
  creds key at the yaml-resolved registry URL.
- New `Config::auth_headers` field plumbed through
  `install_package_by_snapshot`'s `DownloadTarballToStore`.
- Tests under `crates/config/src/workspace_yaml/tests.rs`
  pick up the new ParseYaml unit test added on this branch.
zkochan added a commit that referenced this pull request May 13, 2026
… slice 3) (#467)

## Summary

Slice 3 of the [#434 umbrella](#434) (`Proper optionalDependencies support`). Slice 1 (#439) computed a `SkippedSnapshots` set on every frozen-lockfile install but never serialized it; slice 2 (#456) closed the input side. This PR closes the loop by mirroring three upstream sites pinned at [pnpm/pnpm@94240bc046](https://github.com/pnpm/pnpm/tree/94240bc046):

- **Write** — `Modules.skipped` was declared at [`crates/modules-yaml/src/lib.rs:197`](https://github.com/pnpm/pacquet/blob/b13b8180/crates/modules-yaml/src/lib.rs#L197) with sort-on-write but always serialized empty. `build_modules_manifest` now takes `&SkippedSnapshots` and produces the depPath string list pnpm writes at [`installing/deps-installer/src/install/index.ts:1625`](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-installer/src/install/index.ts#L1625).

- **Seed** — `install_frozen_lockfile::run` reads `.modules.yaml.skipped` before computing the in-memory set and passes the parsed `PackageKey`s as the seed to `compute_skipped_snapshots`. Mirrors upstream's load at [`installing/read-projects-context/src/index.ts:79`](https://github.com/pnpm/pnpm/blob/94240bc046/installing/read-projects-context/src/index.ts#L79) plus the early return at [`deps/graph-builder/src/lockfileToDepGraph.ts:194`](https://github.com/pnpm/pnpm/blob/94240bc046/deps/graph-builder/src/lockfileToDepGraph.ts#L194).

- **Short-circuit** — `compute_skipped_snapshots` returns the seed verbatim on the fast path (no constraints in the lockfile) and, on the slow path, skips the per-snapshot re-check for keys already in the seed without emitting a duplicate `pnpm:skipped-optional-dependency` event. Non-seeded snapshots still re-run `package_is_installable`, covering the \"host arch changed since last install\" case upstream guards at [`:206-215`](https://github.com/pnpm/pnpm/blob/94240bc046/deps/graph-builder/src/lockfileToDepGraph.ts#L206-L215).

`InstallFrozenLockfile::run` now returns a small `InstallFrozenLockfileOutput { hoisted_dependencies, skipped }` struct so `Install::run` can thread both into `.modules.yaml`. Read errors on `.modules.yaml` degrade to an empty seed — the file is a cache artifact, not authoritative.
zkochan added a commit that referenced this pull request May 13, 2026
…e payload (#434 slice 4) (#474)

Slice 4 of the [#434 umbrella](#434) (`Proper optionalDependencies support`). Slices 1–3 (#439, #456, #467) close the installability-driven skip path; this slice adds the second skip pathway upstream handles in the frozen install: an `optional: true` snapshot whose **fetch / extract** step fails at install time must not abort the install. Local-materialization errors (`CreateVirtualDir`) and config-shape errors still abort even for optional snapshots — matching upstream's `linkPkg` path which sits outside the catch.

Mirrors the silent catch sites at [`deps/graph-builder/src/lockfileToDepGraph.ts:294-298`](https://github.com/pnpm/pnpm/blob/94240bc046/deps/graph-builder/src/lockfileToDepGraph.ts#L294-L298) and [`installing/deps-restorer/src/index.ts:912-921`](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-restorer/src/index.ts#L912-L921):

```ts
try { ... fetch ... }
catch (err) { if (pkgSnapshot.optional) return; throw err }
```

## Changes

- **Reporter**: `SkippedOptionalPackage` becomes a `#[serde(untagged)]` enum with `Installed { id, name, version }` and `ResolutionFailure { name?, version?, bareSpecifier }` variants. Models upstream's discriminated union at [`core-loggers/src/skippedOptionalDependencyLogger.ts:10-31`](https://github.com/pnpm/pnpm/blob/94240bc046/core/core-loggers/src/skippedOptionalDependencyLogger.ts#L10-L31). The new variant is wire-shape-only in slice 4 — pacquet has no resolver yet, but a future resolver port at the upstream emit site ([`resolveDependencies.ts:1376-1383`](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-resolver/src/resolveDependencies.ts#L1376-L1383)) lands without re-touching this type.
- **`SkippedSnapshots`**: gains a `fetch_failed` subset disjoint from the existing `installability` subset. `contains` / `iter` return the union (downstream consumers treat both as absent); `iter_installability` returns only the persistent subset that `.modules.yaml.skipped` records, matching upstream's behavior of never updating `opts.skipped` at the fetch-failure catch sites.
- **`CreateVirtualStore`**: cold-batch dispatch swallows per-snapshot errors when `snapshot.optional` is true **and** the error is fetch-side. A new `is_fetch_side_failure` helper restricts the swallow to `InstallPackageBySnapshotError::DownloadTarball` and `::GitFetch` — the same surface upstream wraps in `storeController.fetchPackage`. Local-materialization (`CreateVirtualDir`) and config-shape errors (`MissingTarballIntegrity` / `UnsupportedResolution`) propagate even for optional snapshots, matching upstream's `linkPkg` path which sits outside the catch. Side benefit: the warm-batch path (which only surfaces `CreateVirtualDir` errors) stays consistently abort-on-error without a parallel swallow site. The per-install `fetch_failed: HashSet<PackageKey>` rides out on `CreateVirtualStoreOutput`.
- **`InstallFrozenLockfile::run`**: folds the returned `fetch_failed` into its mutable `SkippedSnapshots` so downstream phases (`SymlinkDirectDependencies`, `LinkVirtualStoreBins`, `BuildModules`, hoist) treat the dropped snapshots as absent through the same gate they already use for installability skips.

## Test plan

- [x] `reporter::tests::skipped_optional_resolution_failure_event_matches_pnpm_wire_shape` — full payload (`bareSpecifier` camelCase, no `id`).
- [x] `reporter::tests::skipped_optional_resolution_failure_omits_absent_name_and_version` — optional-field shape.
- [x] `install::tests::frozen_install_silently_swallows_unreachable_optional_tarball` — ports [`installing/deps-restorer/test/index.ts:340-360`](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-restorer/test/index.ts#L340-L360): unreachable optional tarball (`http://127.0.0.1:1/...`), install succeeds, slot not created, `.modules.yaml.skipped` left empty. **Test-the-test verified** by inverting the `optional` guard in the cold-batch match arm. Runs with `enable_global_virtual_store = false` so the slot-path assertion targets the legacy layout.
- [x] `install::tests::frozen_install_propagates_non_optional_fetch_failure` — pins the polarity: same fixture, non-optional snapshot, install must error.
- [x] `just ready` (typos + fmt + check + nextest + clippy).
- [x] `taplo format --check`.
- [x] `just dylint` (perfectionist).
- [x] `RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace`.

## Out of scope

- Resolver-side `resolution_failure` emit site — needs a resolver, tracked separately.
- `--no-optional` / `--omit=optional` plumbing — umbrella slice 5.
- Surfacing fetch failures to the reporter on the frozen path — upstream itself is silent here; only the resolver-side emit fires `pnpm:skipped-optional-dependency`.

Closes #471.
zkochan added a commit that referenced this pull request May 13, 2026
…#485)

Slice 5 of the [#434 umbrella](#434) (`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`](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-installer/src/install/link.ts#L109-L111):

```ts
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).
- **`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`](https://github.com/pnpm/pnpm/blob/94240bc046/installing/deps-installer/test/install/optionalDependencies.ts#L712) (`dependency that is both optional and non-optional is installed`).
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.

Proper optionalDependencies support — umbrella Install respects every snapshot — no os/cpu/libc filter for optional deps

2 participants