The algorithm

How it works

Ripedeps is a planner, not a guess. Plain deterministic code turns your package.json and the public registry data into one consistent set of versions. It ranks the candidates, decides for every package and explains each decision.

The pipeline

Seven steps, each with a clear job. The same input and the same registry data at the same moment always give the same answer. There is no AI model, no randomness and no hidden configuration.

The Ripedeps pipelineSeven steps: read the file, build candidate versions, choose the movable packages, collect constraints, solve, update @types entries, and write the output.1Read

dependencies and engines.node

2Candidates

age gate, step level, Node filter

3Movable set

everything, or your focus + neighbours

4Constraints

peers and aligned dependencies

5Rank and solve

fewest violations, then freshest

6@types

follow the runtime package

7Decide, explain

diff, new file, a verdict and a reason each

Every step is plain code. The same input and the same registry data always give the same output.

What is read

The roots are the entries of dependencies, devDependencies and optionalDependencies. Besides them Ripedeps reads engines.node and an optional ripedeps entry. Nothing else in your file influences the plan, and everything else is preserved byte for byte.

A package listed in several sections counts once, and a warning tells you. Ranges that are not plain semver are left alone and reported as skipped:

A pair of curly braces around a magnifying glass over code
Skip reasonExample in package.json
Not a plain semver range"latest", "workspace:*", "github:acme/lib", "npm:other@1", "file:../lib"
Open-ended range"*", ">=1.0.0"
Complex range"^1.0.0 || ^2.0.0", ">=1 <3"
Prerelease"2.0.0-rc.1"
Unknown package or failed lookupa name the registry does not know

Candidate versions

For every root, Ripedeps first fixes the baseline: what your project effectively uses today. Then it builds the list of candidates above it. A version is a candidate when it is:

  • a stable release that is not deprecated,
  • within the step size you allow (see step size),
  • old enough (see the age gate),
  • compatible with your engines.node: the lowest Node version you support must satisfy the candidate's own engines.node.
package.json
"engines": { "node": ">=20" }

With this field, a release that requires >=22 is not offered, and the explanation says so:

explanation
`pkg@2.1.0` requires node >=22, the project's engines.node is >=20

The baseline always stays among the candidates, so a package is never moved backwards.

The age gate

The newest release is the least reviewed one. The age gate keeps versions younger than minAgeDays out of the candidate list, so a hijacked or broken release has time to be noticed and pulled before it reaches your file.

Age gate timelineThree releases of one package. With a 7-day age gate the newest release, 2 days old, is held back and the 11-day-old release is recommended.too young: inside the gateage gate: 7 days1.4.026 days old1.5.011 days old1.6.02 days oldrecommendedoldertoday
Example: three releases and a 7-day age gate. The newest version is not proposed yet.

This is a real answer from the service, with a gate of 7 days and major upgrades allowed:

explanation (real output)
next 15.0.0 -> 16.4.2 (major step, 8 days old, age gate 7); newest version that qualifies; not taken: next@16.4.3 is 0 days old, the age gate is 7 days

The age gate lowers the chance of installing a compromised release. It is not a vulnerability scanner and does not know about CVEs. If you must take a fix today, run Ripedeps with a gate of 0 days, or with a focus on that one package.

Step size

maxBump limits how far a version may move away from the one in your file. The step is measured from the lower bound of your range, and your range prefix (^, ~ or none) is kept.

maxBumpAllowed from ^1.2.3Example result
patchsame major and minor^1.2.9
minor (default)same major^1.9.0
majoranything^3.0.1

Focus

Without a focus, every package can move. With one, only the packages you name move, plus the packages that the newest versions of those packages point to, one level forward. Everything else is fixed. A name without a slash and without @also matches the scope, so trpc means @trpc/*.

request
{ "packageJson": "…", "focus": ["zod"], "minAgeDays": 7 }

The real result for the sample file:

statuses (real output)
outside-focus axios
outside-focus express
outside-focus lodash
upgrade       zod
outside-focus @types/node
outside-focus eslint
outside-focus prettier
outside-focus typescript

The typical use: one vulnerable package

An advisory names a package in your file. Instead of a full upgrade, focus on it, and the diff shrinks to what is needed. If you need the fix right away, add a gate of 0 days for this run. Ripedeps does not know which packages are vulnerable, so naming them is your part.

focus ["axios"], minAgeDays 0 (real output)
@@ -2,7 +2,7 @@   "name": "my-app",
   "version": "1.0.0",
   "dependencies": {
-    "axios": "^1.6.0",+    "axios": "^1.20.0",     "express": "^4.18.2",
     "lodash": "^4.17.20",
     "zod": "^3.22.0"

Constraints between packages

Upgrades that break each other are the usual reason an update goes wrong. Ripedeps looks at two kinds of relation, but only between packages that are in your file:

  • Peer: a candidate asks for a peer range, for example react ^19.
  • Aligned dependency: a candidate depends on another root of yours with a range, so you do not end up with the same package in two versions.

A constraint holds when the chosen version of the other package satisfies the range. When it cannot hold, you get a warning with a concrete hint, such as the major upgrade that would resolve it.

Hands fitting colourful flat tiles together on a pink surface
Peer constraint exampleThe newest next requires react ^19 but the project has react 18. Upgrading next alone would break the peer constraint, so Ripedeps keeps next at the newest version that still accepts react 18.Newest of everythingRipedepsnextnewestreactproject has 18.xpeer asks ^19, react is 18:the upgrade would breaknextstill accepts 18react18.x, unchangedpeer holds,alignment keptIllustrative versions
Alignment beats freshness: a package is held back when its newest version would break a constraint.

With major upgrades allowed, next and react move together, as a real result shows:

diff (real output)
--- a/package.json+++ b/package.json@@ -1,8 +1,8 @@ {
   "name": "web",
   "dependencies": {
-    "next": "^15.0.0",-    "react": "^18.3.1",-    "react-dom": "^18.3.1"+    "next": "^16.4.2",+    "react": "^19.3.1",+    "react-dom": "^19.3.1"   }
 }

Ranking

Several combinations of versions are usually possible. Ripedeps does not pick one by feel: it ranks them in a fixed order of priorities, and the best one wins:

  1. fewest peer violations,
  2. then fewest misaligned dependencies,
  3. then the least lag behind the newest candidate.

Alignment always beats freshness. The lag of a package counts a major gap as 10000, a minor gap as 100 and a patch difference as 1, so the levels cannot mix. When two combinations lag equally, the one with the higher versions wins.

The ranking is internal: it decides which combination is chosen. What you see in the response is the result, thedecision and the explanation for each package.

The solver

Choosing one version per package so that all constraints hold is a small combinatorial problem. Ripedeps solves it exactly, and the best answer is the first one in the ranking.

The packages are split into independent groups that are solved separately, each with a search budget. If the budget runs out, the best solution found so far is returned and meta.exact saysfalse.

A multicoloured puzzle cube floating above an open hand

@types and engines.node

Existing @types/* entries follow the package they describe, matched by name (@types/foo__barbelongs to @foo/bar): the highest types version with the same major and minor as the chosen runtime version, falling back to the same major.

  • @types/node follows your engines.node: it is aligned to the lowest major you support, even across a major, and the explanation says so.
  • An @types entry without a runtime counterpart only gets minor and patch updates.
  • Ripedeps never adds or removes @types entries.
explanation (real output)
@types/node 20.10.0 -> 20.19.43 (minor step, 135 days old, age gate 7); newest version that qualifies; not taken: @types/node follows engines.node (lowest supported major 20)

Decisions

The result of the ranking is one decision per package, the status in the response:

  • upgrade: the range moves up to the chosen version.
  • held: a newer version exists but is not taken: too young, too big a step, a newer Node needed, or it would break a peer or an aligned dependency.
  • current: already the newest version that qualifies.
  • outside-focus: not part of the focus, left as it is.
  • skipped: not processable, and skipReason says why.

The response also gives the best candidate (bestVersion) next to the chosen one. When they differ, the decision names what stood in the way. With a step limit of patch, a range that already admits a newer minor, such as ^1.2.0, moves only by the allowed step, and the explanation says so.

Explanations

Every decision is explained. Every package gets one sentence: what happened and why. For an upgrade it names the step, the age of the chosen version and whether it is the newest that qualifies. For a package that stays behind it names the blocked version and the first reason. The sentences are built from templates, never by a model, so they are stable and testable.

They are part of every API response and every MCP result, so an AI agent can relay them to you as the rationale.

A woman holding a pen to her chin and looking up thoughtfully
explanations (real output, 7-day gate, minor steps)
axios 1.6.0 -> 1.20.0 (minor step, 59 days old, age gate 7); newest version that qualifies
express 4.18.2 -> 4.22.3 (minor step, 40 days old, age gate 7); newest version that qualifies; not taken: express@5.3.0 is a major step from 4.18.2, above the allowed minor level
eslint held at 9.10.0; not taken: eslint@10.13.0 is a major step from 9.10.0, above the allowed minor level
@types/node 20.10.0 -> 20.19.43 (minor step, 135 days old, age gate 7); newest version that qualifies; not taken: @types/node follows engines.node (lowest supported major 20)

The reason kinds in the structured data are age-gate, engines, level-limit, peer,dependency and pulled-in.

The output

A change raises the lower bound of the range and keeps your prefix: ^18.2.0 becomes ^18.3.1,1.2.3 becomes 1.4.0. Ripedeps replaces the text in place, so your indentation and key order stay as they are. The diff has a/package.json and b/package.json headers, so patch -p1applies it.

Optionally, the settings used are written to your file as a ripedeps entry, so later runs start from the same limits. The notes list in the response tells you when that happened.

diff (real output, settings inserted)
--- a/package.json+++ b/package.json@@ -2,18 +2,22 @@   "name": "my-app",
   "version": "1.0.0",
   "dependencies": {
-    "axios": "^1.6.0",-    "express": "^4.18.2",-    "lodash": "^4.17.20",-    "zod": "^3.22.0"+    "axios": "^1.20.0",+    "express": "^4.22.3",+    "lodash": "^4.18.1",+    "zod": "^3.25.76"   },
   "devDependencies": {
-    "@types/node": "^20.10.0",+    "@types/node": "^20.19.43",     "eslint": "^9.10.0",
-    "prettier": "^3.1.0",-    "typescript": "^5.3.0"+    "prettier": "^3.9.10",+    "typescript": "^5.9.3"   },
   "engines": {
     "node": ">=20"
+  },+  "ripedeps": {+    "minAgeDays": 7,+    "maxBump": "minor"   }
 }

Limits and non-goals

  • The only input is package.json. Lockfiles are not read or written.
  • No vulnerability database is consulted. Ripedeps reduces exposure to brand-new releases; it does not tell you what is vulnerable yet.
  • Only stable versions are proposed; prerelease versions are ignored for security reasons.
  • Ranges are never tightened or loosened. The prefix you chose stays.
  • Request limits: file up to 256 KB, up to 300 dependencies, up to 50 focus entries.
  • Package data comes from the public npm registry JSON API and is cached for a short time.

Ready to try it? Open the tool, or see the API, the MCP serverand the command line.