Detector Impact Contract¶
A detector that reads the optional L5 source graph
— today's source_graph_findings.py/internal_leak.py/crosscheck.py, and
every future G29 Phase 5/6 family
(template instantiation, macro/config, virtual dispatch, callback/
function-pointer, object/link provenance) — has more evidence available to
it than a plain artifact diff does, and more ways to get the honesty of
that evidence wrong. This page is the checklist such a detector must satisfy
before its findings are trustworthy, on top of the ordinary new-ChangeKind
procedure (/CLAUDE.md "Adding a new ChangeKind", and the
G24 shared checklist).
1. Classification: never fabricate BREAKING from graph-only evidence¶
The one rule that governs everything in abicheck/buildsource/ (see
that directory's CLAUDE.md) applies to every impact-layer consumer too:
artifact-backed L0/L1/L2 evidence stays authoritative for a shipped BREAKING
verdict. Graph evidence (L5) may explain, localize, scope, add confidence/
provenance, or correlate an artifact-proven break — it must never itself
promote a finding to BREAKING_KINDS. A graph-only detector's ChangeKind
defaults to API_BREAK_KINDS (source-level) or RISK_KINDS (deployment/
context risk); it only escalates to BREAKING when an artifact diff also
proves the break (see evidence_policy.apply_evidence_policy's modulation
model for the mechanism).
This matters most for an over-approximating walk: VIRTUAL_CALL_MAY_DISPATCH_TO
(Phase 5 item 3) must stay overapprox/RISK, never exact/BREAKING,
however confident the walk otherwise looks — see
Proof-path preference order
for the effect_transitions mechanism that marks a proof as
over-approximated in the first place, and propagate that marking into your
ChangeKind's classification, not just the displayed path text.
2. Coverage honesty: report what you didn't check, not just what you found¶
A detector built on the graph must be able to say "I checked and found
nothing" apart from "I never checked" — the same distinction
ReachabilityState draws for reachability.
Concretely:
- Stamp
extractor_passes/narrowed_passes/degraded_passes(family grain) and, where your detector's precision genuinely varies by edge role, the(kind, role)-grain coverage matrix (ROLE_COVERAGE_MATRIX) — an absent edge is never proof of an absent dependency when the relevant pass didn't run or ran narrowed. - An absent edge from a confirmed-complete pass is real negative evidence; the same absence from a narrowed/degraded pass is not — don't let your detector's own confidence label imply otherwise.
- If your evidence source is itself opt-in and typically empty today (like
GraphEdge.occurrences— seeoccurrence_id), document that plainly rather than let a reader assume "empty" means "checked, found none."
3. Suppression safety: never silently withhold a public-reachable break¶
If your detector's finding can be suppressed, it must go through the same
reachability-aware gate every other graph-derived finding does
(ADR-044):
Change.reachability_state/public_reachable set honestly (never
PROVEN_UNREACHABLE on UNKNOWN evidence), so suppression.py's
reachability: proven-unreachable-only gate can do its job. A new walk that
skips this — e.g. by hand-rolling its own reachability check instead of
routing through post_processing.MarkReachability or the shared
TraversalPolicy machinery — risks a suppression rule hiding a break it was
never actually proven safe to hide.
4. Populate the impact shape, don't hand-roll a parallel one¶
ImpactAssessment/GraphProofPath/FindingDecision
(Unified Impact Assessment) are the shared
shape every graph-derived finding's reachability/impact fields flow through.
A new detector should:
- Set the existing
Changefields (reachability_state,reachability_kind,reachability_proof_path,public_reachable) the same way today's producers do —impact.engine.assess_changederivesImpactAssessmentfrom them automatically; there is no separate object to populate by hand. - When you have a structured
list[GraphEdge]path (not just a formatted string), callbuildsource.graph_impact.attach_impact_metadata— never hand-build theimpact_proof_pathnode/edge-dict shape inline. If you have more than one candidate path, run them throughselect_preferred_graph_path(or extend it — see Proof-path preference order for which tiers it already covers) and pass the runner-ups asalternative_pathsrather than silently dropping them. - Reuse
TraversalPolicyfor a new graph walk instead of re-deriving an inline edge-kind/stop-condition/confidence-floor combination — a new walk-specific policy instance is fine; a new ad hoc walk that ignores the shape entirely is what this contract exists to prevent.
5. What this contract does not require (yet)¶
- Consumer-proven evidence (
select_preferred_graph_path's tier 1). This now exists — G29 Phase 4 slice 1 (ADR-057) folds a real--used-byconsumer's requirements into the graph — but it is still not something a detector claims. The tier is derived fromCONSUMER_REQUIRES_SYMBOLedges present in the graph the selector is already given, so a detector participates by passing its candidate paths toselect_preferred_graph_pathas it already should; don't set a consumer-proven marker yourself, and don't invent a proxy for the evidence. - Computing your own
root_cause_id/impact_group_id. These do exist onImpactAssessment(ADR-052 Slice 7), but correctly computing either needs whole-DiffResultcontext (which findings elsewhere reference this one) a singleChange's read view can't see — the report-level caller (reporter_markdown.root_cause_lookup_for_changes) resolves them and passes the value intoassess_change, not the detector. A new detector should participate by setting the existingcaused_by_typefield where it applies (the same signal--report-mode root-causegroups on) rather than inventing a parallel correlation mechanism; the fullRootCauseCorrelatorcorrelating findings with nocaused_by_typelink at all is still Phase 6. - A new report format restructuring. Every new field this contract asks
for is additive to the existing JSON/SARIF/JUnit shapes (mirrors how
--report-mode root-causereached JUnit without restructuring its per-symbol<testcase>tree) — don't propose a breaking schema change to accommodate a new detector's output.
Checklist summary¶
Before merging a new graph-derived detector, alongside the ordinary
new-ChangeKind procedure:
- [ ] Classification never escalates to
BREAKINGfrom graph-only evidence. - [ ]
extractor_passes/narrowed_passes/degraded_passes(and the per-role matrix, if applicable) are stamped honestly. - [ ]
reachability_state/public_reachableare set so suppression can't silently hide a real break. - [ ] Structured proof paths go through
attach_impact_metadata/select_preferred_graph_path, not a hand-rolled equivalent. - [ ] A new FP-rate-gate corpus case if the detector is heuristic (mirrors the G24 shared checklist's item 7).