dependency-resolver
Dependency Resolver
Dependency conflicts are a constraint satisfaction problem. The manifest is a set of version constraints; the resolver tries to pick one version per package that satisfies all of them. When it can't, you have to change a constraint.
Conflict types
| Conflict | Symptom | Root cause |
|---|---|---|
| Diamond | A needs X@2, B needs X@1, you need A and B | Transitive deps disagree on a shared dep |
| Upper-bound collision | A needs X<3, you need X>=3 | A pinned too tightly, hasn't updated |
| Peer dep mismatch | npm: "X@2 requires peer Y@^1 but Y@2 is installed" | Peer deps — caller must provide, and did wrong |
| Cycle | A depends on B depends on A | Bad package design (rare in published pkgs, common in monorepos) |
| Yanked / missing | Version in lockfile no longer on registry | Upstream pulled it (security or mistake) |
Step 1 — See the full tree
Before fixing, understand. Every ecosystem has a tree command:
| Ecosystem | Tree command |
|---|---|
| npm | npm ls <pkg> or npm ls --all |
| pip | pipdeptree or pip show <pkg> for one level |
| Maven | mvn dependency:tree -Dincludes=<group>:<artifact> |
| Gradle | gradle dependencies --configuration runtimeClasspath |
| Cargo | cargo tree -i <pkg> (inverted — who depends on this) |
| Go | `go mod graph |
Find every path to the conflicting package. The conflict is at the merge point.
Step 2 — Diagnose the diamond
your-app
├── framework@5.0 ── requests@^2.28
└── legacy-sdk@1.2 ── requests@>=2.20,<2.26
requests@^2.28 means >=2.28, <3.0. Intersection with >=2.20,<2.26 is empty. No version satisfies both.
Who's wrong? Usually the upper-bounded one (legacy-sdk). Upper bounds on deps are almost always too pessimistic — legacy-sdk probably works fine with requests@2.28 but the author pinned defensively.
Step 3 — Resolution strategies, cheapest first
| Strategy | When | Risk |
|---|---|---|
| Update the stale dep | legacy-sdk@1.3 might have relaxed the bound |
None if it exists — check changelog |
| Override / force resolution | Tell the resolver "use X@2.28 anyway" | legacy-sdk might actually break |
| Fork and patch | Fork legacy-sdk, bump its requests bound, publish privately |
Maintenance burden |
| Vendor / inline | Copy legacy-sdk into your repo, drop its requests dep |
Loses upstream updates |
| Remove one side | Do you actually need legacy-sdk? |
Feature loss |
Ecosystem-specific overrides:
| Ecosystem | Override mechanism |
|---|---|
| npm | overrides in package.json (npm 8+), or resolutions (yarn) |
| pip | No first-class override — use constraints file, or fork |
| Maven | <dependencyManagement> forces a version for all transitives |
| Gradle | resolutionStrategy.force 'group:artifact:version' |
| Cargo | [patch.crates-io] section — replace a dep with a fork |
| Go | replace directive in go.mod |
Worked example — npm diamond
Error:
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR! Found: react@18.2.0
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^17.0.0" from old-chart-lib@2.1.0
Tree:
$ npm ls react
├── react@18.2.0 ← your direct dep
└── old-chart-lib@2.1.0
└── react@^17.0.0 (peer) ← wants 17.x
Diagnosis: old-chart-lib declares react@^17 as a peer dep — it was written for React 17, never updated.
Resolution ladder:
- Check for updates:
npm view old-chart-lib versions→2.1.0is latest. No fix upstream. - Check compat: Does
old-chart-libactually break on React 18? Read its React usage — if it uses no removed APIs, the peer bound is overly tight. - Override:
"overrides": { "old-chart-lib": { "react": "$react" } }$reactmeans "use whatever version I have at the top level." Forcesold-chart-libto accept React 18. - Test. Run the app. Chart renders → the peer bound was indeed too tight. Done.
- If it breaks: Fork
old-chart-lib, fix the actual incompatibility (likely a removed lifecycle method), usenpm:@yourorg/old-chart-lib.
Cycles
In monorepos: pkg-a depends on pkg-b depends on pkg-a. Most resolvers handle this (npm, Cargo). Some don't (pip, without -e). Either way it's a smell.
Break the cycle by extracting the shared piece:
pkg-a ─────► pkg-b pkg-a ──► pkg-shared ◄── pkg-b
▲ │ →
└───────────┘
Whatever pkg-a needs from pkg-b and vice versa — that's pkg-shared.
Do not
- Do not use
--force/--legacy-peer-depsas a permanent fix. It silences the resolver; it doesn't resolve anything. The conflict is still there at runtime. - Do not delete the lockfile to "fix" resolution. You'll get a resolution, likely a different one each time. Lockfiles exist for reproducibility.
- Do not override without testing. The resolver's constraint was there for a reason — maybe a wrong reason, but you don't know until you run the code.
- Do not upper-bound your own deps unless you've confirmed incompatibility.
requests<3in your package is how you become someone else's diamond problem.
Output format
## Conflict
<resolver error, verbatim>
## Tree
<paths to the conflicting package — npm ls / cargo tree / etc output>
## Diagnosis
Conflicting constraints: <X requires Y@A> vs <Z requires Y@B>
Intersection: <empty | range>
Suspected over-constrainer: <which one's bound is probably wrong>
## Resolution
Strategy: <update | override | fork | vendor | remove>
Change:
<diff to manifest/lockfile>
Risk: <what might break — and how you verified it doesn't>
## Verification
<install succeeds, tests pass, feature using the overridden dep works>
More from santosomar/general-secure-coding-agent-skills
configuration-generator
Generates configuration files for services and tools (app config, logging config, linter config, database config) from a brief description of desired behavior, matching the target format's idioms. Use when bootstrapping a new service, when the user asks for a config file for a specific tool, or when translating config intent between formats.
3code-refactoring-assistant
Executes refactorings — extract method, inline, rename, move — in small, behavior-preserving steps with a test between each. Use when the user wants to restructure working code, when cleaning up after a feature lands, or when a smell has been identified and needs fixing.
2code-smell-detector
Identifies code smells — structural patterns that correlate with maintainability problems — and explains why each matters in context. Use when reviewing a PR for structural quality, when the user asks what's wrong with a piece of code that isn't buggy, or when prioritizing refactoring targets.
2code-translation
Translates a single function or small code unit between programming languages, mapping idioms and preserving observable behavior. Use when porting one function, when the user pastes code and asks for it in another language, or as the per-unit primitive for larger migrations.
1static-vulnerability-detector
Scans source code for security vulnerabilities by applying Project CodeGuard rules — injection, unsafe deserialization, XSS, path traversal, broken access control. Use when performing a security audit, when reviewing a PR that touches request handlers or database queries, when the user asks for a vulnerability scan, or when wiring security checks into CI.
1code-search-assistant
Finds code by meaning, structure, or text across large codebases — picks the right search strategy (grep, AST query, call graph walk, semantic search) for the question being asked. Use when the user asks where something is implemented, when navigating unfamiliar code, or when a simple grep isn't enough.
1