SIGN IN SIGN UP

fix(arborist): validate peerOptional conflicts in no-save mutations (#9605)

## Problem

Arborist currently treats `save=false` as a signal that invalid
`peerOptional` edges from the lockfile can be trusted and skipped. That
is correct for non-mutating lockfile reads such as `npm ci`, but it is
wrong for commands that still mutate the ideal tree and write a new
lockfile while leaving `package.json` unchanged.

For example, `npm update` runs with `save=false`, but it can still
update package versions in `package-lock.json`. If one of those updates
introduces or exposes an invalid optional peer relationship, Arborist
can finish successfully and write a lockfile that a later plain `npm
install` rejects with `ERESOLVE`.

## Fix

This separates two concepts that were previously conflated:

- `#requestedTreeMutation`: whether the command explicitly requested an
add, remove, or update operation.
- `#mutateTree`: whether the ideal-tree build has actually changed the
tree during placement.

Invalid `peerOptional` policy now uses `#requestedTreeMutation` instead
of `#mutateTree`:

- `save=false` + explicit add/rm/update: invalid `peerOptional` edges
are treated as resolver problems.
- `save=false` + no requested mutation: invalid `peerOptional` edges
remain trusted, preserving the existing `npm ci`-style behavior.

The requeue path also uses `#requestedTreeMutation`, so a `save=false`
update can reprocess an already-seen node when a later placement
invalidates one of its `peerOptional` edges.

## Behavior Changed

This fixes bad lockfile output for `save=false` commands that mutate the
dependency graph, including:

- `npm update`
- `npm install <pkg> --no-save`
- other explicit add/rm/update Arborist builds with `save=false`

Those commands should not silently produce an ideal tree or lockfile
containing invalid `peerOptional` edges that a subsequent install
rejects.

## Behavior Preserved

Non-mutating `save=false` builds continue to trust the lockfile. In
those cases, an invalid `peerOptional` edge is still ignored rather than
forcing re-resolution, preserving the behavior needed by `npm ci`-style
lockfile reads.

Fixes #9604.

## Tests

Added regression coverage for:

- `npm update`-style `save=false` mutation rejecting an invalid
`peerOptional` graph instead of writing a bad lockfile.
- `npm install <pkg> --no-save`-style mutation rejecting the same class
of invalid graph.
- `save=false` update requeueing an already-seen node when later
placement invalidates its `peerOptional` edge.
- non-mutating `save=false` builds continuing to ignore invalid
`peerOptional` problem edges.

Validated with:

```sh
git diff --check
npm exec -- tap # from workspaces/arborist
npm run lint --workspace=@npmcli/arborist
```

Co-authored-by: Dale Lakes <6843636+spitfire55@users.noreply.github.com>
D
Dale Lakes committed
2aa1c7cd25399c49f12aab684341cc1743ad1635
Parent: 690bf17
Committed by GitHub <noreply@github.com> on 6/24/2026, 5:28:46 PM