Unified Impact Assessment¶
abicheck's reachability, graph-proof-path, and suppression-decision signals
for a single finding used to live in several independently-set fields on
that finding, with no shared object to query them through. This page
documents impact_assessment — the unified, queryable view over those
fields — and reachability_state, the tri-state reachability signal it made
visible in JSON/SARIF output for the first time.
What reachability means¶
A finding's subject is reachable when a walk from the public surface —
public headers, exported symbols, and the bodies of public inline functions
— arrives at it: changing it is exactly as breaking as changing a public
declaration, whatever namespace it lives in. It is unreachable when the
walk positively established that nothing public depends on it, which is the
only state in which suppressing a break there is safe. And it is
unknown when no walk reached a verdict, or the only evidence available
is itself flagged narrowed or degraded for the relevant edge family — an
honest "we did not check", never a synonym for safe
(Graph Coverage & Negative Evidence is why the two must
not be conflated). Before this tri-state existed a consumer saw only a
boolean that was false for both of the last two.
The report fields that carry this — reachability_state, the
impact_assessment object that bundles the reachability, proof-path,
root-cause and suppression-decision fields, and their SARIF spellings — are
documented field by field in
Source Graph Schema § reachability_state
and § impact_assessment.
Root-cause grouping¶
--view root-cause groups findings that share a root cause —
today, findings whose caused_by_type names the same internal entity, or
that share a symbol another finding's caused_by_type actually
references (see reporter_markdown._root_cause_key_and_display for the
exact grouping rule). JSON nests grouped findings under a root_causes[]
array; markdown renders one section per root cause; SARIF and JUnit keep
their existing flat shapes and instead add rootCauseId/rootCause to
each result/failure (SARIF: properties.rootCauseId/properties.rootCause;
JUnit: attributes directly on <failure>) — restructuring either format's
established shape would break every existing consumer's parsing
assumptions. root_cause_id is a stable hash of the grouping key, so it is
consistent across JSON/markdown/SARIF/JUnit for the same underlying report —
none of the four formats can disagree about which findings share a root
cause.
When a root_causes[] group is also one of the RootCauseCorrelator's own
multi-piece groups (see root_cause_evidence above), the group entry gains
strongest_evidence_level/evidence_levels — the group-level counterpart
of each member finding's own impact_assessment.root_cause_evidence.
Absent for a group --view root-cause groups by the broader
caused_by_type/symbol rule but that the narrower, four-kind correlator
doesn't cover.
JUnit's own <testcase> groups by symbol, not by finding
(_partition_changes), so a symbol with more than one change gets multiple
<failure> children under one <testcase> — each carries its own
change's rootCauseId/rootCause independently. There is no merging and so
no "what if this testcase's findings disagree on root cause" question: two
sibling <failure> elements can legitimately show two different root
causes.
Why a consumer required a symbol¶
compare --used-by <app> reports the symbols an application binary needs
that the new library no longer exports, as
consumer_required_symbol_removed findings. On its own that answers what
broke — training-service requires _ZN6detail21train_ops_dispatcherEv —
but not why the application ever depended on an internal dispatcher it never
called.
When the old library's snapshot carries an L5 source graph (dump
--sources/--build-info, or the always-on header-only graph), abicheck
folds the consumer's own requirements into a copy of that graph and walks
back through it: symbol → declaration (source_decl_maps_to_symbol) →
whichever public entry point reaches that declaration in the call graph. The
finding then carries the answer in the fields this page already documents —
impact_assessment.proof_path.root names the public entry, steps is the
chain, and the prose reads
training-servicerequires_ZN6detail21train_ops_dispatcherEvvia public entrytrain: train → detail::train_ops_dispatcher → …
The walk uses the same restricted traversal as internal-leak findings, so it stops at any declaration whose body a consumer does not compile — an ordinary out-of-line exported function's internal calls are never attributed to code that cannot see them. That also means a public entry whose body is not consumer-compiled yields no answer rather than a speculative one, as does a missing graph, a symbol with no captured declaration, or no path from any entry. In every one of those cases the finding is exactly what it was before: absence of an explanation is never evidence that a dependency is absent.
This is G29 Phase 4, slice 1. It adds no ChangeKind, changes no verdict, and adds
no report field — it fills in fields the schema already had.
What this does not cover yet¶
impact_assessment does not (yet) include a list of affected consumers or
use cases, or a coverage summary. The consumer graph exists (above), but as
evidence a finding is enriched from — not as its own
affected_consumers/affected_use_cases fields. Declared use cases
(an optional impact-use-cases.yaml manifest, abicheck.impact.use_cases)
are reported by compare --use-cases as their own use_case_impact
section rather than as a per-finding field — see
Use-Case Impact.
Runtime-trace ingestion (for either graph) and the per-role coverage matrix
being wired through the impact layer are the remainder of G29 Phase 4. root_cause_id/impact_group_id (documented above) are implemented,
but impact_group_id is currently only ever an alias of root_cause_id —
distinguishing them (e.g. bucketing several distinct root causes that share
one broader consumer-visible event under one group while keeping their own
individual root-cause identities) is still open; the RootCauseCorrelator
(G29 Phase 6) that would drive that distinction is implemented and now wired
into the report surface as root_cause_evidence (documented above), but it
doesn't yet feed impact_group_id itself — that would need the correlator's
groups to actually re-bucket findings rather than only annotate them, a
distinct follow-up. Computing root_cause_id needs whole-DiffResult context (which
findings elsewhere reference this one) that a single finding's read view
can't see on its own — the caller resolves it per report/scope
(reporter_markdown.root_cause_lookup_for_changes) and passes it in; see
the Detector Impact Contract
for why the underlying grouping stays a report-level decision
(--view root-cause above) rather than something a detector sets
directly. Adding empty placeholder fields for data no producer can populate
would misrepresent what abicheck actually knows, so unimplemented fields are
left out of the schema entirely rather than always-null.
Ladder: ← Graph Coverage & Negative Evidence · Concepts c3 · Internals · Series overview →