ADR-0063.
ADR-0063-what-a-sarif-finding-about-a-missing-file-anchors-to.md
ADR-0063 — What a SARIF finding about a MISSING file anchors to
Date: 2026-08-29 Status: accepted 2026-09-01 (measured against GitHub code scanning; record below) Deciders: the maintainer Method: measured on the emitted documents, then checked against the consumer's documentation
Context
check --sarif gives every finding a physicalLocation naming the artifact it is about. For a
finding whose whole content is that an artifact is absent, that path is by definition not in the
checkout.
Measured on a realistic red mission: six findings, one of them anchored to a path that does not
resolve — runward/deliverable-not-filled on runward/runbook.md, the deliverable it reports as
missing. The scope is narrow and worth stating precisely, because two of the four families that name
a path can never produce it:
| finding | anchors to | can the path be absent? |
|---|---|---|
runward/deliverable-not-filled |
runward/<deliverable> |
yes, when the state is missing |
runward/rule-corpus |
runward/rules/<file> |
yes, for a rule reported gone |
runward/unratified-decision |
runward/adr/<file> |
no — the file exists, it is merely unratified |
runward/evidence-seal, runward/hook-failed |
the lock, hooks.json |
no — present by construction |
The question surfaced because this project already refused a version of it once. RWD-2026-0041
shipped uris no checkout held, and the net that came out of it asserts every uri resolves in the
checkout. Extending test/sarif-shape.js with the fixtures the mutation campaign showed were
missing — a deliverable gap, a corpus divergence — made that invariant fail. It was narrowed to
admit exactly this case rather than dropped, so the behaviour now sits inside a stated exception
rather than inside a guarantee. That is honest, and it is not the same as being right.
What is known about the consequence, and what is not. GitHub's code-scanning documentation is
silent on a result whose path is absent from the analyzed commit. It states that relative paths
resolve against the repository root, that an absolute URI is converted to a relative one to be
"matched against a file committed to the repository", and — for the neighbouring case — that a path
resolved through a symlink leaves code scanning "unable to display the result". Reports from other
tools' users describe the alert being ingested with the file view answering "Sorry, we couldn't
find this file in the repository"; separately, an empty artifactLocation.uri is reported to
make the upload itself fail. Ingested-but-undisplayable and rejected are very different outcomes,
and this decision turns on which one it is.
That was why this ADR was proposed rather than accepted. The evidence has since been gathered —
see What settled it below — and it settles it in favour of the behaviour the tree already had.
Decision
Keep anchoring an absence finding to the path it is about, and hold the "every uri resolves" guarantee for every other finding.
Measured, not assumed: an alert whose path is absent from the commit is created, open, and structurally identical to one whose path exists. The cost of option 1 turns out to be smaller than the cost of either alternative, so the behaviour the tree already had is ratified rather than changed.
Alternatives discarded
- Anchor to the manifest that requires the artifact. Every uri resolves again, unconditionally. Discarded provisionally: the annotation would land on a file that is correct, and a reviewer following it would arrive where the problem is not. Reconsider if the settling evidence below shows the alert is dropped or rejected rather than merely undisplayable.
- Anchor to the manifest and name the absent path in the message. The loss mitigated in prose. Discarded provisionally: two paths in one finding, and the machine surface would no longer say in a readable field which artifact the finding is about.
- Emit no location at all for an absence finding. Discarded outright, and measured: an empty
locationsarray passed the schema net for months precisely because[].every(...)is true, and a finding a forge cannot place anywhere is a finding nobody reads.
Consequences
- Positive: the finding names the artifact it is about, in a field a tool can read, and the reviewer needs no prose to learn which file is missing.
- Negative, accepted: on a forge the alert may be created without a displayable source, and the "every uri resolves" invariant survives only as a stated exception rather than a guarantee.
- On other boundaries:
test/sarif-shape.jscarries that exception explicitly — a non-resolving uri must belong to a finding about an absent artifact, and every other uri must resolve. If this ADR is decided the other way, that assertion becomes unconditional again.
What would settle it
One upload of a document carrying an absent-path finding to GitHub code scanning, and a look at what the alert becomes: created and displayable, created and undisplayable, or rejected.
What settled it
Run 2026-09-01. A throwaway branch of this repository carried one file, probe-present.txt, and a
SARIF was uploaded for that ref through POST /repos/{owner}/{repo}/code-scanning/sarifs with two
results: one anchored to probe-present.txt, one to probe-absent.txt, which the commit does not
contain. A dedicated tool name kept the probe separable from the repository's real analyses.
The upload was accepted without error or warning — processing_status: complete, error: "",
warning: "" — and the analysis reports results_count: 2. Both alerts were created and both are
open, and the API returns them with the same fields: the absent-path alert carries its path,
start_line, message and classifications exactly as its sibling does. Nothing marks it, nothing
drops it, nothing dismisses it.
So of the three outcomes the question named, it is the first: created. What a reader loses is the rendered snippet in the file view, not the alert. That is a smaller cost than moving the annotation onto a file that is correct, which is what both alternatives require.
The probe left nothing behind: the branch was deleted, the analysis deleted through
DELETE /code-scanning/analyses/{id}, and the repository verified back at its prior alert count —
0 alerts and 0 analyses under the probe's tool name.
Reevaluation trigger (mandatory, dated)
The upload above is performed, or a consumer other than GitHub code scanning is targeted by a runward user and reports how it treats an absent path. Either signal ratifies or reverses this decision; until one appears, the behaviour stands as proposed and the exception in the shape test stands with it.
Trigger set on: 2026-08-29 · Watched via: test/sarif-shape.js, whose stated exception —
a non-resolving uri belongs to a finding about an absent artifact, and every other uri resolves —
now rests on this measurement rather than on an assumption. A consumer other than GitHub code
scanning reporting a different treatment reopens the decision.