Skip to content
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,92 @@ by walking up rather than by anything the caller spelled, and a relative answer
relative to a directory the caller never named. A dependency the root turns out not to
declare gets no attribution at all, and a warning saying so.

### Forced versions (overrides and resolutions)

An npm-family `package.json` can force a version onto the *resolved* tree, past
whatever the packages in it asked for: npm's `overrides`, Yarn's `resolutions`,
and `pnpm.overrides` — including npm's nested form (`"overrides": { "parent":
{ "child": "…" } }`) and pnpm's scoped keys (`"foo@2>bar"`, which forces a version
onto **bar**). Those maps are the only ones any parser tags as a forced version,
so **`package.json` is the only manifest this section's guarantee covers.**

Other ecosystems have forcing mechanisms of their own. None of them is covered,
and the two reasons for that are not the same — which is the part worth knowing:

- **Never read, so nothing can rewrite them.** Cargo's `[patch]` and `[replace]`,
Composer's `replace` and `conflict`, and `pnpm-workspace.yaml`'s `overrides:`
(only its `catalog:` and `catalogs:` maps are read) are absent from every
parser. They are safe because nothing looks at them, not because anything
protects them.
- **Read, but not yet recognised as forced.** A Gradle version catalog's rich
versions are read for their version string, and `strictly` — Gradle's pinning
form, and the usual shape of a JVM security pin — is recorded as an ordinary
constraint. A plain `dependable fix`, with no flags, will advance one. That is
[issue #147](https://github.com/getkono/dependable/issues/147), and until it is
fixed a Gradle `strictly` pin gets none of the protection described below.

A forced version is usually there for a reason — most often a security pin,
holding a transitive dependency above a vulnerable release — and the tool cannot
tell that from a compatibility pin that has outlived its cause. So for the
entries it does recognise, **`fix` never rewrites one by default**, `--all`
included. It says so instead:

```
$ dependable fix .
note: left lodash = 1.0.0 alone in package.json: 1.9.0 is available, but an override
forces this version onto the resolved tree; pass --overrides to advance it
Nothing to rewrite. 1 available update left alone; see the notes on stderr.
```

That note is the point: a stale pin used to be skipped in silence, so a manifest
whose only outdated entry was an override was reported by `check` and then
answered by `fix` with "Everything is already up to date."

`--overrides` is how you say yes:

```bash
dependable fix . --overrides # advance forced versions within the range the tool reads
dependable fix . --overrides --all # …and beyond it, like --all everywhere else
dependable fix . --overrides --dry-run # see it first; nothing is written
```

"Within the range the tool reads" is the honest boundary, and for npm it is not
always the range npm reads. The requirement is built with Cargo's `VersionReq`,
which takes a bare `1.0.0` as `^1.0.0` — so `"overrides": { "lodash": "1.0.0" }`
is advanced to `1.9.0` by `--overrides` alone, while npm reads that same string as
exactly `1.0.0`, which is what `Ecosystem::bare_version` records for it. The two
readings disagree, and closing that gap is
[issue #118](https://github.com/getkono/dependable/issues/118) — it changes how
every bare version is read, not just a forced one, so it is not settled here. A
forced version spelled as a range (`"^1.0.0"`) or as an explicit pin (`"=1.0.0"`)
carries no such ambiguity: the first advances within the range, the second needs
`--all` like any other pin.

Before you reach for it, check *why* each pin is there. `--overrides` is the
destructive flag in this command, and here is what it cannot work out for you:

- **Which release the pin was chosen for.** A forced version carries no record of
its reason, so whether the pin has outlived it is a question only the author can
answer.
- **Whether the release it advances to is any safer.**
`CheckResult::all_vulnerabilities` is declared but nothing populates it yet, so
advisories are only known for the version currently declared. A pin can be moved
from one vulnerable release to another with no signal at all.
- **Which direction the pin points.** A pnpm override can hold a package *below* a
release — a regression, a breaking change — and advancing that one walks straight
into what it was written to avoid.
- **Whether the upper bound was the point.** For a compatibility pin it usually is,
and raising it is the whole of the damage.

`--dry-run` prints every rewrite it would make without touching a file.

Constraint rules still apply on top: `--overrides` lifts the rule about the kind
of entry, not the rules about what a constraint means. An override written as a
wildcard (`"resolutions": { "lodash": "1.x" }`) is still left alone and still
reported, because pinning an npm wildcard to one release changes what the entry
admits. An override written as a `$name` reference to another entry is never
rewritten either — the version it names lives in the entry it points at.

## Project inventory (`list`)

`dependable list` answers "what lives in this repository" — every manifest it
Expand Down
5 changes: 5 additions & 0 deletions crates/dependable/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ pub struct FixArgs {
/// Update all, including beyond the declared constraint.
#[arg(long)]
pub all: bool,
/// Also rewrite `overrides` / `resolutions` entries — versions this manifest
/// forces onto the resolved tree, often to hold a transitive dependency above
/// a vulnerable release.
#[arg(long)]
pub overrides: bool,
/// Print what would change without writing.
#[arg(long)]
pub dry_run: bool,
Expand Down
Loading
Loading