The Disposition Audit¶
Every abicheck compare run reports two totals, not one:
- detected — every change the detectors observed, before any policy ran;
- effective — the changes that actually contribute to this run's gate.
Between them sits a per-change disposition: the single reason each detected change is, or is not, in the effective set. The disposition audit is the block that reconciles the two, so a run that reports "no changes" can still tell you that 100 removals were detected and every one of them was withheld by a rule you wrote.
Why the two totals are separate¶
A compatibility check has one selector grammar for suppression but several places where a finding can stop counting: an explicit suppression rule, deduplication of two views of the same change, a contract that excludes the finding's surface, evidence that ran out before relevance could be decided, or a severity setting that scores the finding as a warning. Reporting only the survivors makes all of those look identical to "nothing happened".
The rule the audit makes checkable is that policy never changes what was observed — it only moves a finding between dispositions. Adding a suppression rule to a run must leave the detected total exactly where it was.
The six dispositions¶
Each detected change ends in exactly one of these, and the counts sum to the detected total:
| Disposition | What it means |
|---|---|
gating |
Contributes to this run's exit code. |
non_gating |
Evaluated, and scored as not gating — a compatible addition, or a category your severity settings rate below error. |
suppressed |
A --suppress rule matched it. The audit names the rule. |
out_of_contract |
Proven outside the contract the run was told to judge against (--contract). |
unresolved_relevance |
Relevance could not be decided, because the evidence for deciding it was absent. Not the same as "irrelevant". |
deduplicated |
A second view of a change already counted once — a grouped child, a collapsed versioned symbol, a redundant finding. |
out_of_contract and unresolved_relevance are deliberately distinct: the
first is a positive determination, the second is evidence running out. See
Contract Evaluation.
Where it appears¶
The one-line summary carries the counts inline:
$ abicheck compare old.so new.so --suppress suppressions.yaml -o oneline=-
NO_CHANGE: no changes (0 total) [audit: 100 detected, 0 gating, 100 suppressed]
The JSON report (-o json=...) carries a disposition_audit block with
the same counts plus the rules that produced
them; the review digest renders it as a table; the sticky PR comment carries a
counts row and rule attribution; SARIF, JUnit and HTML each carry the counts in
their own format's idiom. A view may collapse detail — the not-evaluated
detector list, the rule tail — but none of them drops the counts. See
Output Formats.
Detectors that did not run¶
A detector whose supporting evidence was absent is reported as
not_evaluated, not as a detector that ran and found nothing. "No DWARF on
either side, so no layout comparison happened" and "layouts were compared and
are identical" are different statements about a run, and the audit keeps them
apart. not_evaluated is a reporting distinction only: it never changes a
verdict, an exit code, or the run's reported confidence.
Configured rules and their status¶
Every configured suppression, reclassify: and acknowledgment rule has one row
in the audit, including a rule that matched nothing. Each row says how the rule
fared, so "this waiver does nothing any more" is visible rather than silent:
| Field | Meaning |
|---|---|
status |
active (disposed of at least one finding), shadowed_only (matched findings, but an earlier rule claimed them all), expired (past its expires date), unused (matched nothing, and at least one detector that can produce one of the rule's change kinds ran), not_evaluated (matched nothing, and none of those detectors ran). |
matched_count |
Findings the rule disposed of. For a reclassify rule, the findings whose verdict it changed (also changed_count). |
shadowed_count |
Findings the rule also matched but an earlier rule claimed, or a no-op reclassification. |
selector_matched_count |
matched_count plus shadowed_count. |
rule_id |
The rule's id: when it has one, else rule- plus the first 12 hex digits of the SHA-256 of its canonical identity (sorted-key JSON of its selector and gate fields, dates in ISO 8601), e.g. rule-2f4c127a72d9. Matches [A-Za-z0-9._-]+ and is stable across Python versions and YAML key order; give the rule an id: for a readable name. label and reason are their own fields. |
rule_identity |
Only on a rule with no id:: the canonical identity its rule_id digests, as readable JSON. suppression_audit.stale_rules shows the same text, and suppression_audit.stale_rule_ids gives each stale entry's rule_id in the same order, so you can join a stale entry to its row here. |
entry_index |
The rule's position in its file, so two identical entries stay two rows. |
The status is decided per rule. A rule's kind/change_kind selector names
the change kinds it can match; a rule with no kind selector could match any
kind. A detector that can produce one of those kinds is a candidate for the
rule. A zero-match rule reads unused when at least one candidate ran, and
not_evaluated when none did. So on an ELF-only library, a stale symbol-kind
rule reads unused, while a rule scoped to a DWARF-only kind reads
not_evaluated because its only candidate (the DWARF detector) did not run.
A rule scoped to a kind that no detector produces (one a pipeline step emits)
also reads not_evaluated: no detector ran that could have checked it.
See the
unused-rules axis
for the unused_rules: block gate that reacts to unused only.
What it means for suppression¶
A suppression records that a finding was withheld; it is not evidence the finding was wrong. Two consequences follow, both visible in the audit:
- the withheld finding stays in the detected total and names the rule that hid
it, including the suppression document's path,
reason,labelandexpires; - the release recommendation reads the same conserved ledger, so a suppressed
ABI/API break is reported as a
major-class finding needing review rather than "no version bump required".
Full rule syntax and the audit trail a rule leaves behind: Suppressions.
What it means for acknowledgment¶
An acknowledgment records that a finding was seen and intentionally
accepted — the opposite intent from suppression. An acknowledged finding
keeps its own disposition from the six above (it is not a seventh state);
the audit additionally names which acknowledgment record covered it
(acknowledged_by), and a policy acknowledgments: block can separately
flag a public addition that carries no such record. Full record format and
the additions review gate: Change acknowledgment.