The update pyramid¶
Part of Recommendations and Risk.
--update-strategy picks one of five tiers. Each tier is a strict superset of the one below: it
admits every reason to move that the lower tier admits, and it can reach at least as far.
┌──────────────┐
│ cutting-edge │ + prereleases
┌─┴──────────────┴─┐
│ latest │ + widen the constraint to reach newest
┌─┴──────────────────┴─┐
│ standard │ + any drift, inside the declared range
┌─┴──────────────────────┴─┐
│ deprecation │ + end-of-life packages
┌─┴──────────────────────────┴─┐
│ security │ exploitable CVEs only, minimal diff
└──────────────────────────────┘
Tier |
Moves a package for |
Base reach |
Picks |
Answers |
|---|---|---|---|---|
|
an exploitable CVE |
in-range |
the nearest CVE-clear version |
“What must I patch today?” |
|
+ end-of-life |
in-range |
the nearest version that resolves it |
“What is dying under me?” |
|
+ plain drift |
in-range |
the newest version in range |
“What would I install today?” |
|
+ plain drift |
latest |
the newest version, range rewritten |
“What is current?” |
|
+ plain drift |
latest, prereleases included |
the newest release of any kind |
“What is about to be current?” |
The bottom two tiers minimise the diff; the top three maximise freshness. That split is the point of the pyramid: a security patch that also carries six months of unrelated drift is an unreviewable diff, and an unreviewable diff is how a rushed CVE fix breaks production.
Two axes, not one dial¶
A tier sets two independent things, kept in two separate tables on purpose:
Motive — why a package may move (
ADMITTED_MOTIVES).Reach — how far up the ladder it may go on that motive (
MAX_REACH).
Collapsing them would make “why did this move” and “how far could it move” the same question, and
they are not. An end-of-life package may have to leave its declared range even under the otherwise
minimal-diff deprecation tier.
Escalation: the pyramid refuses to say “stay put”¶
Two motives — exploitable_cve and end_of_life — push reach to latest regardless of tier.
“Remain inside a range that contains no safe version” is not an answer OSS IQ will give. When that
happens, the selection carries an escalation string naming the reason. The same applies when
every reachable version still carries a qualifying CVE: OSS IQ picks the newest anyway and says so
rather than silently recommending nothing.
Freshness tiers get one more escape: if drift alone leaves nothing in reach — an exact pin whose
only newer releases sit outside the declared range — reach widens one rung at a time until
something is found. This is why a pydantic==1.10.13 pin yields 1.10.26 instead of None.
The module-system line¶
For a CommonJS npm project, a release that is ESM-only is a break however semver numbers it:
require() of it fails outright on older Node, and on a Node that supports require(esm) it
returns the module namespace, so a package with only a default export (chalk) still breaks. The
manifest can’t say which shape a package has. So the tier also decides whether a recommendation
may cross from the installed module system to ESM-only:
Tier |
May recommend an ESM-only release to a CommonJS project |
|---|---|
|
no |
|
only when the runtime can |
any tier, CVE or end-of-life motive |
yes, when no clean release is left on the CommonJS line — flagged the same way |
The permission only grows up the pyramid, so a higher tier’s target is still never lower. Drift
alone never crosses on a lower tier. When every newer release is ESM-only, the target stays blank
and the refused release is named in rejected_candidates, rather than being taken anyway. The
record carries both answers: latest_preserving_module_system (where your code keeps working as
it does today) and latest_compatible_major (what the runtime might load), with
module_system_note saying which one the runtime qualifies for.
A real run, two tiers¶
Same project (testdata/pypi/version-constraint),
two tiers, verified output:
Package |
Declared |
Installed |
|
|
|---|---|---|---|---|
|
|
2.31.0 |
2.32.4 — nearest CVE-clear |
2.34.2 — newest |
|
|
4.4.0 |
no target |
4.26.0 — widening |
|
|
1.26.4 |
no target |
2.5.3 — widening, new major |
|
|
2.12.5 |
no target |
2.13.5 |
|
|
1.8.0 |
no target |
1.9.1 |
ossiq plan at security closes with 4 more updates available under --update-strategy standard. — the tier reports what it withheld and names the cheapest tier that would move it. In
status --full the same fact arrives per package, as the ↳ row under Withheld by strategy.
What tier choice risks¶
Tier |
The risk you accept |
How it surfaces |
|---|---|---|
|
Drift and maintenance debt accumulate invisibly; a package with only low-EPSS CVEs is never moved at all. At |
“N more updates available under…” footer; packages sit at |
|
The same minimal-diff risk, now resting on maintenance evidence that may never have been gathered — see Partial data. |
Silent: an unassessed package simply carries no |
|
Packages whose only newer releases sit outside the declared range are recommended but not written — the default tier proposes changes it will not apply. |
Requires constraint widening block in |
|
Authorizes rewriting declared ranges. That range was a decision someone made, possibly encoding an incompatibility OSS IQ cannot see. |
Second confirmation prompt in |
|
Prereleases enter the candidate set. Cooldown still applies, but a prerelease is by definition unproven. |
The picked version itself |
Note
--strategy-override pkg=tier runs one package at a different tier; --override pkg==version
forces an exact version and wins over both. A forced version bypasses the solver, the cooldown, the
widening hold and the acknowledgement prompt — its compatibility is deliberately unverified.