Skip to content

feat: smarter install dedupe behavior#11502

Open
aramissennyeydd wants to merge 3 commits into
pnpm:mainfrom
aramissennyeydd:feat/smart-auto-dedupe
Open

feat: smarter install dedupe behavior#11502
aramissennyeydd wants to merge 3 commits into
pnpm:mainfrom
aramissennyeydd:feat/smart-auto-dedupe

Conversation

@aramissennyeydd

@aramissennyeydd aramissennyeydd commented May 6, 2026

Copy link
Copy Markdown

we've been seeing this non-deterministic update behavior for a long while now - running pnpm dedupe and using auto-install-peers=false have helped but we're still seeing this impact core transitive dependencies that are AFAICT unrelated to the newly installed packages. This happens for both peer dependency resolutions and version pinning so a dependency can either switch from v1.0.1 -> v1.0.2 randomly or v1.0.1(react@18) -> v1.0.1 with no peer dep.

from a comment on #11110 (comment), I would have expected that to be the default behavior.

disclaimer: the vast majority of this was claude generated - the overall idea is I think solid but I'm assuming there are gaps in implementation, please let me know 😁

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features
    • Added opt-in smart-auto-dedupe (disabled by default). When enabled, pnpm performs a conservative post-resolution deduplication pass to rewrite dependency edges to higher compatible in-graph versions and prunes orphaned lockfile snapshots.
  • Tests
    • Added extensive smart auto-dedupe coverage, including rewrite success, non-rewrite scenarios, patch/peer/alias handling, reachability constraints, idempotence, and default-off behavior.
  • Chores
    • Updated the spell dictionary to include “dedupable”.

@aramissennyeydd aramissennyeydd requested a review from zkochan as a code owner May 6, 2026 19:43
Copilot AI review requested due to automatic review settings May 6, 2026 19:43
@aramissennyeydd aramissennyeydd marked this pull request as draft May 6, 2026 19:43
@aramissennyeydd aramissennyeydd marked this pull request as ready for review May 6, 2026 19:43
@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3740f912-ac87-4c16-a6f2-61fdcc11016f

📥 Commits

Reviewing files that changed from the base of the PR and between aba8553 and 0c82d83.

📒 Files selected for processing (2)
  • installing/deps-resolver/src/smartAutoDedupe.ts
  • installing/deps-resolver/test/smartAutoDedupe.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • installing/deps-resolver/src/smartAutoDedupe.ts

📝 Walkthrough

Walkthrough

Adds an opt-in config flag smartAutoDedupe (disabled by default). When enabled, the resolver runs a post-resolution pass that rewrites parent→child edges to higher in-graph versions that satisfy the original semver spec and share compatible peer/patch metadata, then prunes orphaned snapshots.

Changes

Smart Auto-Dedupe

Layer / File(s) Summary
Configuration Schema
config/reader/src/Config.ts, config/reader/src/types.ts, config/reader/src/configFileKey.ts, config/reader/src/index.ts, cspell.json, .changeset/smart-auto-dedupe.md
New optional config smartAutoDedupe added to Config interface, registered in pnpmTypes schema, excluded from global config file keys, defaulted to false, with spell-check entry and changeset documentation.
CLI Installation Options Propagation
installing/commands/src/install.ts, installing/commands/src/installDeps.ts, installing/deps-installer/src/install/extendInstallOptions.ts, installing/deps-installer/src/install/index.ts
smartAutoDedupe threaded through InstallCommandOptionsInstallDepsOptionsStrictInstallOptions with default value, then passed into the resolveDependencies call.
Resolver Wiring and Spec Capture
installing/deps-resolver/src/index.ts, installing/deps-resolver/src/resolveDependencyTree.ts, installing/deps-resolver/src/resolveDependencies.ts
smartAutoDedupe option added to ResolveDependenciesOptions and ResolutionContext; ResolvedPackage extended with optional depSpecs field; captureDepSpecs flag introduced to record dependency ranges when enabled; applySmartAutoDedupe invoked conditionally after graph build.
Smart Dedupe Algorithm
installing/deps-resolver/src/smartAutoDedupe.ts
New module implementing applySmartAutoDedupe(graph) that groups registry tarball resolutions by package name, peer-graph hash, and patch hash, rewrites edges to higher in-graph versions when semver-compatible and metadata-safe, provides helper functions for spec normalization and upgrade candidate selection, and skips non-registry or non-tarball resolution kinds.
Unit Tests
installing/deps-resolver/test/smartAutoDedupe.test.ts
Comprehensive Jest test suite covering rewrite eligibility across semver satisfaction, no-downgrades enforcement, patch-hash and peer-suffix handling, prerelease avoidance, resolution-kind interchangeability, idempotence verification, npm-alias unwrapping, and reachability-signature preservation.
Integration Tests
installing/deps-installer/test/install/dedupe.ts
Three integration tests validating rewrite behavior when higher versions satisfy semver specs, no-rewrite when versions don't satisfy ranges, and default-off behavior.

Sequence Diagram

sequenceDiagram
  participant User
  participant Install as Install<br/>Handler
  participant Resolver as Dependency<br/>Resolver
  participant SmartDedupe as Smart<br/>Auto-Dedupe
  participant Graph as Dependency<br/>Graph

  User->>Install: run install with smartAutoDedupe=true
  Install->>Resolver: resolveDependencies(opts)
  Resolver->>Graph: build dependency graph
  Graph-->>Resolver: graph built
  alt smartAutoDedupe enabled
    Resolver->>SmartDedupe: applySmartAutoDedupe(graph)
    SmartDedupe->>SmartDedupe: group nodes by name, peer-hash, patch-hash
    SmartDedupe->>SmartDedupe: find higher in-graph versions satisfying spec
    SmartDedupe->>Graph: rewrite edges and prune orphaned snapshots
    Graph-->>SmartDedupe: updated graph
    SmartDedupe-->>Resolver: void (in-place)
  end
  Resolver-->>Install: resolved packages
  Install-->>User: install complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • zkochan
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits specification with 'feat:' prefix and accurately describes the main feature being introduced: smarter deduplication behavior for pnpm installs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds an opt-in “smart auto dedupe” pass to restore safer automatic subdependency deduplication during pnpm install (intended to recover behavior lost after the deterministic resolution change in #11110), by backtracking over the resolved dependency graph and rewriting eligible parent→child edges to higher in-graph versions that still satisfy the original requesting spec.

Changes:

  • Introduces a new applySmartAutoDedupe() graph-rewrite pass in @pnpm/installing.deps-resolver, gated behind smartAutoDedupe.
  • Captures per-package dependency spec ranges (depSpecs) during resolution to validate dedupe rewrites against the requesting manifest ranges.
  • Plumbs the new flag through config/CLI → installer → resolver, and adds both unit + integration tests plus a changeset.

Reviewed changes

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

Show a summary per file
File Description
installing/deps-resolver/test/smartAutoDedupe.test.ts Adds unit tests for the new graph rewrite logic and safety constraints (peers, patches, non-registry resolutions, etc.).
installing/deps-resolver/src/smartAutoDedupe.ts Implements the smart auto dedupe pass and resolution eligibility checks.
installing/deps-resolver/src/resolveDependencyTree.ts Threads smartAutoDedupe into resolver context/options.
installing/deps-resolver/src/resolveDependencies.ts Adds optional depSpecs capture on resolved packages when the feature is enabled.
installing/deps-resolver/src/index.ts Invokes applySmartAutoDedupe() when enabled and exposes the option on resolver entrypoint.
installing/deps-installer/test/install/dedupe.ts Adds integration tests asserting lockfile pruning/retention behavior with the feature on/off.
installing/deps-installer/src/install/index.ts Passes smartAutoDedupe into resolveDependencies() from installer options.
installing/deps-installer/src/install/extendInstallOptions.ts Adds the new option to strict install options with a default of false.
installing/commands/src/installDeps.ts Allows the install-deps command options to pick up smartAutoDedupe from config.
installing/commands/src/install.ts Allows the install command options to pick up smartAutoDedupe from config.
config/reader/src/types.ts Registers smart-auto-dedupe as a boolean config key.
config/reader/src/index.ts Adds default value for smart-auto-dedupe.
config/reader/src/configFileKey.ts Categorizes smart-auto-dedupe among excluded YAML config keys.
config/reader/src/Config.ts Adds smartAutoDedupe?: boolean to the config shape.
.changeset/smart-auto-dedupe.md Declares the new opt-in feature and publishes minor bumps across affected packages.

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

Comment thread installing/deps-resolver/src/smartAutoDedupe.ts Outdated
Comment thread installing/deps-resolver/test/smartAutoDedupe.test.ts Outdated
Comment thread .changeset/smart-auto-dedupe.md Outdated
@aramissennyeydd aramissennyeydd force-pushed the feat/smart-auto-dedupe branch from 372d319 to 4f4f415 Compare May 6, 2026 20:25

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
installing/deps-installer/src/install/index.ts (1)

1286-1301: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

smartAutoDedupe can be skipped by frozen/headless install paths.

Line 1300 forwards the flag, but the same file’s frozen/full-resolution gating (e.g., Line 817 and Lines 514-519) does not consider smartAutoDedupe. With an up-to-date lockfile, resolution may be skipped entirely, so smart dedupe never runs.

💡 Proposed fix
-    let needsFullResolution = outdatedLockfileSettings ||
+    let needsFullResolution = outdatedLockfileSettings ||
       opts.fixLockfile ||
       !upToDateLockfileMajorVersion ||
       opts.forceFullResolution ||
-      forceResolutionFromHook
+      forceResolutionFromHook ||
+      opts.smartAutoDedupe
-      !opts.dedupe &&
+      !opts.dedupe &&
+      !opts.smartAutoDedupe &&
🤖 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 @.changeset/smart-auto-dedupe.md:
- Line 9: Update the release note for the smartAutoDedupe feature to explicitly
document the registry/patch guardrails: state that smartAutoDedupe
(smart-auto-dedupe = true) only rewrites edges between dedupable registry
tarball entries (not git/workspace/local tarballs) and will only rewrite when
the target snapshot has the same patch hash (in addition to matching resolved
peer set and satisfying the original spec range), so users understand
patched/git/workspace/local entries are not eligible for rewrites.

In `@installing/deps-resolver/src/smartAutoDedupe.ts`:
- Around line 120-123: The semver.validRange check on parent.depSpecs[alias]
fails for aliased specifiers like "npm:`@scope/pkg`@^1.0.0"; update the logic
around parent.depSpecs[alias] (the code using semver.validRange, spec,
findUpgrade, child.version, childDepPath) to normalize or strip alias wrappers
before calling semver.validRange (e.g., remove "npm:" and any alias syntax) so
aliased ranges are parsed correctly, or alternatively normalize depSpecs earlier
when populated in resolveDependencies.ts so parent.depSpecs contains plain
semver ranges for semver.validRange to succeed.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3124ff01-b42d-4c2e-bacd-dd76f5b5a10f

📥 Commits

Reviewing files that changed from the base of the PR and between 372d319 and 4f4f415.

📒 Files selected for processing (16)
  • .changeset/smart-auto-dedupe.md
  • config/reader/src/Config.ts
  • config/reader/src/configFileKey.ts
  • config/reader/src/index.ts
  • config/reader/src/types.ts
  • cspell.json
  • installing/commands/src/install.ts
  • installing/commands/src/installDeps.ts
  • installing/deps-installer/src/install/extendInstallOptions.ts
  • installing/deps-installer/src/install/index.ts
  • installing/deps-installer/test/install/dedupe.ts
  • installing/deps-resolver/src/index.ts
  • installing/deps-resolver/src/resolveDependencies.ts
  • installing/deps-resolver/src/resolveDependencyTree.ts
  • installing/deps-resolver/src/smartAutoDedupe.ts
  • installing/deps-resolver/test/smartAutoDedupe.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • installing/deps-installer/src/install/extendInstallOptions.ts
  • config/reader/src/Config.ts
  • installing/deps-resolver/src/index.ts

Comment thread .changeset/smart-auto-dedupe.md Outdated
Comment thread installing/deps-resolver/src/smartAutoDedupe.ts Outdated
aramissennyeydd and others added 2 commits June 17, 2026 09:53
Adds an opt-in `smartAutoDedupe` setting that runs a backtracking pass
over the resolved dependency graph: for every parent → child edge, if a
higher version of the same package already exists in the graph (sharing
the same peer-dep-graph hash and patch hash, both registry-resolved)
and that higher version satisfies the original spec range, the edge is
rewritten to point at the higher version. Orphaned snapshots are then
pruned from the lockfile.

The pass is intentionally conservative: only registry-tarball
resolutions participate, peer/patch boundaries are never crossed,
non-semver specs are skipped, and importer-level direct refs are left
untouched. Restores the automatic dedupe behavior removed in pnpm#11110
without reintroducing the non-determinism that motivated that fix.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- exclude git-hosted tarballs from dedupe (gitHosted flag, isGitHostedPkgUrl,
  and non-semver depPaths) so a registry edge is never rewritten to a
  git-hosted depPath
- unwrap npm: alias specifiers before the semver range check so aliased
  edges can be deduped
- skip the optimistic frozen fast-path when smartAutoDedupe is enabled so a
  plain install re-resolves and runs the pass, while leaving explicit
  --frozen-lockfile installs untouched
- clarify the changeset activation (pnpm-workspace.yaml / --smart-auto-dedupe)
  and document the registry/patch/peer/git/workspace/local guardrails
- rename a misleading test; add regression tests for the above

🤖 Generated with [Claude Code](https://claude.com/claude-code) (Opus 4.8)

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 17, 2026 08:37
@zkochan zkochan force-pushed the feat/smart-auto-dedupe branch from 4f4f415 to aba8553 Compare June 17, 2026 08:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Stale optional after rewrites ✓ Resolved 🐞 Bug ≡ Correctness
Description
applySmartAutoDedupe rewrites parent→child edges but never recomputes the target node’s reachability
flags (notably optional), so a node that was previously optional-only can become required while
still being recorded as optional. This optional bit is serialized into the lockfile and can cause
required packages’ fetch/install failures to be incorrectly ignored in later restore/hoisted flows.
Code

installing/deps-resolver/src/smartAutoDedupe.ts[R122-138]

+  for (const parentDepPath of Object.keys(graph) as DepPath[]) {
+    const parent = graph[parentDepPath]
+    if (parent.depSpecs == null) continue
+    for (const [alias, childDepPath] of Object.entries(parent.children)) {
+      const child = graph[childDepPath]
+      if (child == null || !child.name || !child.version) continue
+      if (!isDedupableResolution(child.resolution)) continue
+      const { version, peerDepGraphHash = '', patchHash = '' } = parseDepPath(childDepPath)
+      if (version == null) continue
+      const bucket = candidates.get(child.name)?.get(peerDepGraphHash)?.get(patchHash)
+      if (bucket == null) continue
+      const spec = normalizeRange(parent.depSpecs[alias])
+      if (spec == null) continue
+      const upgrade = findUpgrade(bucket, child.version, spec)
+      if (upgrade != null && upgrade !== childDepPath) {
+        parent.children[alias] = upgrade
+      }
Evidence
The smart dedupe pass mutates only parent.children, while optional is computed earlier during
resolution and later persisted to the lockfile; downstream restore logic treats snapshots with
optional: true as ignorable on fetch failure, so stale optional metadata can suppress errors for
required packages.

installing/deps-resolver/src/smartAutoDedupe.ts[122-139]
installing/deps-resolver/src/resolveDependencies.ts[1946-1998]
installing/deps-resolver/src/updateLockfile.ts[26-47]
installing/deps-resolver/src/updateLockfile.ts[116-118]
installing/deps-restorer/src/lockfileToHoistedDepGraph.ts[249-262]

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

## Issue description
`applySmartAutoDedupe()` mutates `parent.children` to point at an upgraded depPath, but does not update the upgraded node’s `optional` (and other reachability) flags to reflect its new required usage. This can persist incorrect `optional: true` into `pnpm-lock.yaml`, which downstream install/restore code uses to decide whether to ignore failures.
## Issue Context
- A node’s `optional` flag is computed during resolution based on whether it is reached via an optional chain, and is tightened on reuse via `optional = optional && currentIsOptional`.
- After the smart dedupe pass, required edges may now point at a node that was previously only reached via optional paths.
- `updateLockfile()` serializes `pkg.optional` into the snapshot’s `optional` field, and restore/hoist code can ignore fetch errors for snapshots marked optional.
## Fix Focus Areas
- installing/deps-resolver/src/smartAutoDedupe.ts[122-140]
- installing/deps-resolver/src/resolveDependencies.ts[1946-1998]
- installing/deps-resolver/src/updateLockfile.ts[26-47]
- installing/deps-resolver/src/updateLockfile.ts[116-118]
- installing/deps-restorer/src/lockfileToHoistedDepGraph.ts[249-262]
## Suggested fix approach
1. When rewriting an edge, determine whether the edge is optional under pnpm’s semantics (e.g. `edgeOptional = parent.optional || parent.optionalDependencies.has(alias) || parent.peerDependencies[alias]?.optional === true`).
2. If `edgeOptional` is false, ensure the upgraded node and its transitively-required children are marked non-optional:
- Set `graph[upgrade].optional = false`.
- Propagate this requirement down the dependency tree: for each required (non-optional) node, walk its `children` and for each child reached via a non-optional edge, set `child.optional = false` and continue.
- (If you also need prod/dev correctness, consider similarly updating `prod/dev` flags, but the critical correctness bug here is `optional`.)
3. Add/extend a test that constructs a graph where the higher in-graph version is initially optional-only and becomes the rewrite target of a non-optional edge, asserting the resulting lockfile snapshot is not marked `optional` and that failures are not suppressed.

ⓘ 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 0c82d83

Results up to commit N/A


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Stale optional after rewrites ✓ Resolved 🐞 Bug ≡ Correctness
Description
applySmartAutoDedupe rewrites parent→child edges but never recomputes the target node’s reachability
flags (notably optional), so a node that was previously optional-only can become required while
still being recorded as optional. This optional bit is serialized into the lockfile and can cause
required packages’ fetch/install failures to be incorrectly ignored in later restore/hoisted flows.
Code

installing/deps-resolver/src/smartAutoDedupe.ts[R122-138]

+  for (const parentDepPath of Object.keys(graph) as DepPath[]) {
+    const parent = graph[parentDepPath]
+    if (parent.depSpecs == null) continue
+    for (const [alias, childDepPath] of Object.entries(parent.children)) {
+      const child = graph[childDepPath]
+      if (child == null || !child.name || !child.version) continue
+      if (!isDedupableResolution(child.resolution)) continue
+      const { version, peerDepGraphHash = '', patchHash = '' } = parseDepPath(childDepPath)
+      if (version == null) continue
+      const bucket = candidates.get(child.name)?.get(peerDepGraphHash)?.get(patchHash)
+      if (bucket == null) continue
+      const spec = normalizeRange(parent.depSpecs[alias])
+      if (spec == null) continue
+      const upgrade = findUpgrade(bucket, child.version, spec)
+      if (upgrade != null && upgrade !== childDepPath) {
+        parent.children[alias] = upgrade
+      }
Evidence
The smart dedupe pass mutates only parent.children, while optional is computed earlier during
resolution and later persisted to the lockfile; downstream restore logic treats snapshots with
optional: true as ignorable on fetch failure, so stale optional metadata can suppress errors for
required packages.

installing/deps-resolver/src/smartAutoDedupe.ts[122-139]
installing/deps-resolver/src/resolveDependencies.ts[1946-1998]
installing/deps-resolver/src/updateLockfile.ts[26-47]
installing/deps-resolver/src/updateLockfile.ts[116-118]
installing/deps-restorer/src/lockfileToHoistedDepGraph.ts[249-262]

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

## Issue description
`applySmartAutoDedupe()` mutates `parent.children` to point at an upgraded depPath, but does not update the upgraded node’s `optional` (and other reachability) flags to reflect its new required usage. This can persist incorrect `optional: true` into `pnpm-lock.yaml`, which downstream install/restore code uses to decide whether to ignore failures.
## Issue Context
- A node’s `optional` flag is computed during resolution based on whether it is reached via an optional chain, and is tightened on reuse via `optional = optional && currentIsOptional`.
- After the smart dedupe pass, required edges may now point at a node that was previously only reached via optional paths.
- `updateLockfile()` serializes `pkg.optional` into the snapshot’s `optional` field, and restore/hoist code can ignore fetch errors for snapshots marked optional.
## Fix Focus Areas
- installing/deps-resolver/src/smartAutoDedupe.ts[122-140]
- installing/deps-resolver/src/resolveDependencies.ts[1946-1998]
- installing/deps-resolver/src/updateLockfile.ts[26-47]
- installing/deps-resolver/src/updateLockfile.ts[116-118]
- installing/deps-restorer/src/lockfileToHoistedDepGraph.ts[249-262]
## Suggested fix approach
1. When rewriting an edge, determine whether the edge is optional under pnpm’s semantics (e.g. `edgeOptional = parent.optional || parent.optionalDependencies.has(alias) || parent.peerDependencies[alias]?.optional === true`).
2. If `edgeOptional` is false, ensure the upgraded node and its transitively-required children are marked non-optional:
 - Set `graph[upgrade].optional = false`.
 - Propagate this requirement down the dependency tree: for each required (non-optional) node, walk its `children` and for each child reached via a non-optional edge, set `child.optional = false` and continue.
 - (If you also need prod/dev correctness, consider similarly updating `prod/dev` flags, but the critical correctness bug here is `optional`.)
3. Add/extend a test that constructs a graph where the higher in-graph version is initially optional-only and becomes the rewrite target of a non-optional edge, asserting the resulting lockfile snapshot is not marked `optional` and that failures are not suppressed.

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


Qodo Logo

Comment thread installing/deps-resolver/src/smartAutoDedupe.ts
Only merge graph nodes that share the same prod/dev/optional reachability
signature. Previously a required parent→child edge could be rewritten onto
a node that was reached only through optional (or dev-only) edges, leaving
the now-required package marked `optional: true` (or dev-only) in the
lockfile — which suppresses its fetch/install failures and makes it
prunable under --prod. Since the rewrite target now always carries the
exact reachability of the redirected edge, no node's flags become stale.

🤖 Generated with [Claude Code](https://claude.com/claude-code) (Opus 4.8)

Co-Authored-By: Claude <noreply@anthropic.com>

@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 `@config/reader/src/configFileKey.ts`:
- Line 94: Add support for the `smart_auto_dedupe` configuration field to match
pnpm's implementation. Add `smart_auto_dedupe: Option<bool>` field to the
`WorkspaceSettings` struct, include it in the `clear_workspace_only_fields()`
function to ensure it is properly cleared as a workspace-only field, and add the
corresponding `smart_auto_dedupe` field with an appropriate default value to the
`Config` struct to maintain parity with pnpm's behavior for this configuration
option.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 87408f48-043b-45e8-8a92-5ec76268eaa5

📥 Commits

Reviewing files that changed from the base of the PR and between 4f4f415 and aba8553.

📒 Files selected for processing (5)
  • .changeset/smart-auto-dedupe.md
  • config/reader/src/Config.ts
  • config/reader/src/configFileKey.ts
  • config/reader/src/index.ts
  • config/reader/src/types.ts
💤 Files with no reviewable changes (1)
  • config/reader/src/types.ts
✅ Files skipped from review due to trivial changes (1)
  • config/reader/src/Config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • config/reader/src/index.ts

'dedupe-peers',
'dedupe-direct-deps',
'dedupe-injected-deps',
'smart-auto-dedupe',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read-only parity check: confirm pacquet mirrors the newly excluded key.
set -euo pipefail

echo "Searching pnpm side:"
rg -n --type=ts -C2 "'smart-auto-dedupe'" config/reader/src/configFileKey.ts config/reader/src/types.ts config/reader/src/index.ts

echo
echo "Searching pacquet mirror paths for the same key/filter:"
rg -n -C3 "smart-auto-dedupe|excludedPnpmKeys|clear_workspace_only_fields|isConfigFileKey" pacquet

echo
echo "Inspecting the parity function block (if present):"
fd workspace_yaml.rs pacquet --exec sed -n '540,660p' {}

Repository: pnpm/pnpm

Length of output: 16360


🏁 Script executed:

# Check if smart_auto_dedupe field exists in WorkspaceSettings and whether it's being cleared
echo "=== Searching for smart_auto_dedupe in pacquet config ==="
rg -n "smart_auto_dedupe|smart-auto-dedupe" pacquet/crates/config/src/

echo
echo "=== Full clear_workspace_only_fields function ==="
sed -n '595,650p' pacquet/crates/config/src/workspace_yaml.rs

echo
echo "=== WorkspaceSettings struct definition to see all fields ==="
rg -B5 -A80 "^pub struct WorkspaceSettings" pacquet/crates/config/src/workspace_yaml.rs | head -120

Repository: pnpm/pnpm

Length of output: 7181


🏁 Script executed:

# Check if smart_auto_dedupe field is defined anywhere in pacquet WorkspaceSettings
echo "=== Full WorkspaceSettings struct to find smart_auto_dedupe ==="
rg -A200 "^pub struct WorkspaceSettings" pacquet/crates/config/src/workspace_yaml.rs | grep -i "smart\|dedupe"

echo
echo "=== Check if Config struct has smart_auto_dedupe ==="
rg -n "smart_auto_dedupe|smart-auto-dedupe" pacquet/crates/config/src/lib.rs

echo
echo "=== Check all dedupe fields in Config struct ==="
rg -B2 -A2 "dedupe.*:" pacquet/crates/config/src/lib.rs | head -50

Repository: pnpm/pnpm

Length of output: 2195


Pacquet is missing the smart-auto-dedupe field entirely—add it to maintain parity.

The new config key 'smart-auto-dedupe' added to pnpm's excludedPnpmKeys (forbidding it in global config.yaml) is not defined in pacquet's WorkspaceSettings or Config structs at all. Users setting smart-auto-dedupe in pnpm-workspace.yaml will have it silently ignored in pacquet, and if the field is later added without the clear_workspace_only_fields() filter, pacquet would allow it in global config while pnpm forbids it. Add smart_auto_dedupe: Option<bool> to WorkspaceSettings, include it in clear_workspace_only_fields(), and add the corresponding field and default to Config to match pnpm's behavior.

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

In `@config/reader/src/configFileKey.ts` at line 94, Add support for the
`smart_auto_dedupe` configuration field to match pnpm's implementation. Add
`smart_auto_dedupe: Option<bool>` field to the `WorkspaceSettings` struct,
include it in the `clear_workspace_only_fields()` function to ensure it is
properly cleared as a workspace-only field, and add the corresponding
`smart_auto_dedupe` field with an appropriate default value to the `Config`
struct to maintain parity with pnpm's behavior for this configuration option.

Source: Coding guidelines

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

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 0c82d83

zkochan added a commit that referenced this pull request Jun 18, 2026
…#12472)

When a package is reused from the lockfile, its child edges are taken
verbatim and bypass the preferred-versions walk, so a transitive
dependency can stay pinned to an older version even after a direct
dependency resolved to a higher version that satisfies the same range —
leaving the lockfile non-convergent (an incremental install keeps a
duplicate that a fresh install would not).

The resolver now refreshes such a stale pin to the higher
direct-dependency version during resolution, via `preferredVersion`
(singular), which overrides the EXISTING_VERSION_SELECTOR_WEIGHT stability
bias. The older version is never resolved or fetched, and the incremental
result converges to what a fresh install produces. The pick is anchored to
direct dependencies (which resolve first), so it restores the automatic
dedupe removed in #11110 without reintroducing its
non-determinism, and unlike the post-pass in #11502 it does not
over-fetch.

pacquet is ported in the same change. Its full-subtree lockfile reuse is
coarser than pnpm's per-edge reuse, so it records per importer which direct
deps changed and their resolved versions, declines full-subtree reuse for a
parent that depends on a changed direct dep, and forces the higher version
in the child walk. Range satisfaction uses plain semver (not
prerelease-inclusive), matching pnpm's semver.satisfies(.., true).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants