Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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, 3 since the tolerant-reason reshape), 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.

Both report readers use the same result model and verdict check. They require every result member and reject unknown members, negative counts, and disagreements between complete, status, and exit_code. Bootstrap also checks the recorded finding count against the array.

Summary and result counts are limited to 0..=9007199254740991 by their Serde models on both input and output. They remain ordinary u64 fields in Rust; serialization refuses an out-of-range count rather than emitting a number the report reader cannot accept.

Bootstrap decodes the complete report once, including document, observation, finding and error rows, and passes the typed evaluation and controls through verification. After LF framing, malformed fields fail the shape check before canonical bytes, digests and semantic bindings are checked. Validly shaped engine, identity, control and verdict mismatches retain their specific errors. Exact canonical byte equality prevents accepting a differently shaped input with the digest of its typed normalization. Unknown payload-root fields are rejected by both readers.

The public reader also decodes the complete model. It accepts whitespace, member reordering and equivalent JSON string escapes, but not changes to the data’s shape. At ingress, serde-transcode feeds the original JSON directly to the canonical writer; a domain-separated SHA-256 check compares that envelope with the typed envelope before its payload digest is trusted. This prevents a positional array, dropped member or normalized field from borrowing the digest of a different report, without retaining a dynamic payload or staging two output buffers. Malformed fields and unsupported compatibility tags return the report-shape error; validly shaped payload-digest and result-tuple mismatches retain their specific errors.

Engine, action, adapter and summary metadata also use closed models. Unknown members are rejected even with a matching payload digest. Bootstrap validates the summary’s declared fields as well as the engine block; a local action tag cannot conceal a forge-action body.

Document, feedback and analysis-error rows reject unknown members too, including nested source spans and byte-path objects. An unavailable feedback tag cannot hide available items. Writers retain explicit nullable members. Direct Serde decoding treats an omitted ordinary Option<T> field as None, like null; report admission still checks the original envelope against its typed canonical representation, so omitted output members cannot borrow its digest.

Resolution tags select closed bodies: fields belonging to another kind are rejected, and targets, blob evidence and version scopes reuse the producer’s types.

One value inside those bodies is not closed. A reason says why an answer could not be given, and the answer itself sits beside it: a document row’s unsupported_reason next to its status, an unsupported-semantics row’s reason next to its kind. A consumer that meets a spelling it has never seen still knows the document went unscanned, or the reference went unevaluated, so those two are open strings inside the major. A reader keeps a spelling it does not know and writes it back byte for byte, which the payload digest requires of it. The unsupported-semantics row is one shape for every reason, the reason and the target it located when it located one, so a new reason needs no new row; the engine writes a target for query, code-fragment and fragment and for nothing else.

Everything a consumer has to name in order to judge stays closed and still refuses a value it does not know: every kind, disposition, status, schema name, and compatibility itself. The missing reasons stay closed as well, since each one selects which evidence its row carries and each feeds the finding key.

Observation comparisons, occurrences, intents and identity inputs are closed too. An occurrence is its identity input plus its outcome: observation_id_input carries the document, adapter, source construct, source projection digest and extracted intent once, and beside it sit only the observation id, the resolution, the source span, the block kind and any external destination. A comparison’s sides says what each tree held: same carries the one occurrence both trees hold, and each carries a base and a candidate occurrence, either of them null for a side that has none. An each pair whose two occurrences are equal is not the writer’s spelling and is refused. The refs command retains the decoded models for querying and output; it does not reparse individual occurrences or retain unknown extension fields. Candidate and alternative order, byte-path spelling and canonical JSON output remain unchanged.

Finding-key scopes use the enum’s own tag and reject unknown members, including nested target intents and occurrence identities. Finding keys are hashed through the canonical writer, so Rust field declaration order does not affect identity or adoption reproduction.

Projection sources reuse the policy producer’s model through report decoding. Their tags select closed bodies; a record-set tag cannot hide a record-value selection. A tree-path suffix may be absent or null when decoded directly. Writers omit an absent suffix; report admission rejects an explicit null that would change during typed canonicalization.

Projection differences and debt/waiver diagnostics also use their enum’s own tag to select a closed body. Diagnostic trees must be objects. A count’s observed_count and a waiver’s current_fact_digest are emitted even when null. Canonical fact bytes stay unchanged.

Finding facts reject unknown members throughout their evidence and control-state objects. One tagged evidence enum serves both report construction and borrowed adoption preimages; the latter keeps the original resolution borrowed. Site-defect identities use its derived kind label, preserving their kind/route preimages. Embedded document rows and observation comparisons must be objects, not positional arrays.

Finding rows, locations, aggregation, fixes, policy steps and applied debt/waiver metadata reject unknown members. Evaluation carries applied exceptions directly into the report; it retains their provenance without cloning the accepted or authorized fact again. Nullable finding fields remain explicit on output, and canonical report bytes stay unchanged.

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.

Candidate identity is serialized from the same typed evaluation carried by the report, with evaluation_instant and trusted_time excluded and the candidate-identity schema added. The report reader and sealed acceptance reject unknown evaluation and snapshot members, as the published schema requires; they never discard extra identity data before hashing it. Missing clock fields fail the canonical comparison; wrongly typed clock fields fail decoding. A null instant or a string that disagrees with the trusted statement fails control verification.

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": [...]}, the raw bytes as an array of byte values. 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&amp;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. The summary counts every candidate reference twice over: once by what it names, explicit_local, same_repository or external_out_of_scope, and once by what the run concluded, resolved, missing, or unsupported for one it declined to judge, whatever it names. 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. An occurrence whose target misses a heading anchor or a label carries fragment, the text its fragment_digest digests, so a reader can name what is missing without the tree. It stays outside the observation identity and every fact, so no key or fact digest moves with it. 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.

A row is one finding key, and the key’s scope decides how much one row covers. A missing or mistyped target keys on the document, the exact target, and the block holding the reference, so a link one block repeats is one row, and each block that repeats it has a row of its own. The four records that answer for a single reference, target-declared-untracked, unsupported-reference-semantics, unsupported-target-kind, and unsupported-version-scope, key on the document alone. That makes one row per document and kind: aggregation.member_count counts its references, observation_ids names each of them, and the location is the first. Each reference keeps its own resolution and reason on its observation row. An invalid reference keeps a row of its own, since it carries an attribution. aggregation.locations_omitted counts the members whose place the row cannot point to: none where observation_ids names every member, and all but the representative for a claim or control row, which names no observation.

feedback is the smaller review surface derived by the engine from those exact findings. Related introduced problems become one fix per target, and so do problems whose attribution is unknown, since enforce-introduced keeps those blocking. 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 pre-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 evaluation identity follows the same rule: a side that never resolved to a commit and its tree is {"kind":"unavailable"} with the reasons it did not, while a side that did keeps its real identity, so a commit ID never stands in for a tree ID.

The envelope, down to its top-level keys:

{
  "schema": "amiss/scanner-report-envelope",
  "payload": {
    "schema": "amiss/scanner-report-payload",
    "compatibility": "3",
    "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 Analysis errors 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 character outside printable ASCII with a \uXXXX escape, and every such byte of a path that is not UTF-8 with \xhh, so a hostile filename cannot inject terminal control codes or a forged CI command into a log and a raw byte never reads as the character sharing its value, and states any overflow explicitly. It keeps raw totals and prints each sentence once as a note line, for every error code and every finding kind the run reports, while the rows themselves stay in JSON. The places under a row read by document, then line, then column, so a reader can scan down to one, and two places at a single position settle on the finding key, which the report holds distinct. The findings array itself keeps its finding-key order, since that is the identity a consumer matches across runs and a location order would move a row whenever a line moved. 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. The message leads with the row’s own words, the kind, reason and target a human place line prints, and then its description, so two broken links under one rule read apart; a recorded row, which shows no place, carries the sentence alone. Each rule carries that sentence as its help, linked to Profiles and findings. A row carrying a fix projects it as a SARIF fix with the byte region and replacement, and the finding key rides as a partialFingerprints entry, so a consumer that reads either matches results across runs by the identity the report uses. GitHub code scanning reads neither, as CI explains. A row located in the base, such as a reference the change resolved or removed, is left out: the line it names is gone from the candidate, and a code-scanning tool closes an alert only when its row stops appearing. A location’s URI is the path’s own bytes percent-encoded, so a hostile path cannot break it and a name that is not UTF-8 still names its file. The run declares columnKind as unicodeCodePoints, the unit every report column counts. 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, leaving out rows located in the base for the same reason, the row’s own words and then its description as the issue text, as in SARIF, 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 whose message leads with the row’s own words as in SARIF, 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, since 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.

Control provenance retains the concrete execution descriptor, trusted-time statement and semantic producer metadata. Readers reject unknown control and sandbox members, including when the report’s payload digest matches. Sealed acceptance additionally requires verified controls and an explicit evidence list, then checks the retained bodies and their independent bindings. It never reconstructs those bodies from arbitrary JSON maps.

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: 3 since the tolerant-reason reshape, additive within the major. A 3 report may gain optional fields and new reason spellings as 3 rolls forward, and nothing a 3.0 consumer parsed ever changes meaning or disappears. The promise is mechanical: the frozen example that opened the major 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 4, and that release is a major one. Every reader of 3 refuses a 2 report: the render and refs verbs, the external plan, and the controller. The record of how the contract earned the freeze, and of the reshape that minted 2, is in A settled wire; the record of 3 is in A reason may grow.

Last change: , commit: e94587f1