ADR-015: Machine-Readable Output Contract¶
Status: Accepted
Context: check emits three formats: text (human), json, and sarif
(both machine-readable). The 1.0 release is a SemVer stability commitment —
once tagged, breaking the shape of a machine-readable format requires a major
version bump, because external consumers (CI pipelines, dashboards, scripts)
parse it.
Through 0.x the JSON output was a bare top-level array of finding objects:
[ { "file": "...", "rule_id": "CL-0001", "severity": "critical", "...": "..." } ]
A bare array is the hardest shape to evolve. It has nowhere to carry run-level
metadata — tool version, the files that failed to parse, or any future summary
— so adding any of those later would move consumers from data[i] to
data["findings"][i], a breaking change. SARIF already carries this metadata
(tool driver version, and invocations[].toolExecutionNotifications for parse
errors), so JSON consumers were strictly worse off: a file that failed to parse
was invisible in JSON, with exit code 2 the only signal.
Decision: Before 1.0, wrap the JSON output in a versioned envelope:
{
"version": "1",
"tool": { "name": "compose-lint", "version": "0.8.0" },
"findings": [ "..." ],
"errors": [ { "file": "...", "message": "..." } ]
}
versionis the envelope schema version (a string). It is bumped only on a breaking change to the shape. Adding a new top-level field (e.g. a futuresummary) is additive and does not bump it — that is the point of the envelope.findings[]keeps the exact per-finding fields from 0.x:file,line,rule_id,severity,service,message,fix,references,suppressed, andsuppression_reason(only when suppressed).errors[]lists files that could not be parsed (the exit-2 cases), mirroring SARIF'stoolExecutionNotifications. ADR-013 "not applicable" skips (Compose v1 / fragments, exit 0) are deliberately excluded — they are not errors.
The JSON envelope and the SARIF 2.1.0 log are the frozen 1.0 contract. Both change only additively post-1.0; any breaking change is a major version bump, recorded by superseding this ADR.
The representation of fixes in SARIF (currently result.properties.fix,
possibly moving to native fixes[]) is out of scope here and tracked with
the auto-fix work in ADR-014.
Consequences:
- One-time breaking change to JSON consumers at the 0.x → 1.0 boundary (bare array → object). This is deliberate: the last chance to make it before the stability freeze.
- JSON and SARIF now report parse failures symmetrically.
- New run-level data (severity summary, timing, config path) can be added later without a major bump.
Alternatives considered:
- Freeze the bare array as-is. Rejected: permanently forecloses run-level metadata in JSON and leaves parse errors unreportable there.
- Add a
summaryblock now. Deferred: no consumer needs it yet, and the envelope makes it a safe additive change whenever one does. Freezing its exact shape (count semantics, severity keys) at 1.0 with no demand is unnecessary surface.