Context
Pacquet's resolver inherits a longstanding pnpm-CLI quirk: on revisit of a package, ResolvedPackage.optional is AND-folded only on the directly-visited package — not on its transitive descendants. Walked first via an optional path and then revisited via a non-optional path, the direct package gets optional: false correctly, but its already-walked children stay stuck at optional: true.
Concrete scenario:
Walk order: optional → A → C (sets C.optional = true), then prod → B → A (revisits A, sets A.optional = false). C should now be optional: false (reachable through the all-non-optional path prod → B → A → C) but the in-memory ResolvedPackage.optional stays true.
Upstream observed at:
How pnpm CLI hides this from users
lockfile/pruner/src/index.ts:160-205 (copyDependencySubGraph) does a BFS from importers through the dep graph, tracking a nonOptional set. Any package reached by an all-non-optional path is stamped optional: false on both depLockfile (the snapshot) and dependenciesGraph (the in-memory graph the installer reads). Install-time consumers — e.g. installing/deps-installer/src/install/link.ts:92 and link.ts:519 — read from the post-pruner snapshot or graph, so they never see the stale resolver value.
Why pacquet is exposed
Pacquet has no equivalent pruner pass. dependencies_graph_to_lockfile.rs:409 writes the raw node.optional (sourced from the resolver) straight to the lockfile snapshot, and the install-time gates (create_virtual_store.rs:762 for fetch-side failures, installability.rs:394 for build-side) read from that snapshot. So a fetch or build failure on a transitively-required package can be silently tolerated as if it were optional.
Proposal
Port copyDependencySubGraph's BFS into pacquet. Run it after resolve_importer returns (or as part of dependencies_graph_to_lockfile) so the lockfile and any in-memory consumers see the corrected optional values.
Add a regression test that builds the scenario above and asserts that C.optional == false after resolution.
Why not fix the resolver directly
Fixing the resolver to do the optional fold during the walk (re-walking the cached subtree on every revisit) defeats the lazy-children optimization that #11915 just introduced. The pruner BFS is O(V+E) and runs once at the end, so it's strictly cheaper than per-revisit re-walks. Upstream made the same design choice — keeping the resolver fast and centralising the correction at the lockfile/graph boundary.
Follow-up to
#11915 — surfaced during CodeRabbit review of the lazy-children PR.
Written by an agent (Claude Code, claude-opus-4-7).
Context
Pacquet's resolver inherits a longstanding pnpm-CLI quirk: on revisit of a package,
ResolvedPackage.optionalis AND-folded only on the directly-visited package — not on its transitive descendants. Walked first via an optional path and then revisited via a non-optional path, the direct package getsoptional: falsecorrectly, but its already-walked children stay stuck atoptional: true.Concrete scenario:
Walk order:
optional → A → C(setsC.optional = true), thenprod → B → A(revisits A, setsA.optional = false). C should now beoptional: false(reachable through the all-non-optional pathprod → B → A → C) but the in-memoryResolvedPackage.optionalstaystrue.Upstream observed at:
resolveDependencies.ts:1627-1648— revisit branch AND-folds without recursion.How pnpm CLI hides this from users
lockfile/pruner/src/index.ts:160-205(copyDependencySubGraph) does a BFS from importers through the dep graph, tracking anonOptionalset. Any package reached by an all-non-optional path is stampedoptional: falseon bothdepLockfile(the snapshot) anddependenciesGraph(the in-memory graph the installer reads). Install-time consumers — e.g.installing/deps-installer/src/install/link.ts:92andlink.ts:519— read from the post-pruner snapshot or graph, so they never see the stale resolver value.Why pacquet is exposed
Pacquet has no equivalent pruner pass.
dependencies_graph_to_lockfile.rs:409writes the rawnode.optional(sourced from the resolver) straight to the lockfile snapshot, and the install-time gates (create_virtual_store.rs:762for fetch-side failures,installability.rs:394for build-side) read from that snapshot. So a fetch or build failure on a transitively-required package can be silently tolerated as if it were optional.Proposal
Port
copyDependencySubGraph's BFS into pacquet. Run it afterresolve_importerreturns (or as part ofdependencies_graph_to_lockfile) so the lockfile and any in-memory consumers see the correctedoptionalvalues.Add a regression test that builds the scenario above and asserts that
C.optional == falseafter resolution.Why not fix the resolver directly
Fixing the resolver to do the optional fold during the walk (re-walking the cached subtree on every revisit) defeats the lazy-children optimization that #11915 just introduced. The pruner BFS is O(V+E) and runs once at the end, so it's strictly cheaper than per-revisit re-walks. Upstream made the same design choice — keeping the resolver fast and centralising the correction at the lockfile/graph boundary.
Follow-up to
#11915 — surfaced during CodeRabbit review of the lazy-children PR.
Written by an agent (Claude Code, claude-opus-4-7).