Skip to content

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, label and expires;
  • 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.