Skip to content

fix(deps-resolver): make shared children resolution deterministic#12362

Merged
zkochan merged 1 commit into
mainfrom
fix/12358
Jun 12, 2026
Merged

fix(deps-resolver): make shared children resolution deterministic#12362
zkochan merged 1 commit into
mainfrom
fix/12358

Conversation

@zkochan

@zkochan zkochan commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

  • Make shared package child resolution deterministic by choosing the owner by depth, importer order, and parent path instead of async completion timing.
  • Keep non-owner and stale occurrences lazy while reusing the current owner children and missing-peer context.
  • Port the same behavior to pacquet and add TypeScript plus Rust regression coverage.

Verification

  • pnpm --filter @pnpm/installing.deps-resolver test test/resolveDependencyTree.test.ts
  • cargo test -p pacquet-resolving-deps-resolver
  • Pre-push hook: TypeScript typecheck, pnpm bundle, lint, spellcheck, meta lint, cargo fmt, cargo doc, cargo dylint, taplo format check

Fixes #12358


Written by an agent (Codex, GPT-5).

Summary by CodeRabbit

  • New Features

    • Shared package children are resolved deterministically (shallowest → importer order → parent path), making dependency selection predictable.
  • Bug Fixes

    • Missing-peer reporting stabilized so warnings are consistent and tied to the owning importer context.
  • Tests

    • Added tests validating deterministic shared-child resolution and preferred-version behavior.
  • Documentation

    • Added changelog entry documenting the deterministic child-resolution behavior.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 91dd0c4f-bfa9-4532-a778-9636ddbeb7e0

📥 Commits

Reviewing files that changed from the base of the PR and between 90a685c and 25bffbb.

📒 Files selected for processing (10)
  • .changeset/deterministic-shared-children-resolution.md
  • installing/deps-resolver/src/resolveDependencies.ts
  • installing/deps-resolver/src/resolveDependencyTree.ts
  • installing/deps-resolver/test/resolveDependencyTree.test.ts
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs

📝 Walkthrough

Walkthrough

Replace race-based shared-package child selection with deterministic ownership (depth → importer_order → parent_path); add package-resolution barrier, staleness/redo logic, importer-order wiring across TypeScript and Rust resolvers, and regression tests validating deterministic shallow-owner behavior.

Changes

TypeScript: Deterministic Shared-Children Resolution

Layer / File(s) Summary
Changeset note and description
.changeset/deterministic-shared-children-resolution.md
Adds a changeset documenting the deterministic shared-children resolution rule and patch bumps.
Resolution context and data model
installing/deps-resolver/src/resolveDependencies.ts, installing/deps-resolver/src/resolveDependencyTree.ts
ResolutionContext gains package-resolution barrier, per-package children-resolution tracking, importerResolutionOrder, and nodeResolutionContextByNodeId; PkgAddress adds optional childrenResolutionId. buildTree is imported rather than defined inline.
Concurrency and ownership machinery
installing/deps-resolver/src/resolveDependencies.ts
Adds depth "turn" coordination, children-resolution ownership claiming/comparison (including lexicographic parent-path tie-break), deferred MissingPeers creation, staleness checks, and dependency-tree node update utilities.
Resolution entry points and staleness validation
installing/deps-resolver/src/resolveDependencies.ts
resolveDependency runs under the package-resolution barrier and claims children-resolution ownership; deferred resolveChildren calls are bound to a childrenResolutionId, and resolveChildren short-circuits or re-derives missing peers when its children-resolution becomes stale.
Test verification for deterministic shared-children resolution
installing/deps-resolver/test/resolveDependencyTree.test.ts
Adds a Jest test and helpers that mock StoreController and preferred-version selection, asserting both shared nodes resolve their provider:* child to the same preferred version selected by the deterministic owner.

Rust: Deterministic Children Ownership

Layer / File(s) Summary
Ownership model and workspace tracking
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
Introduce ChildrenOwner with tie-breaking by (depth, importer_order, parent_path) and workspace maps (children_owner_by_id, node_parent_ids_by_id) to track owner and parent-id chains.
Importer ordering and TreeCtx wiring
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs, pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs, pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
Add importer_order to TreeCtx with with_importer_order() and thread importer_order through resolve_importer/resolve_workspace calls; set defaults and wire per-importer ordering.
Node resolution refactoring with ownership checks
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
Refactor resolve_node / resolve_reused_node to claim ownership, choose realized vs TreeChildren::Lazy based on ownership, write shared children_by_id only for the owner, record parent-id chains, and retroactively mark non-owners lazy.
Missing-peer tracking and hoist-scope refactor
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs, pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
record_first_walk_missing now accepts importer_id and records missing peers only when the package's children owner matches that importer; peer cache and walker now track children-only missing-peer sets used to compute final missing_names_by_pkg; HoistMissingScope creation now uses per-importer snapshots.
Test coverage for deterministic revisit ordering
pacquet/crates/resolving-deps-resolver/src/tests.rs, pacquet/crates/resolving-deps-resolver/src/resolve_peers/tests.rs
Add DelayedAliasResolver and async tests to simulate timing-dependent resolution and assert the shallower occurrence owns the children context; add a peer-resolution test ensuring missing_names_by_pkg records only children-context missing peers.

Sequence Diagram(s)

(omitted — changes modify control flow but diagrams would require reproducing many concrete function calls beyond summaries)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pnpm/pnpm#11915: Closely related Rust changes to revisit/lazy-children handling in resolve_dependency_tree.rs.
  • pnpm/pnpm#11826: Related TypeScript changes addressing timing nondeterminism in resolveDependencies.ts.
  • pnpm/pnpm#12349: Overlapping work on missing-peer/owner-tracking and hoist suppression semantics.

"I hopped through depths and ordered lanes,
Chose shallow roots where ownership reigns.
No more races, no flaky games,
Shared children stable — no more flames.
🐇✨"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: making shared children resolution deterministic by replacing async race-based selection with a deterministic ownership model.
Linked Issues check ✅ Passed All coding requirements from issue #12358 are met: deterministic winner selection using (depth, importer order, parent path), takeover machinery for out-of-order arrivals, tracking of children-resolution ownership, and parallel implementation in both pnpm and pacquet with regression test coverage.
Out of Scope Changes check ✅ Passed All changes directly support the deterministic shared children resolution objective. File modifications across TypeScript and Rust codebases consistently implement ownership-based selection, update tracking fields, and add regression tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/12358

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


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

❤️ Share

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

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

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (7) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Grey Divider


Action required

1. Stale owner skips tree node 🐞 Bug ☼ Reliability
Description
In resolveDependenciesOfDependency(), the early stale-owner short-circuit returns missing-peers
without calling resolveChildren(), so resolveChildren()’s stale handling
(setDependencyTreeNodeWithCurrentChildren) never runs and the occurrence may never be added to
ctx.dependenciesTree. Downstream code assumes ctx.dependenciesTree has an entry for every non-linked
dep.nodeId (ctx.dependenciesTree.get(dep.nodeId)!), so this can crash or yield an incomplete tree
when ownership flips between claim and postponedResolution execution.
Code

installing/deps-resolver/src/resolveDependencies.ts[R1008-1011]

postponedResolution: async (postponedResolutionOpts) => {
+      if (!isCurrentChildrenResolution(ctx, resolveDependencyResult.pkgId, resolveDependencyResult.childrenResolutionId)) {
+        return resolveMissingPeersFromCurrentChildrenResolution(ctx, resolveDependencyResult.pkgId, postponedResolutionOpts.parentPkgAliases)
+      }
Evidence
The stale-owner pre-check returns without invoking resolveChildren (so no tree node is created
there), while owner occurrences also skip pendingNodes (which is the other mechanism used to
populate dependenciesTree). resolveDependencyTree later drains pendingNodes into dependenciesTree
and then dereferences dependenciesTree entries for direct deps with a non-null assertion, so missing
nodes can crash.

installing/deps-resolver/src/resolveDependencies.ts[994-1021]
installing/deps-resolver/src/resolveDependencies.ts[1314-1324]
installing/deps-resolver/src/resolveDependencies.ts[721-777]
installing/deps-resolver/src/resolveDependencies.ts[1935-2005]
installing/deps-resolver/src/resolveDependencyTree.ts[316-352]

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

## Issue description
A package occurrence can lose children-ownership between `resolveDependency()` and the later `postponedResolution()` execution (because `resolveDependencies()` resolves deps in parallel, then runs all postponed resolutions). When that happens, `postponedResolution()` currently returns early on the stale check without calling `resolveChildren()`, which means the dependency-tree node for that occurrence is never created/updated.
Downstream, `resolveDependencyTree()` assumes `ctx.dependenciesTree` contains an entry for each non-linked `dep.nodeId` (it uses a non-null assertion), so missing entries can crash or produce an inconsistent tree.
## Issue Context
- `resolveChildren()` *does* handle staleness by calling `setDependencyTreeNodeWithCurrentChildren()`, but that logic is bypassed when `postponedResolution()` returns before calling `resolveChildren()`.
## Fix Focus Areas
- installing/deps-resolver/src/resolveDependencies.ts[1008-1015]
### Suggested fix
On the `postponedResolution` stale pre-check (`!isCurrentChildrenResolution(...)`), call the same fallback used inside `resolveChildren()`:
- `setDependencyTreeNodeWithCurrentChildren(ctx, { parentDepth: options.currentDepth, parentIds: [...options.parentIds, resolveDependencyResult.pkgId], parentPkg: resolveDependencyResult })`
Then return the missing-peers result.
This ensures the occurrence is represented in `ctx.dependenciesTree` even when it becomes a non-owner before its postponed resolution runs.

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



Remediation recommended

2. Barrier wakeup thrash 🐞 Bug ➹ Performance ⭐ New
Description
The package-resolution barrier resolves all queued waiters on every package completion, even when
most waiters are still blocked by shallower active depths, causing repeated wake→check→re-wait
cycles and avoidable CPU overhead on large graphs.
Code

installing/deps-resolver/src/resolveDependencies.ts[R1054-1077]

+function startPackageResolution (ctx: ResolutionContext, depth: number): () => void {
+  const activeCount = ctx.packageResolutionBarrier.activeByDepth.get(depth) ?? 0
+  ctx.packageResolutionBarrier.activeByDepth.set(depth, activeCount + 1)
+  let finished = false
+  return () => {
+    if (finished) return
+    finished = true
+    const nextActiveCount = (ctx.packageResolutionBarrier.activeByDepth.get(depth) ?? 1) - 1
+    if (nextActiveCount > 0) {
+      ctx.packageResolutionBarrier.activeByDepth.set(depth, nextActiveCount)
+    } else {
+      ctx.packageResolutionBarrier.activeByDepth.delete(depth)
+    }
+    const waiters = ctx.packageResolutionBarrier.waiters.splice(0)
+    for (const resolve of waiters) {
+      resolve()
+    }
+  }
+}
+
+async function waitForPackageResolutionTurn (ctx: ResolutionContext, depth: number): Promise<void> {
+  if (!hasActivePackageResolutionBeforeDepth(ctx, depth)) return
+  await new Promise<void>((resolve) => ctx.packageResolutionBarrier.waiters.push(resolve))
+  return waitForPackageResolutionTurn(ctx, depth)
Evidence
The barrier completion path unconditionally drains and resolves every waiter, and each waiter
re-checks and may immediately re-register, which scales poorly as waiter count grows.

installing/deps-resolver/src/resolveDependencies.ts[1054-1077]

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

### Issue description
`startPackageResolution()` drains and resolves the entire `packageResolutionBarrier.waiters` list on every completion, and `waitForPackageResolutionTurn()` re-enqueues via recursion if still blocked. This creates churn (many promise resolutions and re-waits) when there are lots of concurrent resolutions at different depths.

### Issue Context
The barrier is intended to enforce depth ordering for deterministic shared-children ownership, but it currently uses a single global waiter queue and broadcasts on every completion.

### Fix Focus Areas
- Replace the single broadcast waiter list with per-depth (or per-threshold) wait queues and only notify those depths that may have become unblocked.
- Alternatively, track the current minimum active depth and only wake waiters whose `depth` is now eligible.
- Avoid recursive re-wait loops by using a `while (blocked) await ...` pattern with a single shared notification primitive.

- installing/deps-resolver/src/resolveDependencies.ts[1054-1077]

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


3. Cache-hit hides own peers 🐞 Bug ≡ Correctness
Description
In pacquet’s peers-cache hit path, cached missing peers (children + own) are re-emitted under
hoist-missing suppression keyed off first_walk_missing_by_pkg, but this PR now populates that map
from children-only missing_names_by_pkg. When chain_with_self includes the package itself,
cached own-peer misses can be suppressed because the owner’s children-only missing set will not
contain them, skewing hoist inputs and warnings.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[R624-627]

+        for (node_id, missing) in &self.node_missing_peers_of_children {
          let Some(tree_node) = self.tree.dependencies_tree.get(node_id) else { continue };
          missing_names_by_pkg
              .entry(tree_node.resolved_package_id.clone())
Evidence
The PR changes missing_names_by_pkg (and therefore first_walk_missing_by_pkg) to be
children-only, but the cache-hit branch still suppresses the full cached missing set
(missing_peers, which includes own misses). This mismatch makes it possible for own-peer misses to
be suppressed on cache hits when the package itself is part of the suppression chain.

pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[622-636]
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[885-914]
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[987-991]
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[1094-1099]
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[172-185]
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[331-344]
pacquet/crates/resolving-deps-resolver/src/resolve_peers/tests.rs[146-195]

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

## Issue description
On peers-cache hits, `resolve_peers` re-emits cached `missing_peers` (which is the union of children+own missing peers) but applies `missing_issue_suppressed(...)` using `first_walk_missing_by_pkg`, which this PR now fills from `missing_names_by_pkg` derived from **children-only** misses. This can incorrectly suppress a package’s **own** missing-peer diagnostics on cache-hit revisits during hoist passes.
### Issue Context
- `missing_names_by_pkg` is now constructed from `node_missing_peers_of_children` (children-only).
- The peers cache still stores:
- `missing_peers` = children + own
- `missing_peers_of_children` = children-only
- The cache-hit re-emission loop currently suppresses *all* entries in `missing_peers`, even those not present in `missing_peers_of_children`.
### Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[885-910]
### Suggested fix
In the cache-hit branch, only apply `missing_issue_suppressed(...)` to cached misses that originate from children context (i.e. keys present in `missing_of_children` / `cached.missing_peers_of_children`). For cached misses that are *own-peer* misses (present in `missing` but not in `missing_of_children`), always emit them (no suppression) so hoist-time diagnostics match the intended “children-only suppression” model.

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


4. Quadratic node updates 🐞 Bug ➹ Performance
Description
updateChildrenResolutionNodes() linearly scans every recorded node context and is invoked after each
resolveChildren() completion, which can compound into O(N^2) work and noticeably slow resolution on
large graphs.
Code

installing/deps-resolver/src/resolveDependencies.ts[R1227-1237]

+function updateChildrenResolutionNodes (
+  ctx: ResolutionContext,
+  pkgId: PkgResolutionId,
+  currentNodeId: NodeId
+): void {
+  for (const [nodeId, nodeContext] of ctx.nodeResolutionContextByNodeId) {
+    if (nodeId === currentNodeId || nodeContext.pkgId !== pkgId) continue
+    const node = ctx.dependenciesTree.get(nodeId)
+    if (node == null || node.depth === -1) continue
+    node.children = () => buildTree(ctx, pkgId, nodeContext.parentIds, ctx.childrenByParentId[pkgId] ?? [], nodeContext.depth + 1, nodeContext.installable)
+  }
Evidence
The update function performs a full iteration over ctx.nodeResolutionContextByNodeId, and it is
called every time resolveChildren() finishes setting the node context, so the scan cost repeats
across many packages.

installing/deps-resolver/src/resolveDependencies.ts[1227-1237]
installing/deps-resolver/src/resolveDependencies.ts[1399-1406]

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

## Issue description
`updateChildrenResolutionNodes()` iterates the entire `nodeResolutionContextByNodeId` map every time children are resolved, and is called unconditionally at the end of `resolveChildren()`. On large trees this produces repeated full scans and can become a major runtime cost.
### Issue Context
This scan is only needed to retarget *other occurrences* of the same `pkgId` when shared-children ownership changes. Doing a full-map scan for every owner-resolution is unnecessarily expensive.
### Fix Focus Areas
- installing/deps-resolver/src/resolveDependencies.ts[1227-1237]
- installing/deps-resolver/src/resolveDependencies.ts[1399-1406]
### Suggested approach
- Maintain an index from `pkgId -> Set<NodeId>` (or `pkgId -> NodeId[]`) in the resolution context.
- Update it wherever `nodeResolutionContextByNodeId.set(...)` is called.
- Change `updateChildrenResolutionNodes()` to iterate only the NodeIds for the given `pkgId`, rather than scanning the full map.
- Optionally, skip the update entirely when the set has size `<= 1` (no other occurrences to retarget).
- If possible, only invoke the update when ownership actually *changed* (e.g., `claimChildrenResolution` can return a `didTakeover`/`replacedExisting` flag).

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


View more (2)
5. Owner update scans tree 🐞 Bug ➹ Performance
Description
make_non_owner_nodes_lazy() clones the entire node→parentIds map and then iterates the full
dependencies tree on each ownership update, which can become quadratic and adds significant memory
churn for large workspaces.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[R1415-1428]

+fn make_non_owner_nodes_lazy(ctx: &TreeCtx, pkg_id: &str, owner_node_id: &NodeId) {
+    let parent_ids_by_node = lock_recoverable(&ctx.workspace.node_parent_ids_by_id).clone();
+    let mut tree = lock_recoverable(&ctx.workspace.dependencies_tree);
+    for (node_id, node) in tree.iter_mut() {
+        if node_id == owner_node_id || node.resolved_package_id != pkg_id {
+            continue;
+        }
+        let Some(parent_ids) = parent_ids_by_node.get(node_id) else {
+            continue;
+        };
+        node.children =
+            crate::resolved_tree::TreeChildren::Lazy { parent_ids: Arc::clone(parent_ids) };
+    }
+}
Evidence
The new helper explicitly clones the full node_parent_ids_by_id map and then performs a complete
dependencies_tree iteration; it is invoked on ownership confirmation, so the scan can happen
repeatedly during resolution.

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1415-1428]
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1357-1359]

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

## Issue description
On each ownership confirmation, `make_non_owner_nodes_lazy()` clones `node_parent_ids_by_id` and then scans the entire `dependencies_tree` to relabel non-owner occurrences as `Lazy`. This is an O(tree_size) operation that can repeat many times, compounding runtime and memory overhead.
### Issue Context
This relazy operation only needs to touch nodes whose `resolved_package_id == pkg_id` (and excluding the owner node). Scanning everything is unnecessary once you have an index.
### Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1357-1359]
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1415-1428]
### Suggested approach
- Maintain an index `pkg_id -> Vec<NodeId>` (or `HashSet<NodeId>`) for occurrences in `WorkspaceTreeCtx`.
- Update it whenever a node is inserted/updated in `dependencies_tree`.
- Rework `make_non_owner_nodes_lazy()` to iterate only the occurrence NodeIds for that `pkg_id`.
- Avoid cloning the whole `node_parent_ids_by_id` map:
- Look up `parent_ids` for each target node directly from the map (or store `parent_ids` on the tree node itself so the relazy update doesn’t need a second map).
- Keep the locking strategy safe (minimize lock hold time; avoid nested locks if possible).

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


6. Resolution barrier hurts parallelism 🐞 Bug ➹ Performance
Description
resolveDependency() now waits on a global depth barrier before calling
storeController.requestPackage(), which can serialize network/cache work across the whole workspace
and slow installs on large graphs. The barrier is held for the full resolveDependency() duration,
further extending the critical section beyond the ownership-claim logic the PR is trying to order.
Code

installing/deps-resolver/src/resolveDependencies.ts[R1668-1671]

+  const finishPackageResolution = startPackageResolution(ctx, options.currentDepth)
try {
-    const calcSpecifier = options.currentDepth === 0
-    if (!options.update && currentPkg.version && currentPkg.pkgId?.endsWith(`@${currentPkg.version}`) && !calcSpecifier) {
-      wantedDependency.bareSpecifier = replaceVersionInBareSpecifier(wantedDependency.bareSpecifier, currentPkg.version, ctx.namedRegistryPrefixes)
-    }
-    pkgResponse = await ctx.storeController.requestPackage(wantedDependency, {
-      allowBuild: ctx.allowBuild,
-      alwaysTryWorkspacePackages: ctx.linkWorkspacePackagesDepth >= options.currentDepth,
-      currentPkg: currentPkg
-        ? {
-          id: currentPkg.pkgId,
-          name: currentPkg.name,
-          resolution: currentPkg.resolution,
-          version: currentPkg.version,
-          publishedAt: currentPkg.pkgId ? ctx.wantedLockfile.time?.[currentPkg.pkgId] : undefined,
-        }
-        : undefined,
-      expectedPkg: currentPkg,
-      defaultTag: ctx.defaultTag,
-      ignoreScripts: ctx.ignoreScripts,
-      publishedBy: options.publishedBy,
-      publishedByExclude: ctx.publishedByExclude,
-      pickLowestVersion: options.pickLowestVersion,
-      downloadPriority: -options.currentDepth,
-      lockfileDir: ctx.lockfileDir,
-      preferredVersions,
-      preferWorkspacePackages: ctx.preferWorkspacePackages,
-      projectDir: (
-        options.currentDepth > 0 &&
-        !wantedDependency.bareSpecifier.startsWith('file:')
-      )
-        ? ctx.lockfileDir
-        : options.parentPkg.rootDir,
-      skipFetch: ctx.dryRun,
-      trustPolicy: ctx.trustPolicy,
-      trustPolicyExclude: ctx.trustPolicyExclude,
-      trustPolicyIgnoreAfter: ctx.trustPolicyIgnoreAfter,
-      update: options.update,
-      updateChecksums: options.updateChecksums,
-      workspacePackages: ctx.workspacePackages,
-      supportedArchitectures: options.supportedArchitectures,
-      onFetchError: (err: any) => { // eslint-disable-line
-        err.prefix = options.prefix
-        err.pkgsStack = getPkgsInfoFromIds(options.parentIds, ctx.resolvedPkgsById)
-        return err
-      },
-      injectWorkspacePackages: ctx.injectWorkspacePackages,
-      calcSpecifier,
-      pinnedVersion: options.pinnedVersion,
-    })
-  } catch (err: any) { // eslint-disable-line
-    const wantedDependencyDetails = {
-      name: wantedDependency.alias,
-      bareSpecifier: wantedDependency.bareSpecifier,
-      version: wantedDependency.alias ? wantedDependency.bareSpecifier : undefined,
-    }
-    if (wantedDependency.optional && err.code !== 'ERR_PNPM_TRUST_DOWNGRADE') {
-      skippedOptionalDependencyLogger.debug({
-        details: err.toString(),
-        package: wantedDependencyDetails,
-        parents: getPkgsInfoFromIds(options.parentIds, ctx.resolvedPkgsById),
-        prefix: options.prefix,
-        reason: 'resolution_failure',
+    await waitForPackageResolutionTurn(ctx, options.currentDepth)
+
Evidence
The barrier is acquired at the start of resolveDependency(), then blocks on shallower depths before
requestPackage(), and is only released in the function’s finalizer—so it serializes the most
expensive I/O step (package requests) and the rest of resolveDependency’s async work across depths.

installing/deps-resolver/src/resolveDependencies.ts[1668-1728]
installing/deps-resolver/src/resolveDependencies.ts[2038-2040]

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

## Issue description
`resolveDependency()` currently gates *all* package resolution work at depth `d` behind completion of any active work at shallower depths, because it awaits `waitForPackageResolutionTurn()` before `requestPackage()`, and holds the barrier until the end of `resolveDependency()`.
This reduces parallelism (especially packument/tarball fetch overlap) and can slow down real-world installs, even though determinism is already enforced by the `(depth, importer order, parent path)` ownership comparison + takeover machinery.
### Issue Context
The determinism goal is to make shared-children ownership independent of async timing. That can be achieved without globally ordering *fetch/resolve* across depths: you can allow `requestPackage()` to run concurrently, then apply deterministic ownership selection when the `pkgId` is known (and only serialize the parts that must be consistent: ownership claim + shared children/missing-peer publication).
### Fix Focus Areas
- installing/deps-resolver/src/resolveDependencies.ts[1668-1671]
- installing/deps-resolver/src/resolveDependencies.ts[1049-1073]
Concretely, consider one of:
- Move `await waitForPackageResolutionTurn(...)` to just before `claimChildrenResolution(...)` / shared-children publication (after `pkgResponse.body.id` is known), so fetches stay parallel.
- Or remove the barrier entirely and rely on `claimChildrenResolution()` + takeover + node-updating logic for determinism (the compare/takeover path already exists).

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



Informational

7. Sleep-based tests flaky 🐞 Bug ☼ Reliability ⭐ New
Description
New regression tests force ordering using real-time sleeps (setTimeout/tokio::time::sleep), which
can make CI runs flaky or slower under load due to timer throttling and scheduling variability.
Code

installing/deps-resolver/test/resolveDependencyTree.test.ts[R15-18]

+  const storeController = createStoreController(async (wantedDependency, options) => {
+    if (wantedDependency.alias === 'shared' && !hasPreferredVersion(options.preferredVersions, 'provider', '1.0.0')) {
+      await new Promise((resolve) => setTimeout(resolve, 50))
+    }
Evidence
The TS test explicitly sleeps 50ms to force one resolution to complete later; the Rust stub resolver
sleeps 25ms for the same purpose, which is inherently timing/scheduler dependent.

installing/deps-resolver/test/resolveDependencyTree.test.ts[15-18]
pacquet/crates/resolving-deps-resolver/src/tests.rs[63-65]

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

### Issue description
The regression tests introduce wall-clock sleeps to bias async completion order. This can be flaky (especially on busy CI) and increases test runtime.

### Issue Context
The tests are trying to simulate a slower resolution for a particular dependency to ensure deterministic ownership selection.

### Fix Focus Areas
- Replace `setTimeout`/sleep with an explicit synchronization primitive:
 - TS: use `p-defer`/a manual promise barrier so the test controls when the delayed request unblocks.
 - Rust: use `tokio::sync::Notify` / `Barrier` (or `tokio::time::pause` + deterministic `advance`) instead of sleeping.
- Keep the “delayed completion” behavior but make it deterministic and independent of wall-clock timing.

- installing/deps-resolver/test/resolveDependencyTree.test.ts[15-18]
- pacquet/crates/resolving-deps-resolver/src/tests.rs[63-65]

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


Grey Divider

Previous review results

Review updated until commit 25bffbb

Results up to commit 90a685c


🐞 Bugs (5) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Action required
1. Stale owner skips tree node 🐞 Bug ☼ Reliability
Description
In resolveDependenciesOfDependency(), the early stale-owner short-circuit returns missing-peers
without calling resolveChildren(), so resolveChildren()’s stale handling
(setDependencyTreeNodeWithCurrentChildren) never runs and the occurrence may never be added to
ctx.dependenciesTree. Downstream code assumes ctx.dependenciesTree has an entry for every non-linked
dep.nodeId (ctx.dependenciesTree.get(dep.nodeId)!), so this can crash or yield an incomplete tree
when ownership flips between claim and postponedResolution execution.
Code

installing/deps-resolver/src/resolveDependencies.ts[R1008-1011]

  postponedResolution: async (postponedResolutionOpts) => {
+      if (!isCurrentChildrenResolution(ctx, resolveDependencyResult.pkgId, resolveDependencyResult.childrenResolutionId)) {
+        return resolveMissingPeersFromCurrentChildrenResolution(ctx, resolveDependencyResult.pkgId, postponedResolutionOpts.parentPkgAliases)
+      }
Evidence
The stale-owner pre-check returns without invoking resolveChildren (so no tree node is created
there), while owner occurrences also skip pendingNodes (which is the other mechanism used to
populate dependenciesTree). resolveDependencyTree later drains pendingNodes into dependenciesTree
and then dereferences dependenciesTree entries for direct deps with a non-null assertion, so missing
nodes can crash.

installing/deps-resolver/src/resolveDependencies.ts[994-1021]
installing/deps-resolver/src/resolveDependencies.ts[1314-1324]
installing/deps-resolver/src/resolveDependencies.ts[721-777]
installing/deps-resolver/src/resolveDependencies.ts[1935-2005]
installing/deps-resolver/src/resolveDependencyTree.ts[316-352]

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

## Issue description
A package occurrence can lose children-ownership between `resolveDependency()` and the later `postponedResolution()` execution (because `resolveDependencies()` resolves deps in parallel, then runs all postponed resolutions). When that happens, `postponedResolution()` currently returns early on the stale check without calling `resolveChildren()`, which means the dependency-tree node for that occurrence is never created/updated.
Downstream, `resolveDependencyTree()` assumes `ctx.dependenciesTree` contains an entry for each non-linked `dep.nodeId` (it uses a non-null assertion), so missing entries can crash or produce an inconsistent tree.
## Issue Context
- `resolveChildren()` *does* handle staleness by calling `setDependencyTreeNodeWithCurrentChildren()`, but that logic is bypassed when `postponedResolution()` returns before calling `resolveChildren()`.
## Fix Focus Areas
- installing/deps-resolver/src/resolveDependencies.ts[1008-1015]
### Suggested fix
On the `postponedResolution` stale pre-check (`!isCurrentChildrenResolution(...)`), call the same fallback used inside `resolveChildren()`:
- `setDependencyTreeNodeWithCurrentChildren(ctx, { parentDepth: options.currentDepth, parentIds: [...options.parentIds, resolveDependencyResult.pkgId], parentPkg: resolveDependencyResult })`
Then return the missing-peers result.
This ensures the occurrence is represented in `ctx.dependenciesTree` even when it becomes a non-owner before its postponed resolution runs.

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



Remediation recommended
2. Cache-hit hides own peers 🐞 Bug ≡ Correctness ⭐ New
Description
In pacquet’s peers-cache hit path, cached missing peers (children + own) are re-emitted under
hoist-missing suppression keyed off first_walk_missing_by_pkg, but this PR now populates that map
from children-only missing_names_by_pkg. When chain_with_self includes the package itself,
cached own-peer misses can be suppressed because the owner’s children-only missing set will not
contain them, skewing hoist inputs and warnings.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[R624-627]

+        for (node_id, missing) in &self.node_missing_peers_of_children {
            let Some(tree_node) = self.tree.dependencies_tree.get(node_id) else { continue };
            missing_names_by_pkg
                .entry(tree_node.resolved_package_id.clone())
Evidence
The PR changes missing_names_by_pkg (and therefore first_walk_missing_by_pkg) to be
children-only, but the cache-hit branch still suppresses the full cached missing set
(missing_peers, which includes own misses). This mismatch makes it possible for own-peer misses to
be suppressed on cache hits when the package itself is part of the suppression chain.

pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[622-636]
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[885-914]
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[987-991]
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[1094-1099]
pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[172-185]
pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs[331-344]
pacquet/crates/resolving-deps-resolver/src/resolve_peers/tests.rs[146-195]

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

### Issue description
On peers-cache hits, `resolve_peers` re-emits cached `missing_peers` (which is the union of children+own missing peers) but applies `missing_issue_suppressed(...)` using `first_walk_missing_by_pkg`, which this PR now fills from `missing_names_by_pkg` derived from **children-only** misses. This can incorrectly suppress a package’s **own** missing-peer diagnostics on cache-hit revisits during hoist passes.

### Issue Context
- `missing_names_by_pkg` is now constructed from `node_missing_peers_of_children` (children-only).
- The peers cache still stores:
 - `missing_peers` = children + own
 - `missing_peers_of_children` = children-only
- The cache-hit re-emission loop currently suppresses *all* entries in `missing_peers`, even those not present in `missing_peers_of_children`.

### Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs[885-910]

### Suggested fix
In the cache-hit branch, only apply `missing_issue_suppressed(...)` to cached misses that originate from children context (i.e. keys present in `missing_of_children` / `cached.missing_peers_of_children`). For cached misses that are *own-peer* misses (present in `missing` but not in `missing_of_children`), always emit them (no suppression) so hoist-time diagnostics match the intended “children-only suppression” model.

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


3. Quadratic node updates 🐞 Bug ➹ Performance
Description
updateChildrenResolutionNodes() linearly scans every recorded node context and is invoked after each
resolveChildren() completion, which can compound into O(N^2) work and noticeably slow resolution on
large graphs.
Code

installing/deps-resolver/src/resolveDependencies.ts[R1227-1237]

+function updateChildrenResolutionNodes (
+  ctx: ResolutionContext,
+  pkgId: PkgResolutionId,
+  currentNodeId: NodeId
+): void {
+  for (const [nodeId, nodeContext] of ctx.nodeResolutionContextByNodeId) {
+    if (nodeId === currentNodeId || nodeContext.pkgId !== pkgId) continue
+    const node = ctx.dependenciesTree.get(nodeId)
+    if (node == null || node.depth === -1) continue
+    node.children = () => buildTree(ctx, pkgId, nodeContext.parentIds, ctx.childrenByParentId[pkgId] ?? [], nodeContext.depth + 1, nodeContext.installable)
+  }
Evidence
The update function performs a full iteration over ctx.nodeResolutionContextByNodeId, and it is
called every time resolveChildren() finishes setting the node context, so the scan cost repeats
across many packages.

installing/deps-resolver/src/resolveDependencies.ts[1227-1237]
installing/deps-resolver/src/resolveDependencies.ts[1399-1406]

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

## Issue description
`updateChildrenResolutionNodes()` iterates the entire `nodeResolutionContextByNodeId` map every time children are resolved, and is called unconditionally at the end of `resolveChildren()`. On large trees this produces repeated full scans and can become a major runtime cost.
### Issue Context
This scan is only needed to retarget *other occurrences* of the same `pkgId` when shared-children ownership changes. Doing a full-map scan for every owner-resolution is unnecessarily expensive.
### Fix Focus Areas
- installing/deps-resolver/src/resolveDependencies.ts[1227-1237]
- installing/deps-resolver/src/resolveDependencies.ts[1399-1406]
### Suggested approach
- Maintain an index from `pkgId -> Set<NodeId>` (or `pkgId -> NodeId[]`) in the resolution context.
- Update it wherever `nodeResolutionContextByNodeId.set(...)` is called.
- Change `updateChildrenResolutionNodes()` to iterate only the NodeIds for the given `pkgId`, rather than scanning the full map.
- Optionally, skip the update entirely when the set has size `<= 1` (no other occurrences to retarget).
- If possible, only invoke the update when ownership actually *changed* (e.g., `claimChildrenResolution` can return a `didTakeover`/`replacedExisting` flag).

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


4. Owner update scans tree 🐞 Bug ➹ Performance
Description
make_non_owner_nodes_lazy() clones the entire node→parentIds map and then iterates the full
dependencies tree on each ownership update, which can become quadratic and adds significant memory
churn for large workspaces.
Code

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[R1415-1428]

+fn make_non_owner_nodes_lazy(ctx: &TreeCtx, pkg_id: &str, owner_node_id: &NodeId) {
+    let parent_ids_by_node = lock_recoverable(&ctx.workspace.node_parent_ids_by_id).clone();
+    let mut tree = lock_recoverable(&ctx.workspace.dependencies_tree);
+    for (node_id, node) in tree.iter_mut() {
+        if node_id == owner_node_id || node.resolved_package_id != pkg_id {
+            continue;
+        }
+        let Some(parent_ids) = parent_ids_by_node.get(node_id) else {
+            continue;
+        };
+        node.children =
+            crate::resolved_tree::TreeChildren::Lazy { parent_ids: Arc::clone(parent_ids) };
+    }
+}
Evidence
The new helper explicitly clones the full node_parent_ids_by_id map and then performs a complete
dependencies_tree iteration; it is invoked on ownership confirmation, so the scan can happen
repeatedly during resolution.

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1415-1428]
pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1357-1359]

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

## Issue description
On each ownership confirmation, `make_non_owner_nodes_lazy()` clones `node_parent_ids_by_id` and then scans the entire `dependencies_tree` to relabel non-owner occurrences as `Lazy`. This is an O(tree_size) operation that can repeat many times, compounding runtime and memory overhead.
### Issue Context
This relazy operation only needs to touch nodes whose `resolved_package_id == pkg_id` (and excluding the owner node). Scanning everything is unnecessary once you have an index.
### Fix Focus Areas
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1357-1359]
- pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs[1415-1428]
### Suggested approach
- Maintain an index `pkg_id -> Vec<NodeId>` (or `HashSet<NodeId>`) for occurrences in `WorkspaceTreeCtx`.
- Update it whenever a node is inserted/updated in `dependencies_tree`.
- Rework `make_non_owner_nodes_lazy()` to iterate only the occurrence NodeIds for that `pkg_id`.
- Avoid cloning the whole `node_parent_ids_by_id` map:
- Look up `parent_ids` for each target node directly from the map (or store `parent_ids` on the tree node itself so the relazy update doesn’t need a second map).
- Keep the locking strategy safe (minimize lock hold time; avoid nested locks if possible).

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


View more (1)
5. Resolution barrier hurts parallelism 🐞 Bug ➹ Performance
Description
resolveDependency() now waits on a global depth barrier before calling
storeController.requestPackage(), which can serialize network/cache work across the whole workspace
and slow installs on large graphs. The barrier is held for the full resolveDependency() duration,
further extending the critical section beyond the ownership-claim logic the PR is trying to order.
Code

installing/deps-resolver/src/resolveDependencies.ts[R1668-1671]

+  const finishPackageResolution = startPackageResolution(ctx, options.currentDepth)
try {
-    const calcSpecifier = options.currentDepth === 0
-    if (!options.update && currentPkg.version && currentPkg.pkgId?.endsWith(`@${currentPkg.version}`) && !calcSpecifier) {
-      wantedDependency.bareSpecifier = replaceVersionInBareSpecifier(wantedDependency.bareSpecifier, currentPkg.version, ctx.namedRegistryPrefixes)
-    }
-    pkgResponse = await ctx.storeController.requestPackage(wantedDependency, {
-      allowBuild: ctx.allowBuild,
-      alwaysTryWorkspacePackages: ctx.linkWorkspacePackagesDepth >= options.currentDepth,
-      currentPkg: currentPkg
-        ? {
-          id: currentPkg.pkgId,
-          name: currentPkg.name,
-          resolution: currentPkg.resolution,
-          version: currentPkg.version,
-          publishedAt: currentPkg.pkgId ? ctx.wantedLockfile.time?.[currentPkg.pkgId] : undefined,
-        }
-        : undefined,
-      expectedPkg: currentPkg,
-      defaultTag: ctx.defaultTag,
-      ignoreScripts: ctx.ignoreScripts,
-      publishedBy: options.publishedBy,
-      publishedByExclude: ctx.publishedByExclude,
-      pickLowestVersion: options.pickLowestVersion,
-      downloadPriority: -options.currentDepth,
-      lockfileDir: ctx.lockfileDir,
-      preferredVersions,
-      preferWorkspacePackages: ctx.preferWorkspacePackages,
-      projectDir: (
-        options.currentDepth > 0 &&
-        !wantedDependency.bareSpecifier.startsWith('file:')
-      )
-        ? ctx.lockfileDir
-        : options.parentPkg.rootDir,
-      skipFetch: ctx.dryRun,
-      trustPolicy: ctx.trustPolicy,
-      trustPolicyExclude: ctx.trustPolicyExclude,
-      trustPolicyIgnoreAfter: ctx.trustPolicyIgnoreAfter,
-      update: options.update,
-      updateChecksums: options.updateChecksums,
-      workspacePackages: ctx.workspacePackages,
-      supportedArchitectures: options.supportedArchitectures,
-      onFetchError: (err: any) => { // eslint-disable-line
-        err.prefix = options.prefix
-        err.pkgsStack = getPkgsInfoFromIds(options.parentIds, ctx.resolvedPkgsById)
-        return err
-      },
-      injectWorkspacePackages: ctx.injectWorkspacePackages,
-      calcSpecifier,
-      pinnedVersion: options.pinnedVersion,
-    })
-  } catch (err: any) { // eslint-disable-line
-    const wantedDependencyDetails = {
-      name: wantedDependency.alias,
-      bareSpecifier: wantedDependency.bareSpecifier,
-      version: wantedDependency.alias ? wantedDependency.bareSpecifier : undefined,
-    }
-    if (wantedDependency.optional && err.code !== 'ERR_PNPM_TRUST_DOWNGRADE') {
-      skippedOptionalDependencyLogger.debug({
-        details: err.toString(),
-        package: wantedDependencyDetails,
-        parents: getPkgsInfoFromIds(options.parentIds, ctx.resolvedPkgsById),
-        prefix: options.prefix,
-        reason: 'resolution_failure',
+    await waitForPackageResolutionTurn(ctx, options.currentDepth)
+
Evidence
The barrier is acquired at the start of resolveDependency(), then blocks on shallower depths before
requestPackage(), and is only released in the function’s finalizer—so it serializes the most
expensive I/O step (package requests) and the rest of resolveDependency’s async work across depths.

installing/deps-resolver/src/resolveDependencies.ts[1668-1728]
installing/deps-resolver/src/resolveDependencies.ts[2038-2040]

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

## Issue description
`resolveDependency()` currently gates *all* package resolution work at depth `d` behind completion of any active work at shallower depths, because it awaits `waitForPackageResolutionTurn()` before `requestPackage()`, and holds the barrier until the end of `resolveDependency()`.
This reduces parallelism (especially packument/tarball fetch overlap) and can slow down real-world installs, even though determinism is already enforced by the `(depth, importer order, parent path)` ownership comparison + takeover machinery.
### Issue Context
The determinism goal is to make shared-children ownership independent of async timing. That can be achieved without globally ordering *fetch/resolve* across depths: you can allow `requestPackage()` to run concurrently, then apply deterministic ownership selection when the `pkgId` is known (and only serialize the parts that must be consistent: ownership claim + shared children/missing-peer publication).
### Fix Focus Areas
- installing/deps-resolver/src/resolveDependencies.ts[1668-1671]
- installing/deps-resolver/src/resolveDependencies.ts[1049-1073]
Concretely, consider one of:
- Move `await waitForPackageResolutionTurn(...)` to just before `claimChildrenResolution(...)` / shared-children publication (after `pkgResponse.body.id` is known), so fetches stay parallel.
- Or remove the barrier entirely and rely on `claimChildrenResolution()` + takeover + node-updating logic for determinism (the compare/takeover path already exists).

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


Results up to commit 90a685c


🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Action required
1. Stale owner skips tree node 🐞 Bug ☼ Reliability
Description
In resolveDependenciesOfDependency(), the early stale-owner short-circuit returns missing-peers
without calling resolveChildren(), so resolveChildren()’s stale handling
(setDependencyTreeNodeWithCurrentChildren) never runs and the occurrence may never be added to
ctx.dependenciesTree. Downstream code assumes ctx.dependenciesTree has an entry for every non-linked
dep.nodeId (ctx.dependenciesTree.get(dep.nodeId)!), so this can crash or yield an incomplete tree
when ownership flips between claim and postponedResolution execution.
Code

installing/deps-resolver/src/resolveDependencies.ts[R1008-1011]

   postponedResolution: async (postponedResolutionOpts) => {
+      if (!isCurrentChildrenResolution(ctx, resolveDependencyResult.pkgId, resolveDependencyResult.childrenResolutionId)) {
+        return resolveMissingPeersFromCurrentChildrenResolution(ctx, resolveDependencyResult.pkgId, postponedResolutionOpts.parentPkgAliases)
+      }
Evidence
The stale-owner pre-check returns without invoking resolveChildren (so no tree node is created
there), while owner occurrences also skip pendingNodes (which is the other mechanism used to
populate dependenciesTree). resolveDependencyTree later drains pendingNodes into dependenciesTree
and then dereferences dependenciesTree entries for direct deps with a non-null assertion, so missing
nodes can crash.

installing/deps-resolver/src/resolveDependencies.ts[994-1021]
installing/deps-resolver/src/resolveDependencies.ts[1314-1324]
installing/deps-resolver/src/resolveDependencies.ts[721-777]
installing/deps-resolver/src/resolveDependencies.ts[1935-2005]
installing/deps-resolver/src/resolveDependencyTree.ts[316-352]

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

## Issue description
A package occurrence can lose children-ownership between `resolveDependency()` and the later `postponedResolution()` execution (because `resolveDependencies()` resolves deps in parallel, then runs all postponed resolutions). When that happens, `postponedResolution()` currently returns early on the stale check without calling `resolveChildren()`, which means the dependency-tree node for that occurrence is never created/updated.
Downstream, `resolveDependencyTree()` assumes `ctx.dependenciesTree` contains an entry for each non-linked `dep.nodeId` (it uses a non-null assertion), so missing entries can crash or produce an inconsistent tree.
## Issue Context
- `resolveChildren()` *does* handle staleness by calling `setDependencyTreeNodeWithCurrentChildren()`, but that logic is bypassed when `postponedResolution()` returns before calling `resolveChildren()`.
## Fix Focus Areas
- installing/deps-resolver/src/resolveDependencies.ts[1008-1015]
### Suggested fix
On the `postponedResolution` stale pre-check (`!isCurrentChildrenResolution(...)`), call the same fallback used inside `resolveChildren()`:
- `setDependencyTreeNodeWithCurrentChildren(ctx, { parentDepth: options.currentDepth, parentIds: [...options.parentIds, resolveDependencyResult.pkgId], parentPkg: resolveDependencyResult })`
Then return the missing-peers result.
This ensures the occurrence is represented in `ctx.dependenciesTree` even when it becomes a non-owner before its postponed resolution runs.

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



Remediation recommended
2. Quadratic node updates 🐞 Bug ➹ Performance ⭐ New
Description
updateChildrenResolutionNodes() linearly scans every recorded node context and is invoked after each
resolveChildren() completion, which can compound into O(N^2) work and noticeably slow resolution on
large graphs.
Code

installing/deps-resolver/src/resolveDependencies.ts[R1227-1237]

+function updateChildrenResolutionNodes (
+  ctx: ResolutionContext,
+  pkgId: PkgResolutionId,
+  currentNodeId: NodeId
+): void {
+  for (const [nodeId, nodeContext] of ctx.nodeResolutionContextByNodeId) {
+    if (nodeId === currentNodeId || nodeContext.pkgId !== pkgId) continue
+    const node = ctx.dependenciesTree.get(nodeId)
+    if (node == null || node.depth === -1) continue
+    node.children = () => buildTree(ctx, pkgId, nodeContext.parentIds, ctx.childrenByParentId[pkgId] ?? [], nodeContext.depth + 1, nodeContext.installable)
+  }
Evidence
The update function performs a full iteration over ctx.nodeResolutionContextByNodeId, and it is
called every time resolveChildren() finishes setting the node context, so the scan cost repeats
across many packages.

installing/deps-resolver/src/resolveDependencies.ts[1227-1237]
installing/deps-resolver/src/resolveDependencies.ts[1399-1406]

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

### Issue description
`updateChildrenResolutionNodes()` iterates the entire `nodeResolutionContextByNodeId` map every time children are resolved, and is called unconditionally at the end of `resolveChildren()`. On large trees this produces repeated full scans and can become a major runtime cost.

### Issue Context
This scan is only needed to retarget *other occurrences* of the same `pkgId` when shared-children ownership changes. Doing a full-map scan for every owner-resolution is unnecessarily expensive.

### Fix Focus Areas
- installing/deps-resolver/src/resolveDependencies.ts[1...

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

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

Copy link
Copy Markdown

PR Summary by Qodo

Deterministic shared-children ownership in deps resolver (TS + pacquet)
🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Deterministically pick the shared-children “owner” by depth, importer order, and parent path.
• Keep non-owner occurrences lazy while reusing owner children and missing-peer context.
• Add TS + Rust regression tests and document the behavioral change.
Diagram
graph TD
  A["resolveDependencyTree.ts"] --> B["ResolutionContext"] --> C["resolveDependencies.ts"] --> D["requestPackage()"]
  C --> E[("childrenByParentId / dependenciesTree")]
  C --> F["Children owner selection"] --> E
  C --> G["Per-depth barrier"] --> C
  H["pacquet: resolve_dependency_tree.rs"] --> I[("children_by_id + owner maps")]
  J["TS + Rust tests"] --> C
  J --> H

  subgraph Legend
    direction LR
    _f["Code module"] ~~~ _db[("Shared state")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Two-phase resolution (discover graph, then choose owners)
  • ➕ Eliminates async-timing sensitivity by design; ownership computed from a complete view.
  • ➕ May reduce need for a per-depth barrier and mid-flight owner changes.
  • ➖ Likely large refactor: requires storing intermediate graph + revisiting nodes.
  • ➖ Higher memory footprint and risk; hard to keep performance parity.
2. Deterministic priority queue scheduler for resolution tasks
  • ➕ Centralizes ordering (depth/importer/path) without per-depth waiter recursion.
  • ➕ Potentially easier to reason about fairness/starvation than ad-hoc barriers.
  • ➖ More invasive concurrency model change; touches many call sites.
  • ➖ Still must handle owner changes and cached children updates correctly.

Recommendation: The PR’s approach (deterministic owner selection plus guarding against stale/non-owner resolutions) is a good incremental fix: it preserves the current architecture while removing async completion timing from correctness. The added safeguards (checking childrenResolutionId before/after child resolution, re-pointing non-owner nodes to current children, and mirroring behavior in pacquet) directly target the reported nondeterminism with manageable blast radius. Alternatives were considered but are significantly more invasive.

Grey Divider

File Changes

Enhancement (1)
resolve_importer.rs Thread importer order into per-importer resolution context +17/-15

Thread importer order into per-importer resolution context

• Extends resolve_importer_with_workspace to accept importer_order and sets it on TreeCtx. Adjusts hoist-missing scope creation to snapshot per peer pass since auto-hoisting can change ownership and tree shape.

pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs


Bug fix (4)
resolveDependencies.ts Deterministic shared-children ownership and depth-ordered resolution +636/-342

Deterministic shared-children ownership and depth-ordered resolution

• Introduces deterministic ownership for a package’s shared children context via a per-pkg childrenResolution record and an owner comparator (depth, importer order, parent path). Adds a per-depth resolution barrier to prioritize shallower work and avoid timing-driven ownership. Guards child resolution and missing-peer propagation with childrenResolutionId checks; updates previously-created nodes to use the current owner’s children via stored node contexts, and exports buildTree for reuse.

installing/deps-resolver/src/resolveDependencies.ts


resolve_dependency_tree.rs Port deterministic shared-children ownership to pacquet resolver +191/-100

Port deterministic shared-children ownership to pacquet resolver

• Adds a ChildrenOwner model and per-package ownership tracking to pick the deterministic child-context owner by (depth, importer order, parent path). Ensures only the current owner realizes children and populates children_by_id; non-owner occurrences remain lazy, and previously-realized non-owner nodes can be flipped back to Lazy when ownership changes to a better occurrence.

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs


resolve_peers.rs Align missing-names aggregation with children-context semantics +29/-20

Align missing-names aggregation with children-context semantics

• Tracks missing peers of children separately from a node’s own missing peers, and uses the children-only view when building missing_names_by_pkg for hoist scoping. Updates comments and cache items to carry missing_peers_of_children for deterministic suppression behavior aligned with upstream.

pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs


resolve_workspace.rs Assign stable importer ordering for deterministic ownership tie-breaks +4/-1

Assign stable importer ordering for deterministic ownership tie-breaks

• Enumerates workspace importers and passes importer_order through resolve_importer_with_workspace. Ensures tie-breaking is stable across runs and matches the documented (depth, importer order, parent path) rule.

pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs


Refactor (1)
resolveDependencyTree.ts Initialize new resolver coordination state (barrier + ownership maps) +10/-47

Initialize new resolver coordination state (barrier + ownership maps)

• Extends the ResolutionContext initialization with packageResolutionBarrier, childrenResolutionByPkgId/id counters, importer resolution order, and nodeResolutionContextByNodeId. Removes the local buildTree implementation in favor of the exported version from resolveDependencies.

installing/deps-resolver/src/resolveDependencyTree.ts


Tests (3)
resolveDependencyTree.test.ts Regression test for deterministic shared-children context selection +208/-0

Regression test for deterministic shared-children context selection

• Adds a Jest test that simulates timing skew in storeController.requestPackage to ensure shared package children resolve from the shallowest deterministic context. Verifies the chosen provider version is stable and that multiple shared occurrences see identical children.

installing/deps-resolver/test/resolveDependencyTree.test.ts


tests.rs Test: missing_names_by_pkg records only children-context missing peers +52/-0

Test: missing_names_by_pkg records only children-context missing peers

• Adds a unit test ensuring missing_names_by_pkg includes missing peers from descendants but excludes the node’s own missing peers. Updates test walker construction to include the new node_missing_peers_of_children field.

pacquet/crates/resolving-deps-resolver/src/resolve_peers/tests.rs


tests.rs Async regression test for shallower ownership takeover despite timing skew +114/-1

Async regression test for shallower ownership takeover despite timing skew

• Adds a DelayedAliasResolver to simulate delayed resolution for a chosen alias. Introduces a tokio test asserting that a shallower revisit can deterministically take over the shared children context and update children_by_id accordingly.

pacquet/crates/resolving-deps-resolver/src/tests.rs


Documentation (1)
deterministic-shared-children-resolution.md Document deterministic shared-children resolution behavior +6/-0

Document deterministic shared-children resolution behavior

• Adds a patch changeset for pnpm and @pnpm/installing.deps-resolver. Notes the deterministic owner tie-break (depth → importer order → parent path) and links the fixed issue.

.changeset/deterministic-shared-children-resolution.md


Grey Divider

Qodo Logo

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.04      7.9±0.31ms   551.6 KB/sec    1.00      7.6±0.04ms   571.0 KB/sec

@codecov-commenter

codecov-commenter commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.27891% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.06%. Comparing base (43b5bf7) to head (25bffbb).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...lving-deps-resolver/src/resolve_dependency_tree.rs 96.82% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12362      +/-   ##
==========================================
+ Coverage   88.05%   88.06%   +0.01%     
==========================================
  Files         294      294              
  Lines       37292    37375      +83     
==========================================
+ Hits        32837    32916      +79     
- Misses       4455     4459       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs`:
- Around line 619-642: The code in record_first_walk_missing persists full
missing_by_pkg (which includes a package’s own missing peers plus descendant
misses); change it to store only children-context misses by removing the
package’s own missing-peers before inserting. Concretely, in
record_first_walk_missing use missing_by_pkg.get(pkg_id) minus the package’s own
missing set (from the ResolvePeersResult / node_missing_peers data structure for
that pkg_id) when populating first_walk_missing_by_pkg (both in the owner branch
and the non-owner/or_insert branch), so only descendant/children-context names
are recorded for each pkg_id.
🪄 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: a2e91107-5b5d-42d3-aac9-f3860fc54605

📥 Commits

Reviewing files that changed from the base of the PR and between 52148e6 and 5873cf5.

📒 Files selected for processing (9)
  • .changeset/deterministic-shared-children-resolution.md
  • installing/deps-resolver/src/resolveDependencies.ts
  • installing/deps-resolver/src/resolveDependencyTree.ts
  • installing/deps-resolver/test/resolveDependencyTree.test.ts
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/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: Code Coverage
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Dylint
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Doc
🧰 Additional context used
📓 Path-based instructions (3)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plain String or &str in Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too via TryFrom<String> and/or FromStr and do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallible From<String> constructor
If upstream TypeScript occasionally constructs a branded string without validation, expose from_str_unchecked in Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Derive simple conversions for branded strings using #[derive(derive_more::From)] and #[derive(derive_more::Into)] instead of handwriting impl blocks; use manual impl only when conversion needs custom logic
Model TypeScript string literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like `${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide in CODE_STYLE_GUIDE.md — imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Choose owned vs. borrowed parameters to minimize copies; widen to t...

Files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • installing/deps-resolver/test/resolveDependencyTree.test.ts
  • installing/deps-resolver/src/resolveDependencyTree.ts
  • installing/deps-resolver/src/resolveDependencies.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use instanceof Error for checking if a caught error is an Error object in Jest tests; use util.types.isNativeError() instead to work across realms

Files:

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

Applied to files:

  • .changeset/deterministic-shared-children-resolution.md
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-05-24T21:10:50.292Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:10:50.292Z
Learning: When implementing dependency “revisit”/optional-folding in pacquet, mirror pnpm’s `resolveDependencies.ts` behavior: on revisit, update the `optional` flag only for the directly visited package, and do not automatically change `optional` for transitive descendants. pnpm corrects stale optional flags later via `copyDependencySubGraph` (a BFS in `lockfile/pruner/src/index.ts`/`copyDependencySubGraph`). Until pacquet has an equivalent pruner/copy-subgraph step, be aware that the raw `node.optional` can flow through to the lockfile/virtual store via `dependencies_graph_to_lockfile.rs` → `create_virtual_store.rs` → `installability.rs`, so review changes that affect optional flag propagation for this mismatch.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-05-24T21:10:50.292Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:10:50.292Z
Learning: In pacquet’s Rust resolver layer, the behavior of `ResolvedPackage.optional` “AND-folding”/revisit updates is intentionally aligned with pnpm: when a package is directly revisited, only that package’s `optional` flag is updated; transitive descendants are not re-walked/rescored in this stage. pnpm later corrects any stale optional flags downstream via its pruner/BFS logic (`copyDependencySubGraph` + `nonOptional` reachability stamping in the lockfile pruner). Since pacquet does not yet have that pruner equivalent, review should NOT flag the downstream/stale optional propagation as a bug in pacquet PRs; treat the resolver-layer optional propagation gap as expected parity until the pruning/stamping step is ported.

Applied to files:

  • pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs
  • pacquet/crates/resolving-deps-resolver/src/tests.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs
  • pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.

Applied to files:

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

Applied to files:

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

Applied to files:

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

Applied to files:

  • installing/deps-resolver/test/resolveDependencyTree.test.ts
  • installing/deps-resolver/src/resolveDependencyTree.ts
  • installing/deps-resolver/src/resolveDependencies.ts
🔇 Additional comments (18)
.changeset/deterministic-shared-children-resolution.md (1)

1-7: LGTM!

installing/deps-resolver/src/resolveDependencies.ts (9)

52-52: LGTM!

Also applies to: 170-175, 219-241, 271-271


1049-1080: LGTM!


1082-1148: LGTM!


1150-1159: LGTM!


1008-1022: LGTM!


1235-1278: LGTM!


1314-1324: LGTM!

Also applies to: 1363-1400


1668-1670: LGTM!

Also applies to: 1935-1940, 2038-2040


994-1005: LGTM!

installing/deps-resolver/src/resolveDependencyTree.ts (1)

26-42: LGTM!

Also applies to: 229-236

installing/deps-resolver/test/resolveDependencyTree.test.ts (2)

9-102: LGTM!


104-208: LGTM!

pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs (1)

459-472: LGTM!

Also applies to: 498-500, 530-540, 746-750, 836-843, 1243-1359, 1371-1428, 1641-1717

pacquet/crates/resolving-deps-resolver/src/resolve_importer.rs (1)

235-243: LGTM!

Also applies to: 253-261, 298-302, 321-343

pacquet/crates/resolving-deps-resolver/src/resolve_workspace.rs (1)

209-223: LGTM!

pacquet/crates/resolving-deps-resolver/src/tests.rs (2)

1-1: LGTM!

Also applies to: 45-77


180-257: LGTM!

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit dc411d1

Comment thread installing/deps-resolver/src/resolveDependencies.ts
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 90a685c

@zkochan zkochan marked this pull request as draft June 12, 2026 15:32
@zkochan zkochan marked this pull request as ready for review June 12, 2026 18:53
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 90a685c

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

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 25bffbb

@zkochan zkochan marked this pull request as draft June 12, 2026 19:10
@zkochan zkochan marked this pull request as ready for review June 12, 2026 19:11
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 25bffbb

@zkochan zkochan merged commit 9b35a60 into main Jun 12, 2026
21 of 22 checks passed
@zkochan zkochan deleted the fix/12358 branch June 12, 2026 19:13
zkochan added a commit that referenced this pull request Jun 12, 2026
…rounds (#12357)

## Summary

Two parity changes for pacquet's resolver, continuing #12266. Measured on the monorepo (fresh state, `install --lockfile-only`, back-to-back vs **pnpm 11.6.0**), the real-lockfile document diff drops from **128 to 5 changed lines** (re-measured after rebasing over #12361/#12362: **132 → 11**, where 8 of the 11 are a divergence the pacquet side of #12362 itself introduced — see the analysis on #12266 — and 3 are the known cycle-closing-edge gap).

### 1. Per-level preferred-version fold

pnpm extends the preferred-versions map per resolution level: after a package's direct dependencies settle, their `(name, version)` pairs join the map the *children's* subtree resolutions pick against ([resolveDependencies.ts#L717-L746](https://github.com/pnpm/pnpm/blob/ce9c096e8e/installing/deps-resolver/src/resolveDependencies.ts#L717-L746)). So `signed-varint`'s `varint@~5.0.0` dedupes to the `varint@5.0.0` its parent pinned as a sibling instead of drifting to `5.0.2`. pacquet picked against a static seed only; besides `varint`/`es-abstract`, this turned out to drive the remaining `jest`/`@types/node` duplicate variants too.

- The walk resolves a whole sibling level before any child subtree starts (upstream's postponed-resolution barrier): `resolve_node` splits into `resolve_node_seed` + `walk_node_children`.
- Each level layers its versions onto a new `PreferredVersionsOverlay` (O(1) `Arc`-chained layers in `resolver-base`); the npm picker folds the per-name view in as plain `version` selectors at both registry seams.
- The overlay's per-name view joins the per-wanted dedup cache key; lockfile-reuse subtrees keep the no-overlay path (exact pins).

### 2. Hoist rounds across all importers (deterministic barrier, same logic as pnpm)

pnpm resolves **every importer's initial wave before any peer hoist**, then repeats global hoist rounds (per round: each importer's required-peer loop to a fixpoint, then one optional-peer hoist) until no importer hoists ([resolveDependencies.ts#L335-L445](https://github.com/pnpm/pnpm/blob/ce9c096e8e/installing/deps-resolver/src/resolveDependencies.ts#L335-L445)). pacquet ran each importer's whole hoist loop before the next importer's initial wave, so an early importer's optional-peer pick couldn't see versions a later importer resolves — `@cyclonedx`'s `spdx-expression-parse` hoisted `3.0.1` where pnpm's barrier-visible map picks `4.0.0`. `resolve_importer_with_workspace` is now an `ImporterHoistState` (`init` / `run_required_round` / `hoist_optional_round`) driven by `resolve_workspace` in upstream's exact phase order. Both implementations are deterministic here; the rule is identical.

## Verification

- New regression test `child_resolution_prefers_parent_level_sibling_versions` (fails with the fold disabled) + full `resolving-*`, `package-manager`, `cli` suites: 1,242 tests pass; clippy `--deny warnings`, rustfmt, typos clean.
- Whole-monorepo diff vs fresh pnpm 11.6.0: 128 → 5 changed lines; consecutive pacquet runs byte-identical.
zkochan pushed a commit that referenced this pull request Jun 20, 2026
… resolution order (#12514)

claimChildrenResolution let a non-owner occurrence of a shared package reuse the
owner's missingPeersOfChildren when `existing.owner.depth >= currentDepth ||
existing.missingPeersOfChildren.resolved`. The second clause made reuse depend on
whether the owner's resolution had settled by claim time. Under concurrent
resolution that timing varies run to run, so a deeper consumer inherited the
owner's missing peers on some runs but not others — flipping an optional
transitive peer (e.g. `@babel/core`, reached via styled-jsx) in and out of a
package's resolved peer suffix and churning the lockfile, with intermittent
`pnpm dedupe --check` failures in CI.

Drop the `.resolved` clause: reuse only when the owner is at the same or a deeper
depth, a function of the dependency graph rather than completion order.

The `.resolved` flag was introduced in #5467 (closing #5454) to
avoid a deadlock where, with auto-install-peers in a workspace, a shared package
awaited its own not-yet-settled missingPeersOfChildren promise. Removing the
clause is strictly more conservative — a shallower owner's promise is never
reused, settled or not, so no unsettled promise is ever awaited and the deadlock
cannot return. The #5454 regression test still passes, as do the peer,
dedupe, and cyclic suites. The deterministic owner selection from #12362
made shared-package ownership order-independent but had moved this timing branch
in verbatim without neutralizing it.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resolution: a shared package's children context is decided by an async race (nondeterministic lockfiles)

2 participants