Report reference
precommiteu scan produces up to four report artifacts. This page is the canonical reference for their structure. All keys are always present; fields that do not apply are null (stable schema: parse values, never key presence).
| Flag | File | Purpose |
|---|---|---|
--json-out | JSON | Full machine-readable result (this page) |
--sarif | SARIF 2.1.0 | Code-scanning UIs (GitHub code scanning, etc.) |
--out | Markdown | Human summary, suitable as a PR comment |
--report | JSONL | Append-only event ledger (audit/debugging) |
JSON report structure
{
"findings": [ ... ],
"statuses": [ ... ],
"advisories": [ ... ]
}
findings[]: confirmed violations
Only findings appear here; each one passed validation. With --fail-on-findings, unsuppressed findings set exit code 1.
| Field | Type | Meaning |
|---|---|---|
regulation | string | Regulation pack that produced the finding (e.g. "gdpr") |
source | "precommiteu" | "retrieval" | precommiteu: confirmed by the validator model with quoted evidence. retrieval: an advisory promoted by the calibrated case-similarity gate |
file | string | Path of the offending file, relative to the scanned root |
start_line / end_line | int | Line range of the chunk containing the violation. Single-chunk files span the whole file; code_evidence is the precise pointer |
probable_article_id | string | Most likely article, e.g. "gdpr_art32". Detection is the primary signal; the article is a strong hint, not a legal determination |
code_evidence | string | null | Verbatim code the validator located as proof. null on retrieval-sourced findings (pattern-matched, no quoted line) |
description | string | What the violation is. Retrieval-promoted findings append [matches a known violation pattern: <article>, similarity <s>] |
eu_ignore_reason | string | null | If suppressed by an inline precommiteu-ignore: <article> reason="..." marker, the recorded reason; otherwise null. Suppressed findings stay in the report for auditability but never affect the exit code |
eu_ignore_source | "inline" | "config" | null | Where the suppression came from; null when not suppressed |
To list every accepted finding and its stated justification, see Ignoring code and suppressing findings.
statuses[]: one entry per regulation scanned
| Field | Type | Meaning |
|---|---|---|
regulation | string | Regulation pack name |
status | "scanned" | "skipped" | "failed" | Outcome for this regulation |
detail | string | null | Human-readable note when not simply scanned |
chunks_scanned | int | Code chunks analyzed |
detector_candidates | int | Raw candidates the detector emitted (before validation) |
validator_rejected | int | Validator-kept results dropped at the final translation checks |
advisories[]: unconfirmed candidates, never blocking
Detector candidates the validator could not confirm, emitted only for files with no confirmed finding. Advisories never affect the exit code.
| Field | Type | Meaning |
|---|---|---|
regulation | string | Regulation pack name |
file | string | File the candidate refers to |
description | string | The detector’s description of the suspected issue |
retrieval_verdict | "violation_pattern" | "compliant_pattern" | "inconclusive" | null | How the candidate compares against the pack’s case index; null when the pack ships no index |
retrieval_confidence | float | null | Violation share of the retrieved-neighbor vote, 0..1 |
retrieval_similarity | float | null | Normalized similarity of the closest known case, 0..1 |
retrieval_article_id | string | null | Probable article suggested by the neighbor vote (a soft hint, since advisories are for human review) |
SARIF report
Standard SARIF 2.1.0: one rule per article cited, one result per finding. Suppressed findings carry properties.eu_ignored: true and the reason. All text fields are HTML-escaped. Import it into any SARIF-aware code-scanning UI.
Markdown summary (--out)
A table of confirmed findings (location, article, evidence, description) headed ## precommitEU: regulatory scan. Intended to be posted verbatim as a PR comment.
Event ledger (--report)
Append-only JSONL, one event per line, each {"event", "ts", "payload"}. Written incrementally and fsync-friendly: a crashed scan leaves a readable ledger up to the moment of failure.
event | payload keys |
|---|---|
scan_start | files_total, regulations |
detector_adapter | regulation, adapter |
files_oversized | files |
ci_diff_resolved | merge_target, resolved_ref, changed_files |
file_start | file, regulation, chunks |
file_ignored | file, regulation |
orchestrator_done | file, regulation, route, fell_back, detector_called, validator_called, kept_raw, tool_calls, exit_reason |
finding | file |
file_done | file, regulation, kept |
file_error | file, regulation, error |
scan_interrupted | files_done, files_total |
scan_done | findings_total, advisories_total, elapsed_ms |
A full finding record (the same object as in --json-out) is appended alongside the finding event.
orchestrator_done: what happened to one file
One record per file per regulation. This is the only place the analysis route and its outcome are reported.
| Key | Type | Meaning |
|---|---|---|
route | "direct" | "orchestrator" | Which route the file was sent to. See cli.md for how auto decides |
fell_back | bool | true when the orchestrator stopped without reaching the detector and the file was re-scanned on the direct route. The remaining fields then describe that second pass |
detector_called | bool | Whether the detector ran at least once on this file |
validator_called | bool | Whether the validator ran at least once |
kept_raw | int | Findings the validator kept, before the article and evidence checks. Always >= the file’s confirmed finding count |
tool_calls | int | Direct route: detector + validator calls. Orchestrator route: agent steps that invoked a tool (EMIT is not counted) |
exit_reason | string | Why analysis of this file stopped, from the table below |
kept_raw minus the kept in the following file_done event is the number dropped by the evidence and article-registry checks. Set PRECOMMITEU_DEBUG_VALIDATOR=1 for the per-candidate drop reason.
exit_reason values
| Value | Route | Meaning | Counts as a failed file? |
|---|---|---|---|
direct | direct | Every chunk was processed normally | No |
emit | orchestrator | The agent decided it was done and stopped | No |
budget_exhausted_time | both | The per-file wall clock ran out (--max-wall-seconds-per-file) | No |
budget_exhausted_iters | orchestrator | The agent used all its steps without stopping (--max-orchestrator-iterations) | No |
direct_partial | direct | The detector or validator raised on at least one chunk | Yes |
loop_step_failed | orchestrator | Three consecutive agent steps failed to run or to parse | Yes |
file_ignored | orchestrator | Ignore directives stripped the whole file | No |
unknown_regulation | orchestrator | The regulation pack could not be imported | No |
file_unreadable | orchestrator | The file could not be read. Rarely seen: an unreadable file is normally caught earlier and reported as file_error | No |
Only direct_partial and loop_step_failed increment the per-regulation files_errored counter, flip that regulation’s status to failed, and drive exit code 3 under --fail-on-error (or --ci).
An orchestrator run can stop without ever reaching the detector. Rather than report that file as clean, the scanner re-scans it on the direct route with whatever is left of --max-wall-seconds-per-file and sets fell_back to true.
That recovery is not always possible. A record with detector_called: false is a file that was not analysed: it contributes no findings and does not affect the exit code.
jq -r 'select(.event == "orchestrator_done")
| select(.payload.detector_called == false)
| "\(.payload.file) \(.payload.exit_reason)"' scan-events.jsonl