feat: build concurrency, unsafe-perm, scriptsPrependNodePath (#397 items 12, 14, 15)#429
Conversation
…ems 12, 14, 15) Bundle the remaining moderate/minor parity items from #397 into one PR. Each adds a `pnpm-workspace.yaml` setting, threads it through `Config`, and applies it to the build-modules path so a pnpm yaml that exercises these knobs produces the same install behavior in pacquet. Item 12 — `childConcurrency`. Adds `Config.child_concurrency: u32` with the upstream-matching default `min(4, availableParallelism())` and the negative-offset semantics from [`getWorkspaceConcurrency`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/config/reader/src/concurrency.ts#L25-L34) (`n < 0` → `max(1, parallelism - |n|)`). `BuildModules::run` now dispatches each chunk's members across a bounded rayon thread pool (`par_iter().try_for_each`) — chunks remain sequential to preserve topological order. `ignored_builds` and `deps_state_cache` are wrapped in `Mutex` so the recursive memo and the dedup set survive concurrent chunk-member access. Mirrors upstream's [`runGroups(getWorkspaceConcurrency(opts.childConcurrency), groups)`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/src/index.ts#L124). Item 14 — `unsafePerm`. Adds `Config.unsafe_perm: bool` (default `true`) and threads it through `BuildModules` → `RunPostinstallHooks`. When `false`, the executor sets `TMPDIR=node_modules/.tmp` for the spawn (matching upstream's `safe-buffer` path); the uid/gid drop side is a no-op upstream too because pnpm's `@pnpm/npm-lifecycle` never populates `opts.user`/`opts.group`. The upstream POSIX auto-detect (`getuid() === 0 && setgid → unsafePerm = false`) needs a `libc` dependency, which isn't in `[workspace.dependencies]` — for now, root-run CI must set `unsafePerm: false` in yaml explicitly. Windows is force-overridden to `true` in `WorkspaceSettings::apply_to`, matching upstream's [`process.platform === 'win32'` gate](https://github.com/pnpm/npm-lifecycle/blob/d2d8e790/index.js#L204-L220). Item 15 — `scriptsPrependNodePath`. Adds a tri-state `ScriptsPrependNodePath` enum in `pacquet-config` (custom serde `Deserialize` for the `boolean | "warn-only"` yaml shape from [`Config.scriptsPrependNodePath`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/config/reader/src/Config.ts#L108)), converted to `pacquet_executor::ScriptsPrependNodePath` at the `BuildModules` call site so the executor crate stays free of serde wiring. Default is `Never` to match upstream's [`StrictBuildOptions.scriptsPrependNodePath: false`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/after-install/src/extendBuildOptions.ts#L78). Item 16 — `getSubgraphToBuild` filter trimming — was already implemented in pacquet's `get_subgraph_to_build` via the `child_should_be_built || needs_build || has_patch` gate. Confirmed during this slice; no code change needed for that item. Tests: - `parses_scripts_prepend_node_path_{true,false,warn_only}_from_yaml`, `rejects_invalid_scripts_prepend_node_path`, - `parses_unsafe_perm_from_yaml_and_applies`, `unsafe_perm_force_true_on_windows` (Windows-only), - `parses_{positive,negative}_child_concurrency_from_yaml_and_*`. All 605 workspace tests pass.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds child-concurrency sizing helpers and a tri-state scripts enum to config; parses workspace YAML keys (scriptsPrependNodePath, unsafePerm, childConcurrency) into Config with a Windows override; refactors BuildModules to run chunk members in parallel using a per-install Rayon pool sized by child_concurrency; updates wiring and tests. ChangesParallel Build Execution with Configurable Lifecycle Options
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #429 +/- ##
==========================================
+ Coverage 86.82% 86.94% +0.12%
==========================================
Files 93 93
Lines 6535 6635 +100
==========================================
+ Hits 5674 5769 +95
- Misses 861 866 +5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Micro-Benchmark ResultsLinux |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
crates/package-manager/src/build_modules.rs (2)
405-429: 💤 Low valueThe
#[allow(clippy::too_many_arguments)]is justified but could be documented.The 19 parameters are the natural result of extracting the loop body for parallel dispatch. Refactoring into a parameter struct would add complexity for this internal helper without meaningful benefit. Consider adding a brief inline comment explaining the rationale:
+// Extracted loop body for parallel dispatch; parameters mirror the outer scope bindings. #[allow(clippy::too_many_arguments)] fn build_one_snapshot<R: Reporter>(🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/package-manager/src/build_modules.rs` around lines 405 - 429, The #[allow(clippy::too_many_arguments)] on build_one_snapshot is justified; add a concise inline comment above the attribute or function explaining that the 19 parameters are intentional because this helper was extracted from a loop for parallel dispatch and that wrapping them into a struct would add indirection and complexity for an internal helper (mentioning build_one_snapshot, the parallel dispatch context, and why refactoring is undesirable) so future maintainers understand the rationale.
355-358: 💤 Low valueThread pool creation panic is acceptable but could be more resilient.
The
.expect()onThreadPoolBuilder::build()will panic if the OS refuses to spawn threads (resource exhaustion, ulimit, etc.). While rare, this is a runtime failure outside of developer control.Consider propagating the error or at least logging before panicking. However, given that upstream pnpm's
runGroupsalso doesn't gracefully handle worker pool failures, this matches upstream behavior and the panic message is descriptive enough.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/package-manager/src/build_modules.rs` around lines 355 - 358, The current code calls ThreadPoolBuilder::new().num_threads(child_concurrency.max(1) as usize).build().expect(...), which will panic if the OS refuses to create threads; update this to handle the Result from ThreadPoolBuilder::build() instead of unwrapping: either propagate the error (return a Result from the surrounding function) or call build().map_err(|e| { process_logger.error(format!("failed to build rayon thread pool: {}", e)); e })? and then propagate or abort; at minimum replace expect(...) with proper error handling around build() referencing ThreadPoolBuilder::build, the pool variable, and child_concurrency so the failure can be logged or returned upstream rather than unconditionally panicking.
🤖 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/config/src/defaults.rs`:
- Around line 139-140: The current childConcurrency calculation uses let offset
= (-n) as u32 which overflows for n == i32::MIN; replace that conversion with
n.unsigned_abs() to safely get a u32 absolute value without panic (i.e., use let
offset = n.unsigned_abs()), keeping the subsequent
available_parallelism().saturating_sub(offset).max(1) logic intact; refer to the
variables/functions n, offset, and available_parallelism() to locate the change.
---
Nitpick comments:
In `@crates/package-manager/src/build_modules.rs`:
- Around line 405-429: The #[allow(clippy::too_many_arguments)] on
build_one_snapshot is justified; add a concise inline comment above the
attribute or function explaining that the 19 parameters are intentional because
this helper was extracted from a loop for parallel dispatch and that wrapping
them into a struct would add indirection and complexity for an internal helper
(mentioning build_one_snapshot, the parallel dispatch context, and why
refactoring is undesirable) so future maintainers understand the rationale.
- Around line 355-358: The current code calls
ThreadPoolBuilder::new().num_threads(child_concurrency.max(1) as
usize).build().expect(...), which will panic if the OS refuses to create
threads; update this to handle the Result from ThreadPoolBuilder::build()
instead of unwrapping: either propagate the error (return a Result from the
surrounding function) or call build().map_err(|e| {
process_logger.error(format!("failed to build rayon thread pool: {}", e)); e })?
and then propagate or abort; at minimum replace expect(...) with proper error
handling around build() referencing ThreadPoolBuilder::build, the pool variable,
and child_concurrency so the failure can be logged or returned upstream rather
than unconditionally panicking.
🪄 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: fbf122ab-a07a-4780-b5f2-394011446eca
📒 Files selected for processing (9)
crates/config/src/defaults.rscrates/config/src/lib.rscrates/config/src/workspace_yaml.rscrates/config/src/workspace_yaml/tests.rscrates/executor/src/extend_path.rscrates/package-manager/src/build_modules.rscrates/package-manager/src/build_modules/tests.rscrates/package-manager/src/install_frozen_lockfile.rscrates/package-manager/src/install_package_from_registry/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Agent
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs
📄 CodeRabbit inference engine (AGENTS.md)
**/*.rs: Preserve existing method chains andpipe-traitchains; do not break them into intermediateletbindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Pathover&PathBuf,&strover&String) when it does not force extra copies.
PreferArc::clone(&x)andRc::clone(&x)overx.clone()for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;for any glob whose target is a module you control. External-crate preludes (e.g.,use rayon::prelude::*;) and root-of-module re-exports (e.g.,pub use submodule::*;inlib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plainStringor&str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only viaTryFrom<String>and/orFromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallibleFrom<String>andFrom<&str>constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bareasassertion), add afrom_str_unchecked(or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...
Files:
crates/executor/src/extend_path.rscrates/package-manager/src/install_frozen_lockfile.rscrates/package-manager/src/install_package_from_registry/tests.rscrates/config/src/workspace_yaml/tests.rscrates/config/src/defaults.rscrates/config/src/workspace_yaml.rscrates/config/src/lib.rscrates/package-manager/src/build_modules.rscrates/package-manager/src/build_modules/tests.rs
🧠 Learnings (2)
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.
Applied to files:
crates/executor/src/extend_path.rscrates/package-manager/src/install_frozen_lockfile.rscrates/package-manager/src/install_package_from_registry/tests.rscrates/config/src/workspace_yaml/tests.rscrates/config/src/defaults.rscrates/config/src/workspace_yaml.rscrates/config/src/lib.rscrates/package-manager/src/build_modules.rscrates/package-manager/src/build_modules/tests.rs
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.
Applied to files:
crates/package-manager/src/install_package_from_registry/tests.rscrates/config/src/workspace_yaml/tests.rscrates/package-manager/src/build_modules/tests.rs
🔇 Additional comments (14)
crates/config/src/lib.rs (1)
43-92: Tri-state config wiring looks solid.The
ScriptsPrependNodePathdeserialization and the newConfigfields/defaults are coherent and align cleanly with downstream conversion points.Also applies to: 338-380
crates/config/src/workspace_yaml.rs (1)
110-124: Config application path is correctly wired.The new workspace YAML fields are applied predictably, including the Windows
unsafe_permforce-override andchild_concurrencyresolution flow.Also applies to: 244-263
crates/package-manager/src/install_package_from_registry/tests.rs (1)
49-51: Good test-fixture update for new config surface.Adding explicit defaults for the new fields keeps test behavior deterministic.
crates/config/src/workspace_yaml/tests.rs (1)
279-380: Nice coverage expansion for the new YAML settings.These tests meaningfully pin parsing and
apply_tobehavior, including platform-specific branches.crates/executor/src/extend_path.rs (1)
10-14: Doc update is clear and useful.The added note explains config/executor boundary responsibilities well without coupling serde into executor.
crates/package-manager/src/install_frozen_lockfile.rs (1)
229-260: Field plumbing intoBuildModuleslooks correct.The explicit enum conversion plus the new runtime fields are wired through cleanly.
crates/package-manager/src/build_modules/tests.rs (1)
304-307: Test fixture updates are consistent across scenarios.Good job wiring the new
BuildModulesfields uniformly so existing behavior-focused tests remain valid.Also applies to: 355-358, 433-436, 559-562, 620-623, 674-677, 905-908, 1011-1014, 1126-1129, 1351-1354, 1454-1457, 1528-1531
crates/package-manager/src/build_modules.rs (7)
16-16: LGTM!The
rayon::prelude::*import is appropriate here as external-crate preludes are an allowed exception to the no-star-imports rule. As per coding guidelines: "External-crate preludes (e.g.,use rayon::prelude::*;) ... are exceptions."
215-233: LGTM!The new fields are well-documented with upstream references. Using
u32forchild_concurrencyis appropriate given the.max(1)floor applied at thread pool creation, ensuring forward progress on resource-constrained hosts.
360-392: LGTM!The parallel dispatch pattern is correct:
- Chunks iterate sequentially preserving topological order
pool.install()runs the closure in the bounded thread poolpar_iter().try_for_each()parallelizes chunk members with proper short-circuit on error- Error propagation via
?correctly surfaces failures to the caller
395-396: LGTM!The mutex unwrap pattern with descriptive expect messages is appropriate. Mutex poisoning would only occur if a thread panics while holding the lock, which is a fatal condition that should propagate. The
try_for_eachpattern properly handles errors without panics in the normal path.
494-517: LGTM!The mutex lock scope for
deps_state_cacheis appropriately narrow—held only duringcalc_dep_state. The recursive memoization pattern correctly shares computed hashes across concurrent chunk members via theMutex-protected cache.
527-540: LGTM!The side-effects cache hit check using let-chains is clean and correct. Early return on cache hit properly skips the build, matching upstream's
!node.isBuiltfilter behavior.
590-616: LGTM!The optional dependency error handling correctly mirrors upstream's pattern at
building/during-install/src/index.ts:226-238: failures are logged viapnpm:skipped-optional-dependencychannel and swallowed so the install continues. TheSkippedOptionalPackagefields match upstream's payload structure.
There was a problem hiding this comment.
Pull request overview
Ports three remaining pnpm v11 parity knobs into pacquet’s workspace config and lifecycle/build pipeline: childConcurrency (bounded build parallelism), unsafePerm (TMPDIR behavior + Windows override), and scriptsPrependNodePath (tri-state PATH behavior).
Changes:
- Add workspace-yaml-backed config fields for
scriptsPrependNodePath,unsafePerm, andchildConcurrency(including upstream-like defaults / negative-offset semantics). - Thread these settings through
install_frozen_lockfileintoBuildModules, and use a bounded rayon pool to parallelize chunk members. - Add/extend tests for YAML parsing and update existing build/install tests to include the new
BuildModulesfields.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/package-manager/src/install_package_from_registry/tests.rs | Updates test config construction to include the new Config fields. |
| crates/package-manager/src/install_frozen_lockfile.rs | Converts config enum to executor enum and passes new settings into BuildModules. |
| crates/package-manager/src/build_modules/tests.rs | Updates BuildModules test instantiations with new fields. |
| crates/package-manager/src/build_modules.rs | Implements per-chunk parallel build execution and threads unsafe_perm/scripts_prepend_node_path into lifecycle hook execution. |
| crates/executor/src/extend_path.rs | Clarifies the separation between config (serde) and executor (canonical enum) for ScriptsPrependNodePath. |
| crates/config/src/workspace_yaml/tests.rs | Adds tests for parsing/applying the three new workspace settings. |
| crates/config/src/workspace_yaml.rs | Adds the three settings to WorkspaceSettings and applies them into Config (with Windows override for unsafe_perm). |
| crates/config/src/lib.rs | Introduces serde-enabled ScriptsPrependNodePath mirror enum and adds new fields to Config. |
| crates/config/src/defaults.rs | Adds default/resolve logic for childConcurrency based on available parallelism. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Four review-flagged hardenings from PR #429: 1. `resolve_child_concurrency(Some(i32::MIN))` no longer panics. The negative-offset path now uses `n.unsigned_abs()` instead of `(-n) as u32` — the latter overflows in debug builds because the inner negation itself overflows on `i32::MIN`. Added `resolve_child_concurrency_handles_i32_min` to lock in the non-panicking floor. 2. `pacquet_config::ScriptsPrependNodePath` doc said "Converted at the BuildModules call site via `From`" but no `From` impl exists (the conversion is a manual `match` in `install_frozen_lockfile.rs`). Adding a `From` impl would require inverting the config↔executor crate boundary (neither depends on the other today); corrected the doc to reflect what the code actually does. 3. `rayon::ThreadPoolBuilder::build()` is fallible (the OS may refuse to spawn the worker threads under `RLIMIT_NPROC`). The former `.expect("…")` would panic inside the binary on a resource-constrained host; replaced with a new `BuildModulesError::ThreadPoolBuild` variant (`ERR_PACQUET_BUILD_THREAD_POOL`) carrying the underlying `rayon::ThreadPoolBuildError` and a remediation hint ("lower childConcurrency, or raise RLIMIT_NPROC"). 4. `Mutex::lock().expect(...)` and `into_inner().expect(...)` calls around `ignored_builds` and `deps_state_cache` now use the canonical `unwrap_or_else(|e| e.into_inner())` poison-recovery pattern. Recovery is safe here because the protected types are `BTreeSet<String>` and `DepsStateCache` (a `HashMap`-backed memo), both of which only see one atomic insertion per worker task — a panic mid-call cannot leave the structure in a torn state. Comment at each call site links the safety argument back to the central one in `BuildModules::run`.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/config/src/defaults/tests.rs`:
- Around line 57-59: The test currently only asserts result >= 1 for
resolve_child_concurrency(Some(i32::MIN)) which allows incorrect values >1 to
slip through; change the assertion to require the exact saturated value by
asserting result == 1 (use assert_eq!(result, 1) so test output shows both
sides), i.e. update the assertion in the test that calls
resolve_child_concurrency(Some(i32::MIN)) to assert_eq!(result, 1) to make the
regression check precise.
🪄 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: be8a8111-c68e-4ca7-bf5d-09928aa3163d
📒 Files selected for processing (4)
crates/config/src/defaults.rscrates/config/src/defaults/tests.rscrates/config/src/lib.rscrates/package-manager/src/build_modules.rs
🚧 Files skipped from review as they are similar to previous changes (3)
- crates/config/src/defaults.rs
- crates/config/src/lib.rs
- crates/package-manager/src/build_modules.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Code Coverage
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs
📄 CodeRabbit inference engine (AGENTS.md)
**/*.rs: Preserve existing method chains andpipe-traitchains; do not break them into intermediateletbindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Pathover&PathBuf,&strover&String) when it does not force extra copies.
PreferArc::clone(&x)andRc::clone(&x)overx.clone()for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;for any glob whose target is a module you control. External-crate preludes (e.g.,use rayon::prelude::*;) and root-of-module re-exports (e.g.,pub use submodule::*;inlib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plainStringor&str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only viaTryFrom<String>and/orFromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallibleFrom<String>andFrom<&str>constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bareasassertion), add afrom_str_unchecked(or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...
Files:
crates/config/src/defaults/tests.rs
🧠 Learnings (2)
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.
Applied to files:
crates/config/src/defaults/tests.rs
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.
Applied to files:
crates/config/src/defaults/tests.rs
🔇 Additional comments (1)
crates/config/src/defaults/tests.rs (1)
1-1: Import update is clean and correctly scoped.
resolve_child_concurrencyis used by the new regression test and the explicitsuper::{...}import matches repo style.
Switch from `assert!(result >= 1, ...)` to `assert_eq!(result, 1)` so the test rejects an incorrect value like `2`. The exact result is provably `1`: `i32::MIN.unsigned_abs()` is `2_147_483_648`, which exceeds any plausible host parallelism, so `saturating_sub` produces `0` and `.max(1)` lifts to `1`. Review-suggested in PR #429.
The new struct variant from the previous commit declared a `source` field carrying `rayon::ThreadPoolBuildError` but didn't tag it with `#[error(source)]`, so `Error::source()` returned `None` and the underlying rayon error was invisible to miette's diagnostic chain. Matches the convention used across the workspace (e.g. `StoreIndexError::*`). Review-suggested in PR #429.
Integrated-Benchmark Report (Linux)Scenario: Frozen Lockfile
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.5413818746400003,
"stddev": 0.06720416742527227,
"median": 2.5307324769400004,
"user": 2.54471596,
"system": 3.5248927799999996,
"min": 2.4591740749400004,
"max": 2.65689023994,
"times": [
2.65689023994,
2.5042501779400004,
2.5330955149400003,
2.57808248294,
2.5815059179400004,
2.62432674294,
2.47792933894,
2.4591740749400004,
2.4701948169400003,
2.5283694389400004
]
},
{
"command": "pacquet@main",
"mean": 2.5479696613400002,
"stddev": 0.09498389378171664,
"median": 2.51437107444,
"user": 2.50356836,
"system": 3.52445928,
"min": 2.40524425194,
"max": 2.68538857994,
"times": [
2.4939638649400004,
2.68538857994,
2.51829792894,
2.62123507694,
2.4654233599400004,
2.62039649994,
2.40524425194,
2.48663814894,
2.67266468194,
2.51044421994
]
},
{
"command": "pnpm",
"mean": 6.214172447939999,
"stddev": 0.10622611442454018,
"median": 6.17695053894,
"user": 9.02524356,
"system": 4.50272718,
"min": 6.07542602094,
"max": 6.40312501894,
"times": [
6.14427921794,
6.07542602094,
6.10483766594,
6.18896217494,
6.16493890294,
6.16255544894,
6.26137360194,
6.40312501894,
6.30801483694,
6.32821158994
]
}
]
}Scenario: Frozen Lockfile (Hot Cache)
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6768961966800001,
"stddev": 0.05888396546159356,
"median": 0.6589319948800001,
"user": 0.34676855999999995,
"system": 1.44870996,
"min": 0.6383870583800001,
"max": 0.8384867593800001,
"times": [
0.8384867593800001,
0.68922598438,
0.65401368938,
0.6636074943800001,
0.64122870838,
0.67795795238,
0.6618005743800001,
0.6481903303800001,
0.65606341538,
0.6383870583800001
]
},
{
"command": "pacquet@main",
"mean": 0.67905866838,
"stddev": 0.03781291280071029,
"median": 0.66406757738,
"user": 0.33899636,
"system": 1.48404726,
"min": 0.64059221738,
"max": 0.74276311238,
"times": [
0.7185436353800001,
0.66213214738,
0.6504320883800001,
0.74276311238,
0.6658734283800001,
0.64776810238,
0.6623097923800001,
0.7343467973800001,
0.66582536238,
0.64059221738
]
},
{
"command": "pnpm",
"mean": 2.65134618048,
"stddev": 0.11599941512242547,
"median": 2.63420353688,
"user": 3.19323906,
"system": 2.21810536,
"min": 2.51805690038,
"max": 2.8707296733799996,
"times": [
2.76253661338,
2.51805690038,
2.66963038738,
2.8707296733799996,
2.6189944663799998,
2.55094827838,
2.5364098503799997,
2.64941260738,
2.76215266938,
2.57459035838
]
}
]
} |
Brings pacquet's test coverage for #397 items 12 and 16 in line with the upstream test suites. Two changes: 1. Concurrency tests. Split the `parallelism` input out of `default_child_concurrency` and `resolve_child_concurrency` into the public `..._with_parallelism` helpers so tests can pin the parallelism value (upstream's test suite mocks `os.availableParallelism`; pacquet injects it directly). Ports six tests from upstream's [`config/reader/src/concurrency.test.ts`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/config/reader/src/concurrency.test.ts): - `cpu num < 4` / `= 4` / `> 4` boundaries for the `min(4, p)` cap - `default workspace concurrency` (undefined input) - `match host cores amount` (n=0 → host cores) - `host cores minus X` (n=-1 → cores-1, n=-9999 → 1) The pacquet-only `resolve_child_concurrency_handles_i32_min` test (added in the previous commit) stays — it's coverage upstream doesn't have because TypeScript doesn't have `i32::MIN`. 2. `buildSequence` trim test. Direct port of upstream's [`buildSequence() test 2`](https://github.com/pnpm/pnpm/blob/b4f8f47ac2/building/during-install/test/buildSequence.test.ts#L28-L51): two importers `a` and `b` share a builder leaf `c`; only `a` needs its own build. The result must be `[[c], [a]]` — `b` is trimmed even though it's a root. The port surfaced a pacquet-side determinism gap. `get_subgraph_to_build` is order-sensitive (a root walked first marks shared children as already-walked, so a second root sharing those children gets trimmed), but `collect_root_dep_paths` was sourcing roots from `HashMap<_, ProjectSnapshot>` and `HashMap<PkgName, ResolvedDependencySpec>` — so iteration order varied run to run and the build sequence was flaky. Sorted the roots by `PackageKey` string repr so the trim outcome is reproducible. The long-term fix is to preserve lockfile declaration order via `IndexMap`; the sort is sufficient stabilization until that lands.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
crates/config/src/defaults.rs (2)
109-120: ⚡ Quick winUse
blob/mainfor upstream pnpm references instead of pinned SHAs.These inline links currently point to a specific commit hash, which makes long-term porting/cross-checking drift-prone in this repo. Please switch them to
https://github.com/pnpm/pnpm/blob/main/....As per coding guidelines: "If reading on GitHub, open files from
https://github.com/pnpm/pnpm/blob/main/...(which always resolves to the tip of main) rather than from a permalinked SHA."Also applies to: 126-127, 134-134, 148-148
🤖 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/config/src/defaults.rs` around lines 109 - 120, Update the inline GitHub permalinks in the doc comments to point to the moving tip of main instead of a SHA: replace occurrences of "https://github.com/pnpm/pnpm/blob/<sha>/..." with "https://github.com/pnpm/pnpm/blob/main/..." in the comments surrounding default_child_concurrency, default_child_concurrency_with_parallelism and the referenced getDefaultWorkspaceConcurrency links (also update the other similar comment links nearby). Ensure all doc comment links in this module use blob/main so they track upstream main.
117-123: ⚡ Quick winNarrow visibility for test-only helpers.
default_child_concurrency_with_parallelismandresolve_child_concurrency_with_parallelismare described as internal test helpers but are declaredpub. Consider reducing topub(crate)(or private) to avoid unnecessary public API exposure.Suggested minimal change
-pub fn default_child_concurrency_with_parallelism(parallelism: u32) -> u32 { +pub(crate) fn default_child_concurrency_with_parallelism(parallelism: u32) -> u32 { parallelism.min(4) } -pub fn resolve_child_concurrency_with_parallelism(option: Option<i32>, parallelism: u32) -> u32 { +pub(crate) fn resolve_child_concurrency_with_parallelism(option: Option<i32>, parallelism: u32) -> u32 { match option { None => default_child_concurrency_with_parallelism(parallelism), Some(n) if n > 0 => n as u32, Some(n) => parallelism.saturating_sub(n.unsigned_abs()).max(1), } }Also applies to: 146-151
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/config/src/defaults.rs` around lines 117 - 123, The two test-only helper functions default_child_concurrency_with_parallelism and resolve_child_concurrency_with_parallelism are declared public but should have narrower visibility; change their declarations from pub to pub(crate) (or remove pub entirely if only used within the crate) so they are not exposed in the public API, updating the function signatures for default_child_concurrency_with_parallelism and resolve_child_concurrency_with_parallelism accordingly.crates/config/src/defaults/tests.rs (1)
53-53: ⚡ Quick winNormalize upstream test-reference links to
blob/main.The new upstream references in test docs are pinned to a commit SHA. Please switch them to
blob/mainfor consistency with the repo’s pnpm-porting rule.As per coding guidelines: "If reading on GitHub, open files from
https://github.com/pnpm/pnpm/blob/main/...(which always resolves to the tip of main) rather than from a permalinked SHA."Also applies to: 61-61, 69-69, 77-77, 89-89, 99-99, 118-118
🤖 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/config/src/defaults/tests.rs` at line 53, The test doc links are pinned to a commit SHA; update each GitHub URL that points to "github.com/pnpm/pnpm/blob/<commit-sha>/..." and replace the "<commit-sha>" segment with "main" so they become "github.com/pnpm/pnpm/blob/main/...". Locate the referenced link near the comment text '`getDefaultWorkspaceConcurrency: cpu num < 4`' and apply the same change to the other occurrences in this file (the equivalent link anchors referenced in the comments around the other test descriptions) so all upstream pnpm links use blob/main.crates/package-manager/src/build_sequence/tests.rs (1)
176-177: ⚡ Quick winUse
pnpm/blob/mainfor upstream test reference linksPlease switch the doc link to
https://github.com/pnpm/pnpm/blob/main/...so the reference always points to current upstream behavior.As per coding guidelines: “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/build_sequence/tests.rs` around lines 176 - 177, Update the upstream reference URL in the doc comment inside crates/package-manager/src/build_sequence/tests.rs so it uses the branch-based path (/blob/main/...) instead of the permalinked SHA; locate the comment block containing the link to https://github.com/pnpm/pnpm/blob/b4f8f47ac2/... and replace that SHA segment with main so the doc link always points to current upstream behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/package-manager/src/build_sequence.rs`:
- Around line 144-155: The current fix sorts the initial roots via
roots.sort_by_key(|k| k.to_string()) but traversal remains non-deterministic
because child lists are constructed from HashMap iteration; update the
construction/traversal that feeds get_subgraph_to_build to deterministically
order children (e.g., when building adjacency lists or when collecting
dependency keys from the HashMap ensure you collect keys into a Vec and sort
them by PackageKey/string before iterating), so modify the code paths that
materialize child lists (the place that builds the per-node children passed into
get_subgraph_to_build) to sort by the same PackageKey string representation to
guarantee reproducible trimming across runs.
---
Nitpick comments:
In `@crates/config/src/defaults.rs`:
- Around line 109-120: Update the inline GitHub permalinks in the doc comments
to point to the moving tip of main instead of a SHA: replace occurrences of
"https://github.com/pnpm/pnpm/blob/<sha>/..." with
"https://github.com/pnpm/pnpm/blob/main/..." in the comments surrounding
default_child_concurrency, default_child_concurrency_with_parallelism and the
referenced getDefaultWorkspaceConcurrency links (also update the other similar
comment links nearby). Ensure all doc comment links in this module use blob/main
so they track upstream main.
- Around line 117-123: The two test-only helper functions
default_child_concurrency_with_parallelism and
resolve_child_concurrency_with_parallelism are declared public but should have
narrower visibility; change their declarations from pub to pub(crate) (or remove
pub entirely if only used within the crate) so they are not exposed in the
public API, updating the function signatures for
default_child_concurrency_with_parallelism and
resolve_child_concurrency_with_parallelism accordingly.
In `@crates/config/src/defaults/tests.rs`:
- Line 53: The test doc links are pinned to a commit SHA; update each GitHub URL
that points to "github.com/pnpm/pnpm/blob/<commit-sha>/..." and replace the
"<commit-sha>" segment with "main" so they become
"github.com/pnpm/pnpm/blob/main/...". Locate the referenced link near the
comment text '`getDefaultWorkspaceConcurrency: cpu num < 4`' and apply the same
change to the other occurrences in this file (the equivalent link anchors
referenced in the comments around the other test descriptions) so all upstream
pnpm links use blob/main.
In `@crates/package-manager/src/build_sequence/tests.rs`:
- Around line 176-177: Update the upstream reference URL in the doc comment
inside crates/package-manager/src/build_sequence/tests.rs so it uses the
branch-based path (/blob/main/...) instead of the permalinked SHA; locate the
comment block containing the link to
https://github.com/pnpm/pnpm/blob/b4f8f47ac2/... and replace that SHA segment
with main so the doc link always points to current upstream behavior.
🪄 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: 22643d68-7105-4353-b831-7fa7ab991d9f
📒 Files selected for processing (4)
crates/config/src/defaults.rscrates/config/src/defaults/tests.rscrates/package-manager/src/build_sequence.rscrates/package-manager/src/build_sequence/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). (7)
- GitHub Check: Agent
- GitHub Check: Code Coverage
- GitHub Check: Run benchmark on ubuntu-latest
- GitHub Check: Lint and Test (windows-latest)
- GitHub Check: Lint and Test (macos-latest)
- GitHub Check: Lint and Test (ubuntu-latest)
- GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs
📄 CodeRabbit inference engine (AGENTS.md)
**/*.rs: Preserve existing method chains andpipe-traitchains; do not break them into intermediateletbindings unless there is a concrete justification such as a compilation failure, borrow checker rejection, meaningful performance improvement, or other technical necessity. Refactoring for style alone is not sufficient justification.
Choose owned vs. borrowed parameters to minimize copies; prefer borrowed types (&Pathover&PathBuf,&strover&String) when it does not force extra copies.
PreferArc::clone(&x)andRc::clone(&x)overx.clone()for reference-counted types to make the cost visible at the call site.
Do not use star imports inside module bodies. Writeuse super::{Foo, bar}instead ofuse super::*;for any glob whose target is a module you control. External-crate preludes (e.g.,use rayon::prelude::*;) and root-of-module re-exports (e.g.,pub use submodule::*;inlib.rs) are exceptions.
Follow Rust API Guidelines for naming, as documented in https://rust-lang.github.io/api-guidelines/naming.html.
Declare a newtype wrapper for any branded string type being ported from TypeScript pnpm. Do not collapse the brand into a plainStringor&str; give the type its own struct so misuse is a type error.
When porting branded string types where upstream TypeScript always validates before construction, validate in the Rust port too. Construct the wrapper only viaTryFrom<String>and/orFromStr; do not provide an infallible public constructor that takes an arbitrary string.
For branded string types where upstream TypeScript never validates (used purely for type-safety to prevent confusion between string slots), expose an infallibleFrom<String>andFrom<&str>constructor in the Rust wrapper.
When upstream TypeScript occasionally constructs a branded type without validation (via bareasassertion), add afrom_str_unchecked(or similarly named) constructor on the Rust side. Keep the validating constructor as well; `from_str_u...
Files:
crates/package-manager/src/build_sequence.rscrates/package-manager/src/build_sequence/tests.rscrates/config/src/defaults/tests.rscrates/config/src/defaults.rs
🧠 Learnings (2)
📚 Learning: 2026-05-07T23:19:08.272Z
Learnt from: KSXGitHub
Repo: pnpm/pacquet PR: 401
File: tasks/integrated-benchmark/src/work_env.rs:343-344
Timestamp: 2026-05-07T23:19:08.272Z
Learning: When reviewing Rust code in pnpm/pacquet for deprecated API usage, do not automatically treat `serde_saphyr::to_string` as deprecated. In `serde-saphyr` v0.0.25, `serde_saphyr::to_string` has no `#[deprecated]` attribute (the `#[deprecated]` later in `serde-saphyr-0.0.25/src/lib.rs` applies to a different function). Only flag `serde_saphyr::to_string` as deprecated if the resolved dependency version’s source shows `#[deprecated]` on that specific function.
Applied to files:
crates/package-manager/src/build_sequence.rscrates/package-manager/src/build_sequence/tests.rscrates/config/src/defaults/tests.rscrates/config/src/defaults.rs
📚 Learning: 2026-05-01T10:01:33.766Z
Learnt from: zkochan
Repo: pnpm/pacquet PR: 349
File: crates/reporter/src/tests.rs:121-121
Timestamp: 2026-05-01T10:01:33.766Z
Learning: In Rust test code, follow the repo’s CODE_STYLE_GUIDE test-logging rule: add logging (e.g., `eprintln!`/`eprintln!(...)`) so that useful diagnostic values are printed when a test fails, unless the assertion is `assert_eq!` (where the differing values are already included). Concretely, if you use assertions like `assert!`, `assert_ne!`, etc., ensure the test logs the relevant actual/expected values (or context) before/around the assertion so failures can be diagnosed without rerunning.
Applied to files:
crates/package-manager/src/build_sequence/tests.rscrates/config/src/defaults/tests.rs
🔇 Additional comments (1)
crates/config/src/defaults/tests.rs (1)
52-140: Great test coverage and determinism for concurrency semantics.These additions pin the important edge cases (
None, zero, negative offsets, positive passthrough, andi32::MIN) with deterministic inputs and exact assertions.
The previous commit sorted the *roots* in `collect_root_dep_paths`, but `build_children_map` still constructed per-snapshot child lists by iterating `HashMap` deps — so transitive shared descendants (root → A → [B, C] → shared D) could still flip the trim result across runs. Sort children for the same reason roots are sorted, so every entry-list `get_subgraph_to_build` walks is deterministic end-to-end. Review-suggested by CodeRabbit in PR #429.
Every existing `BuildModules` test runs with `child_concurrency: 1`, which is the sequential codepath. The rayon `pool.install` / `chunk.par_iter().try_for_each(...)` dispatch added for #397 item 12 isn't exercised — a regression that dropped the `Mutex` around `ignored_builds` / `deps_state_cache`, or removed the `pool.install` wrapping, wouldn't be caught by the sequential tests. Adds `build_modules_collects_ignored_builds_under_concurrency` — the same fixture as `build_modules_collects_ignored_builds` but `child_concurrency: 2`. Two policy-denied build candidates land in chunk 0 with no dependency edges between them, so the rayon dispatch actually runs them concurrently. The `BTreeSet`-backed `ignored_builds` normalizes the insertion order, so the expected sorted output is the same as the sequential variant. Review-suggested by Copilot in PR #429.
| .run::<SilentReporter>() | ||
| .expect("run BuildModules under concurrency"); | ||
| dbg!(&ignored); | ||
|
|
There was a problem hiding this comment.
Keeping the dbg! per pacquet's documented test-logging convention in CODE_STYLE_GUIDE.md:
If the values being compared with
assert_eq!have complex structures (such as a struct or an array), they should be logged withdbg!.
ignored is Vec<String>, which qualifies as an array under that rule, and the sibling tests (build_modules_collects_ignored_builds, build_modules_excludes_explicit_deny_from_ignored, do_not_fail_on_optional_dep_with_failing_postinstall) all use the same dbg!(&ignored); pattern. The harness captures debug output on successful runs, so it won't show in CI logs unless the test fails. Removing it here would diverge from the established convention.
Written by an agent (Claude Code, claude-opus-4-7).
Summary
Bundles the three remaining moderate/minor parity items from #397 into one PR. Each adds a
pnpm-workspace.yamlsetting, threads it throughConfig, and applies it toBuildModulesso a pnpm yaml that exercises these knobs produces the same install behavior in pacquet.childConcurrency(upstreamgetWorkspaceConcurrency;runGroups(...)). AddsConfig.child_concurrency: u32with the upstream-matching defaultmin(4, availableParallelism())and the negative-offset semantics (n < 0→max(1, parallelism - |n|)).BuildModules::runnow dispatches each chunk's members across a bounded rayon thread pool viapar_iter().try_for_each— chunks themselves remain sequential to preserve topological order.ignored_buildsanddeps_state_cacheare wrapped inMutexso the recursive memo and the dedup set survive concurrent chunk-member access.unsafePerm. AddsConfig.unsafe_perm: bool(defaulttrue) and threads it through toRunPostinstallHooks. Whenfalse, the executor setsTMPDIR=node_modules/.tmpfor the spawn. The uid/gid drop side is a no-op upstream too because pnpm's@pnpm/npm-lifecyclenever populatesopts.user/opts.group. The POSIX auto-detect (getuid() === 0 && setgid → unsafePerm = false) needslibc, which isn't in[workspace.dependencies]yet — for now, root-run CI must setunsafePerm: falsein yaml explicitly. Windows is force-overridden totrueinWorkspaceSettings::apply_to, matching upstream'sprocess.platform === 'win32'gate.scriptsPrependNodePath(Config.scriptsPrependNodePath). Adds a tri-stateScriptsPrependNodePathenum inpacquet-config(custom serdeDeserializefor theboolean | "warn-only"yaml shape) and converts topacquet_executor::ScriptsPrependNodePathat theBuildModulescall site so the executor crate stays free of serde wiring. Default isNeverto match upstream'sStrictBuildOptions.scriptsPrependNodePath: false.getSubgraphToBuildfilter trimming. Already implemented in pacquet'sget_subgraph_to_buildvia thechild_should_be_built || needs_build || has_patchgate. Confirmed during this slice; no code change needed.Why one PR: items 12 / 14 / 15 are three independent yaml-driven config knobs, all small. Bundling keeps the cross-crate signature churn from happening three times (
BuildModules's field list,install_frozen_lockfile's call site, and the test fixtures all touch each one), and the upstream sources for the three settings live next to each other so the porting context is shared.Test plan
cargo nextest run— all 605 workspace tests pass.just ready— typos, fmt, check, lint clean.cargo doc -D warnings --document-private-items— clean.taplo format --check— clean.parses_scripts_prepend_node_path_{true,false,warn_only}_from_yaml,rejects_invalid_scripts_prepend_node_path.parses_unsafe_perm_from_yaml_and_applies,unsafe_perm_force_true_on_windows(Windows-only).parses_{positive,negative}_child_concurrency_from_yaml_and_*.BuildModulestests cover the concurrency-1 path (so the rayon refactor doesn't regress the single-threaded case).Written by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit
New Features
Behavior
Tests
Documentation