The report
--format json writes exactly one line to stdout: the canonical JSON of the report, then a
newline. Canonical means RFC 8785 canonical JSON:
keys sorted, one byte sequence per possible document, so the same input through the same
engine binary always produces the same bytes. The payload facts agree across platforms; the
envelope’s own digests differ by build, because they name the exact binary that ran. Duplicate keys are rejected everywhere on input, and
the contract’s numbers are integers, never floats.
The outer envelope has three members: its schema, the payload, and payload_digest, a hash
of the payload’s canonical bytes. The payload carries its own schema, compatibility
(the wire’s own version, frozen at 1), and an engine block whose engine_digest names the
binary that produced it. Every digest in the system is domain-separated, meaning the hash
input starts with a label naming its purpose, so a digest computed for one context cannot be
replayed as a digest for another.
Inside the payload: which trees were compared and how; the result block with status,
complete, and exit_code; the PR-facing feedback projection; the summary counts; a documents array with one row per
discovered document, its classification, and whether its content was available; the
findings array; and the errors array of analysis errors the run kept.
The evaluation records candidate_ref and target_ref separately. The candidate ref is the
source branch used for same-repository URL resolution; the target ref is the protected branch
to which branch-scoped controls were matched. Either may be null on a local, self-asserted run,
and the direct CLI currently leaves the target null. Both values enter the candidate-identity
preimage. They describe the exact inputs the engine evaluated; their presence does not prove who
selected or authenticated them.
An exact same-repository forge URL carries an optional commit_oid in its target intent and
finding-key projection. The engine resolves it only from that commit’s objects already available in
the declared Git roots. If the commit or any required object is unavailable, an
unsupported-version resolution retains known-commit with the exact ID and contained path; it
does not turn unavailable evidence into a missing target, and the decoded URL is retained for the
provider-evidence layer. Named refs and ambiguous ref/path splits retain the narrower known-path
or unknown-path forms instead of being guessed into a commit.
The sealed commit-pair path, including every provider lane, still reports
explicit-commit-pair and explicit-replay. Provider event and publication facts remain outside
the engine report.
A repository path anywhere in the payload has exactly one spelling. Valid UTF-8 bytes
travel as a plain string; anything else travels as {"bytes_hex": "..."} naming the raw
bytes as lowercase hex. A writer never uses the object form for bytes that decode as
text, so every derived digest stays whole.
external_destination holds the URL retained by an external or unavailable historical resolution.
Most such URLs delegate evidence to another layer: an external URL or a same-repository exact
commit whose required local objects are unavailable. The occurrence keeps the URL after the
format’s own decoding so that
https://example.com/x?a=1&b=2 is recorded as the address a fetcher would request rather
than as the bytes the source spells. A locally resolved or disproved historical target has no
delegated destination. An ordinary external URL raises no finding and the summary counts it under
external_out_of_scope, because the engine never fetched it and so decided nothing. A Sphinx
label resolved through candidate-bound inventory evidence carries both the selected destination
and reason: "intersphinx-inventory", but counts as resolved and is not delegated again. A
generated route proved by complete site-build evidence similarly carries reason: "site-build",
counts as resolved, and has no external_destination because it names no external target.
The external plan derives the introduced and removed destinations from
a written report, and Amiss and link checkers shows the pipe that hands
them to the tool that does fetch.
Every finding carries its kind, its location with byte offsets, its attribution, the
policy steps that set its final disposition, and the digests of the facts underneath it.
The key_input that produced the finding’s identity is included too, so an external
system can recompute any finding’s identity from the report alone.
feedback is the smaller review surface derived by the engine from those exact findings.
Related introduced problems become one fix per target, changed targets under unchanged
prose become one check, and pre-existing failures and warnings become one existing per
target after them, so the backlog is listed, not only counted; existing_count stays the
number of those grouped subjects. Each item retains its affected-location count and
contributing finding kinds. A Fix may carry one candidate-side text-path annotation;
Checks and Existing items never do. The report retains every item. An incomplete
comparison instead emits exactly {"status":"unavailable"}, so scan failure cannot look
like zero feedback.
The envelope, down to its top-level keys:
{
"schema": "amiss/scanner-report-envelope",
"payload": {
"schema": "amiss/scanner-report-payload",
"compatibility": "1",
"engine": { "engine_digest": "sha256:..." },
"evaluation": {},
"controls": {},
"result": { "status": "fail", "complete": true, "exit_code": 1 },
"feedback": { "status": "available", "items": [], "existing_count": 0 },
"summary": {},
"documents": [],
"observations": [],
"findings": [],
"errors": []
},
"payload_digest": "sha256:..."
}
And one finding row from a real failing run, abridged to its skeleton:
{
"kind": "explicit-target-missing",
"description": "a reference names a repository path, a line range inside one, or a heading anchor no known renderer publishes; restore the target or correct the link",
"attribution": "introduced",
"effective_disposition": "fail",
"location": {
"path": "docs/src/introduction.md",
"side": "candidate",
"span": { "start_line": 49, "start_column": 1, "end_line": 49, "end_column": 38,
"start_byte": 2912, "end_byte": 2949 }
},
"finding_key": "sha256:56a75485757d90b5959298c05f6b0531139b016533db320905ee532e5dd42512"
}
Findings are sorted by finding key, a domain-separated hash of kind plus scope. Every
immutable commit identity is part of that scope, so equal paths in two commits remain different
targets for correlation, findings, debt, and waivers. A reference kind with no repository-path
component retains the empty-string sentinel in normalized_target_intent.path; a repository path
uses the ordinary text-or-byte path form. Every
finding and error row carries a description: the fixed engine-owned sentence for its
kind or code, stating what the row means and what to do about it, so no consumer needs a
second source to act on a report. Beside it sits fix, a machine-applicable rewrite or
null, whose own description is one of a closed set of engine-owned sentences named by
FixKind: when the engine can prove the exact edit, the field names the candidate document,
the byte span to replace, and the replacement text, and a finding whose correct content
is not derivable carries null rather than a guess. Three producers emit one today: the broken
value claim carries its definition respelled to expect the target’s current line,
proven by classifying the rewrite back through the claim grammar (see
Claims), and a lone drifted heading anchor carries its fragment
respelled to the one published identity it names apart from case and separator style,
over bytes the adapter located verbatim, and a lone case-drifted path carries its
written path part respelled to the one tracked path it matches apart from case.
amiss fix applies these spans to the staged working tree in
place, refusing any document whose bytes moved since the evaluation. The sentences live in one place,
FindingKind::meaning, AnalysisErrorCode::meaning, and FixKind::meaning;
the lists in Profiles and findings and Limits and refusals
and the shipped example are checked against that source in CI. The human format prints
the result plus at most ten grouped feedback items, replaces every byte outside printable ASCII with a
\uXXXX escape so a hostile filename cannot inject terminal control codes or a forged CI
command into a log, and states any overflow explicitly. It keeps raw totals and prints
descriptions only for errors; finding kinds and their descriptions stay in JSON. The JSON is never
cut short: a serialized report that would cross the machine-json-bytes ceiling
ends the run incomplete with OUTPUT_LIMIT_EXCEEDED instead of shortening the list, and
the findings count has its own separate ceiling in Limits and refusals.
--format sarif writes exactly one line to stdout: a SARIF 2.1.0 log projected from the
same payload. Every finding row becomes a result under its kind’s rule, fail as error,
warn as warning, and record as note, with the row’s own description as the message,
and a row carrying a fix projects it as a SARIF fix with the byte region and replacement,
which GitHub renders as a suggested edit
and the finding key riding as the stable partialFingerprints entry, so an ingesting
scanner deduplicates across runs by the same identity the report uses. A location renders
when the wire path is printable text, percent-encoded into the artifact URI so a hostile
path cannot break it. Retained analysis errors become tool execution notifications, an
incomplete run reports executionSuccessful false, and a rejected machine invocation
still answers in SARIF with exit class 2. Like the human form, the projection cannot
change facts, ordering, totals, or the exit class; the canonical report stays the only
wire, and consumers that need the full evidence read it there.
--format codequality projects the same payload as GitLab’s Code Quality artifact: a
JSON array with one issue per finding row in report order, the row’s description as
the issue text, its kind as check_name, and fail as major, warn as minor, and
record as info. The finding key rides as the fingerprint, so GitLab’s diff of target
against head recognizes the same finding across runs by the identity the report uses.
GitLab requires a path and a first line on every issue, so a byte-named document answers
with the wire’s hex spelling, a finding on no file answers as (global), and a byte-only
span reads as line one. The format has no
shape for analysis errors or a refusal: a rejected invocation answers with a valid empty
artifact, the exit class still carries the truth, and error detail stays on the JSON and
human lanes. The same projection bounds apply.
A render-only --format junit projects one suite for generic CI test dashboards. Findings
become cases under their stable finding keys: fail becomes a failure, while warn and record
remain passing cases with their exact disposition in system-out. Retained analysis errors become
error cases. A row-free passing report emits one passing report case so the artifact remains
visible. File attributes carry only text that round-trips exactly through XML 1.0, and time is
always zero because the report records no duration. JUnit remains display data—the renderer’s exit
is still the report’s recorded verdict.
A JSON report can be projected later without repeating repository evaluation:
amiss render --report amiss-report.json --format sarif, --format codequality, or
--format junit emits an alternate deterministic view. SARIF and Code Quality are byte-identical
to their originating check projections; JUnit has no direct-check form. Human is available too;
adding --full to that human replay emits every feedback row rather than the two ten-item windows.
The renderer verifies the active envelope and compatibility, the payload digest, and the recorded
result tuple, then exits with that report’s original verdict. JSON is not a render target because
the input file is already that canonical projection.
The report is evidence of engine evaluation, not a self-authenticating provider attestation. A
control row with status: "verified" means that the engine accepted the supplied digest and
repository, target-ref, tree, time, or run relationships required for that control. A caller that
can supply the request can still make those assertions; the enum does not identify or
authenticate the caller. The sealed bootstrap additionally checks the requested identities and
digests against the returned envelope, but republishes the accepted bytes unchanged. The
controls.semantic_evidence array similarly records each accepted envelope’s payload digest and
producer/input identity; it proves engine binding and interpretation, not who acquired the
inventory.
The provider lanes leave separate provider evidence: an App-owned Check Run on GitHub’s test merge, a protected GitLab policy-job result on a merge-train commit, or a dedicated Gitea-family review. GitHub’s Check Run and the Gitea-family review carry the staged summary and report digest; GitLab’s provider-visible evidence is the exact policy job’s outcome. The controller’s saved result binds the plan, execution constraint, and gate identity in every lane. When a report is present, it also binds the report digest. No provider signs or adds fields to the report. Moving the same report bytes away from that gate therefore loses the provider context; there is still no provider attestation inside the current report contract.
Sandbox provenance is separate again. The present writer reports self-asserted assurance,
local-process enforcement, and null verification. The sealed bootstrap requires that honest
projection. Runtime-closure validation, a cleared environment, fixed input, and a watchdog do
not satisfy the report schema’s provider-verified OCI or microVM mechanisms.
The machine contract is the current report schema, its readable example, and the corresponding canonical bytes. The test suite validates emitted bytes with an independent schema validator, checks the canonical example, and checks that the schema identifiers match the writer constants in the documentation contract test.
The wire is versioned by its own compatibility field, not by the engine release: 1
means frozen, additive within the major. A 1 report may gain optional fields as 1
rolls forward, and nothing a 1.0 consumer parsed ever changes meaning or disappears.
The promise is mechanical: the first frozen example is retained permanently beside the
rolling one, a contract test requires every later schema in the major to keep validating
it, and a second test holds the example the last release shipped to the same bar.
Reshaping past that promise mints 2, and that release is a major one. The record of
how the contract earned the freeze is in
A settled wire.