A manifest records the version that was asked for. A lockfile records the one the resolver picked. Installed metadata records the one that is there. Only the last two are facts, and AIROM reports them differently because the difference decides real outcomes.

The two answers

The 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.yaml

Python

poetry.lock, uv.lock, Pipfile.lock
A lockfile holds the full transitive closure, which is deliberate: an AI package pulled in by something else is still AI running in your application, and it is exactly the kind you did not know you had. The AI catalog is what keeps this from becoming a dependency dump — a lockfile of two thousand packages yields the handful that are AI.

What 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:
Reporting zero vulnerabilities against an unresolved range would be worse than useless — the lower bound is usually an old release, so it invents advisories already patched while hiding ones introduced later in the range.
If your scan shows ranges where you expected versions, commit your lockfile. Failing that, scan the deployed artifact — a container or virtualenv carries installed metadata, which is stronger evidence than any lockfile.

The manifest is still recorded

Resolving a version does not erase what was asked for. Both claims stay in the component’s identity evidence, so the document shows the range and the release it resolved to:
This is the same contested-identity mechanism described in evidence: nothing is silently dropped, and a consumer can always see which source won and what it beat.