The two answers
VERSION column prints a resolved release bare and a declared range
verbatim. No legend is needed: anyone who reads a manifest already reads
~0.2.0 as a range and 4.28.4 as a version.
What AIROM will not do is print 0.2.0 for ~0.2.0. That is the range’s
lower bound, not a version anybody verified is installed — and printing it
makes a guess indistinguishable from a fact.
In the lossless formats the two live in separate fields:
version for a
resolved release, versionConstraint for a declared range. They are mutually
exclusive. CycloneDX has no slot for a range, so its version stays empty and
the constraint rides as the airom:version.constraint property.Where a resolved version comes from
Three sources can pin a component to one release. They outrank a manifest by confidence, so when a tree carries both, the resolved answer wins.
Lockfiles read today:
npm
package-lock.json, npm-shrinkwrap.json, yarn.lock (classic and berry),
pnpm-lock.yamlPython
poetry.lock, uv.lock, Pipfile.lockWhat counts as a range
Whether a bare specifier means “exactly this” is an ecosystem question, and AIROM follows each ecosystem’s own rule rather than guessing from the text.Why this matters more than the display
A component with no resolved version cannot be matched against an advisory database. Version ranges are what OSV evaluates advisories against; there is no answer to “is^4.20.0 vulnerable” that is not really a statement about some
specific release.
So AIROM does not ask, and says so rather than reporting a clean result: