AIROM assembles one graph per scan, then renders it. Every format is a pure projection of that same graph — a writer never re-derives a fact, never reaches back into the engine, and never adds information the graph does not already carry. One scan, every format, no drift between them.

The formats

VEX and SPDX

vex emits an OpenVEX 0.2.0 document over the CVE overlay. Every statement is affected, and not_affected is unreachable by construction: AIROM does no reachability analysis, so it knows a component is present at a version an advisory lists as vulnerable and nothing more. An all-clear it cannot justify is the one output that would make you stop looking. The upstream fixed version goes in action_statement, never a fixed status — that would assert your product is remediated when it is still running the vulnerable release. spdx emits an SPDX 3.0.1 JSON-LD graph. Take it when your downstream tooling ingests SPDX — and know what you are giving up. It is the lossiest format AIROM emits, and the loss lands squarely on the thing AIROM exists to record: SPDX 3.0.1 has no slot for an occurrence, so no file:line evidence survives. Each package states its occurrence count in a comment so you know what you are missing, but if you need the evidence itself, take CycloneDX or native JSON. What it does carry: models as ai_AIPackage, datasets as dataset_DatasetPackage, suppliers as Organization elements, licenses as hasDeclaredLicense edges, model retirement as validUntilTime, and both overlays through the Security profile. Required fields on the AI and Dataset classes follow SPDX’s NOASSERTION discipline rather than being omitted — a consumer can tell “we looked and do not know” from “this tool never looked”.
json and yaml are the two lossless formats — they carry the whole Inventory, including relationship evidence that CycloneDX and SARIF have no place to put. If you need to keep everything, keep one of those.

The default: table

With no output flag, AIROM writes a table to stdout.
Scan the working directory
A boxed Scan Summary panel (target, counts, a By-Type breakdown, and a Vulnerabilities-by-severity breakdown when the CVE overlay ran) precedes the component table. The table columns are KIND, NAME, VERSION, PROVIDER, CONF, LOCATION, EVIDENCE — plus a VULN column (top severity + count) when a scan surfaces CVEs, followed by a per-CVE detail table. LOCATION is the primary sighting (path:line); EVIDENCE is the total occurrence count (2 occ), not a file list. --wide expands every occurrence below the table. Empty scalars render as -. The scan-root application component is omitted from the table — it is metadata, not a finding — so the component count covers findings only. If nothing matches, the table degrades to a single line:
When files could not be fully processed, a trailing line reports the count and points at --stats or the JSON output for the detail.
-v/--verbose raises log verbosity only. It does not expand the table, add columns, or change any writer’s output. Nothing on the CLI changes the table’s shape.

Multi-output: one scan, several files

-o/--output takes fmt[=path] and is repeatable. Omit =path and the format goes to stdout.
Emit CycloneDX and SARIF in one pass
That is a single scan. bom.json and scan.sarif are two projections of one graph, so they can never disagree about what was found — no second traversal, no re-detection, no skew.
A file plus a table on stdout
At most one output may write to stdout. Two path-less -o values fail with -o: multiple outputs write to stdout; give all but one a path (-o fmt=path).

--format, the single-format alias

--format is a convenience spelling for a single format on stdout. These are equivalent:
Passing both on the command line is an error:
--format cannot express multi-output — it is one format, stdout only. Reach for -o as soon as you want a file or a second format.
Because the two spellings are mutually exclusive only when both are passed explicitly, they can still coexist across configuration layers: the higher layer wins (flags > AIROM_* env > .airom.yaml), and within one layer an output list beats the single-format alias.
Both spellings work as env vars

Writers are pure projections

This is an architectural invariant, not a stylistic claim, and it has consequences worth relying on:

Deterministic

The assembler sorts everything before a writer runs. Two scans of an unchanged tree produce byte-identical output. YAML and JSON diff cleanly in review. (--stats is the exception: timings are volatile by nature.)

No format-only facts

A writer renders nothing the graph lacks. If a field is missing from CycloneDX, it is missing from the graph — not dropped by the writer.

Consistent across formats

The same component, confidence, and evidence appear in every format you ask for, because they are read from one assembled inventory.

Cheap to add formats

A format is a function from inventory to bytes. Adding one cannot change what a scan finds.

Format-specific flags

--stats only shows up in json and yaml — they are the formats that serialize ScanStats. Without it, the stats block is trimmed to filesWalked, filesProcessed and filesFailed; the volatile timing and per-detector numbers are dropped so the output stays reproducible. The table, CycloneDX and SARIF writers never render the stats block either way.
--min-confidence filters at the presentation layer. On general-purpose directories, --min-confidence 0.8 is the practical setting — see /concepts/confidence.

Findings are not failures

Every format exits 0 on a completed scan, however many components it found. Output selection has no bearing on exit status; only --fail-on (and --exit-code) create a gate. See /reference/cli.