# Resolver (/docs/packages/resolver)



The resolver decides which version of every transitive dependency lands in your `node_modules/`. It walks `package.json`, talks to the appropriate registry for each canonical package, and computes a `(name, version)` set that satisfies every declared semver range. This page is the conceptual deep-dive.

## Two resolvers [#two-resolvers]

LPM CLI ships two resolvers in the same binary. Both speak the same npm-compatible semver dialect (`^`, `~`, `||`, `*`, `x`, hyphen ranges, prereleases, `latest`/`beta`/`next` dist-tags) and produce the same lockfile shape. They differ in how they pick versions when multiple ranges target the same canonical package.

| Resolver                     | Default?    | Algorithm                                                                | Notes                                                                                                |
| ---------------------------- | ----------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| **Greedy-fusion**            | **Yes**     | First-match version pick, reuse-on-compatible / allocate-on-incompatible | Faster. Bun-style. The fused dispatcher IS the metadata fetch dispatcher — no separate walker spawn. |
| **PubGrub-with-split-retry** | No (opt-in) | Conflict-driven backtracking                                             | Slower but exhaustive.                                                                               |

Pick the alternative for one invocation:

```bash
LPM_RESOLVER=pubgrub lpm install
```

For deeper debugging of the dispatcher, force the walker-arm orchestration:

```bash
LPM_GREEDY_FUSION=0 lpm install
```

Both are stable. The default is greedy-fusion because it's faster and matches bun/npm/pnpm semantics for the multi-version case below.

## How greedy-fusion picks versions [#how-greedy-fusion-picks-versions]

The resolver maintains a graph keyed by `(canonical, version)`. When it processes an edge:

1. **Reuse-on-compatible.** If the canonical already has a node and that node's version satisfies the new edge's range, the new edge points at the existing node. First-version-wins inside any single satisfying range bucket — same as bun, npm, and pnpm.
2. **Allocate-on-incompatible.** If no existing node satisfies the new range, the resolver picks the best available version for the new range and allocates a new `(canonical, version)` node. Both versions live independently in the resolved tree.

Example: edge A wants `lodash@^4`. The resolver picks `lodash@4.17.21` and allocates one node. Edge B then wants `lodash@^4` — same range bucket, reuses A's node. Edge C wants `lodash@^3` — `4.17.21` doesn't satisfy `^3`, so the resolver allocates a new `lodash@3.10.1` node. The tree now has two `lodash` versions, each shared by everyone whose range pointed at it.

This is the "multi-version per canonical" property. npm has it. pnpm has it. Yarn-classic (with hoisting) approximated it. LPM CLI follows the bun-shaped recipe.

## Candidate policy [#candidate-policy]

The resolver walks available versions newest-first. Semver range satisfaction is the first filter, then LPM CLI applies install policy:

* **Minimum release age** skips too-new matching candidates for direct/root package ranges by default and keeps walking to the newest mature candidate. Strict release-age policy extends the same check to transitive candidates and lockfile replays. Exact checked pins to a too-new version fail.
* **`trust-policy = "no-downgrade"`** skips candidates that reduce npm trusted-publisher or staged-publish evidence compared with an earlier published version. Registry attestation pointers do not count before verification. Ranges can fall back to an older allowed candidate; exact blocked pins fail with the trust-policy reason. After selection, the install gate separately requires new verified provenance when the lockfile contains verified history for that package name.
* **Platform metadata** (`os`, `cpu`, `libc`) is preserved in the lockfile and applied at reify/filter time, not used to hide the newest semver-satisfying version from the lockfile. Incompatible optional packages are skipped on the current host; incompatible required packages fail with a platform error.

This split keeps lockfiles reviewable and portable across hosts while still avoiding known-bad candidates before the graph is committed.

## Resolution failures [#resolution-failures]

When no compatible package version exists, LPM CLI fails before writing the lockfile or touching `node_modules/`. The error names the dependency edge that failed, the package that required it when that context is available, and the newest published version LPM CLI saw.

```text
✗ Could not resolve dependencies
  package missing-leaf@^2.0.0
  required by parent-pkg@1.0.0
  reason no published version satisfies ^2.0.0
  available 1 version, newest 1.0.0
```

`--json` uses the stable top-level `error_code: "resolution_failed"` and puts resolver-specific fields in the `error` object:

```json
{
  "success": false,
  "error_code": "resolution_failed",
  "error": {
    "code": "RESOLUTION_FAILED",
    "message": "failed to resolve missing-leaf@^2.0.0 required by parent-pkg@1.0.0: no published version satisfies ^2.0.0",
    "package": "missing-leaf",
    "requested": "^2.0.0",
    "dependency": "missing-leaf",
    "kind": "no_matching_version",
    "reason": "no published version satisfies ^2.0.0",
    "required_by": "parent-pkg@1.0.0",
    "available_versions": 1,
    "newest_version": "1.0.0"
  }
}
```

`kind` distinguishes the class of resolver failure (`no_matching_version`, `platform_incompatible`, `policy_blocked`, `fetch_failed`, `no_solution`, or `peer_conflict`). Use that field for automation that needs to decide whether to relax a range, change a platform target, approve a policy exception, or retry a registry lookup.

## Streaming dispatch [#streaming-dispatch]

The resolver loop is single-threaded — bun's PackageManager event loop runs on one thread, and parallelism comes from I/O fan-out. Each iteration:

1. Pop a pending edge off the task queue.
2. Resolve the canonical's manifest. Fast path: the shared cache hit — the BFS walker has been prefetching concurrently. Slow path: wait on the per-canonical `Notify` for an in-flight fetch to complete.
3. Pick a version per the rules above.
4. Enqueue the chosen version's deps for the next iteration.

The dispatcher and metadata fetcher are **fused** in the default resolver — the same loop drives both, so the resolver sees in-flight metadata land as it streams in rather than waiting for level-step batch fetches.

## peerDependencies and optionalDependencies [#peerdependencies-and-optionaldependencies]

* **Peer deps are auto-installed by default.** After the main resolution pass, an eager peer-drain step synthesizes installs for every non-optional `peerDependency` not already in the resolved tree. These "ambient" installs surface at `node_modules/<name>/` even though they aren't in `package.json`. `check_unmet_peers` then runs a post-pass to warn on anything that couldn't be satisfied (e.g., peer-range conflicts between consumers).
* Toggle the auto-install with `package.json > lpm > autoInstallPeers` (default `true`) → `~/.lpm/config.toml > auto-install-peers` → built-in default. Set to `false` for npm-classic / pnpm-classic semantics (no synthesis, peer warnings only).
* **Optional peers** (`peerDependenciesMeta.<name>.optional = true`) are **never** auto-installed regardless of the flag — the manifest author opted out of the dependency entirely.
* **Optional deps** are tried; failure to resolve or download is logged but doesn't fail the install.

## Overrides and resolutions [#overrides-and-resolutions]

The resolver consumes three sources of override declarations, with `lpm.overrides` winning on conflict:

| Source                                    | Priority |
| ----------------------------------------- | -------- |
| `package.json > lpm > overrides`          | Highest  |
| `package.json > overrides` (npm-style)    | Middle   |
| `package.json > resolutions` (yarn-style) | Lowest   |

Selectors:

* `"foo"` — every instance of `foo`
* `"foo@<1.0.0"` — instances whose natural version satisfies a range
* `"baz>foo"` / `"baz>foo@1"` — path-selector: instances reached **through** `baz`

Multi-segment paths (`a>b>c`) are rejected at parse time. See [`package.json` overrides](/docs/reference/package-json-lpm#overrides).

## npm aliases [#npm-aliases]

Deps declared as `"my-pkg": "npm:other-pkg@^1.0.0"` (npm-alias edges) are passed through the resolver verbatim. The cache populates an `aliases` map; the resolved tree records each alias edge. The lockfile's `alias-dependencies` and `root-aliases` blocks preserve the metadata for warm installs.

Projects with alias edges, peer pinning, dependency engine constraints, platform metadata, optional-reachability state, catalogs, or registry signatures may write &#x2A;*only `lpm.lock`** — the binary lockfile (`lpm.lockb`) is skipped whenever its v3 wire format cannot represent the TOML metadata. See [`lpm.lockb` format](/docs/reference/lockb-format#whats-not-stored-in-the-binary-lockfile).

## Tunables [#tunables]

| Env var                | Effect                                                          |
| ---------------------- | --------------------------------------------------------------- |
| `LPM_RESOLVER=pubgrub` | Use PubGrub-with-split-retry instead of greedy-fusion           |
| `LPM_GREEDY_FUSION=0`  | Force walker-arm orchestration (debug)                          |
| `LPM_NPM_FANOUT=N`     | Concurrent npm metadata fetches (default `256`)                 |
| `LPM_WALKER=stream`    | Use the continuous-stream walker; default is the level-step BFS |

## When to look at the resolver [#when-to-look-at-the-resolver]

* An install picked a transitive version you didn't expect → use [`lpm graph --why <pkg>`](/docs/packages/graph) to trace the path, then add an `lpm.overrides` entry.
* Resolution is slow → check whether your tree exercises the multi-version path repeatedly, or try `LPM_NPM_FANOUT` higher.
* A new release breaks a peer dep → check `lpm install` output for unmet peer warnings; add the peer to `dependencies` to opt into the resolver's allocation.

## See also [#see-also]

* [`lpm install`](/docs/packages/install) — runs the resolver as part of the install pipeline
* [`lpm resolve`](/docs/packages/resolve) — runs the resolver standalone, prints the tree, no install
* [Lockfile](/docs/packages/lockfile) — what gets persisted after resolution
* [`package.json` overrides](/docs/reference/package-json-lpm#overrides) — selector grammar
* [Environment variables](/docs/reference/env-vars#resolver-advanced) — every tunable
