Case 195: Ambiguous Simultaneous Rename, Correctly Not Reconciled¶
| Field | Value |
|---|---|
| Verdict | ๐ก COMPATIBLE_WITH_RISK |
| Category | Risk |
| Platforms | Linux |
| Flags | Bad practice |
Detected ChangeKinds |
public_api_internal_dependency_added |
| Source files | examples/case195_header_graph_ambiguous_rename_not_reconciled/ |
Category: Risk (Source Graph / Reconciliation) | Verdict: ๐ก COMPATIBLE_WITH_RISK
Verdict and consumer impact¶
A public struct demo::Config2 has two sibling private field-type
dependencies, demo::detail::RawA and demo::detail::RawB, both declared in
the same private header. In v2, both are renamed simultaneously โ
RawA โ RawX, RawB โ RawY. This is the deliberate counter-example to
case194: when
abicheck's graph reconciliation genuinely cannot tell two declarations
apart, it must refuse to guess. Consumers see no hard break either way โ
both are internal types โ but a reviewer relying on abicheck's rename
explanations should know the tool stays silent here rather than risk
reporting a wrong pairing.
Old/new diff¶
| v1 (conceptual) | v2 (conceptual) |
|---|---|
struct Config2 { detail::RawA a; detail::RawB b; }; |
struct Config2 { detail::RawX a; detail::RawY b; }; |
This case ships a hand-built pair of evidence-model fixtures (old.json /
new.json, SourceGraphSummary objects) instead of compiled v1/v2
sources. See
scripts/gen_l3l4l5_examples.py.
abicheck command¶
There is no compiled binary or header pair to point abicheck compare at โ
the fixture is a raw SourceGraphSummary, the same evidence object
dump --sources/--build-info would embed inside a real snapshot. The
reproducible command runs abicheck's source-graph diff function directly:
python3 -c "
import json
from abicheck.buildsource.source_graph import SourceGraphSummary, diff_source_graph_findings
old = SourceGraphSummary.from_dict(json.load(open('old.json')))
new = SourceGraphSummary.from_dict(json.load(open('new.json')))
for c in diff_source_graph_findings(old, new):
print(c.kind.value, c.symbol, c.old_value, '->', c.new_value)
"
Expected abicheck finding¶
public_api_internal_dependency_added demo::Config2 no internal dependency -> reaches 2 internal decl(s)/type(s)
public_api_internal_dependency_added fires twice (once per newly-reached
internal type โ RawX and RawY). No declaration_renamed finding is
produced for either pair โ the conservative, correct outcome when identity
cannot be safely established.
Minimum evidence¶
min_evidence: L5 โ attempting (and correctly declining) to reconcile the
two renamed nodes requires the derived source graph's structural-context
matching (G31 Phase B, ADR-048). No lower evidence tier carries graph node
identity at all.
Why abicheck catches it¶
abicheck.buildsource.graph_reconcile tries the same two match tiers as
case194. Alias matching fails because every alias (qualified name,
normalized signature, source-relative identity) changed on both renamed
nodes. Structural-context matching fails too, for a more interesting reason:
TYPE_HAS_FIELD_TYPE edges carry no per-field discriminator beyond
role: "field", so both old nodes โ and both new nodes โ occupy the
identical structural position ("the sole field-type target of
demo::Config2, role field" describes two different nodes equally well).
The reconciler detects this collision and refuses to pick a pairing rather
than guess (see graph_reconcile.py's _structural_context docstring and
tests/test_graph_reconcile.py's test_ambiguous_rename_does_not_reconcile).
The raw diff still reports both RawX and RawY as genuinely new internal
dependencies, since neither shares a node id with anything in the old graph.
Runtime failure demonstration¶
There's no app.c here and no crash to demonstrate โ both renamed types are
legitimately internal, and this case is entirely about what abicheck's
reconciliation step does and doesn't claim. The real-world scenario is the
same CI job as case194, but here it correctly avoids putting a specific
"RawA was renamed to RawX" claim into a PR comment when the evidence can't
actually support which old name maps to which new one โ guessing (e.g.
"first old wins first new") would be exactly the false-positive-by-
arbitrary-choice class of bug ADR-045 fixed for flat type matching,
generalized here to graph nodes.
Safe redesign¶
No fix needed for the finding itself โ the two internal types are legitimately internal. If a downstream reviewer wants clearer provenance across renames, avoid renaming multiple sibling internal dependencies of the same public entry in the same release, or keep them structurally distinguishable (e.g. different declaring files) so evidence-based reconciliation has something unambiguous to key on.
Real-world example: a bulk rename pass (e.g. an automated refactoring tool renaming several sibling internal types in one commit) is exactly the scenario where a rename-reconciliation heuristic must know when to stay silent rather than mis-pair two unrelated declarations.
Cross-tool comparison¶
abidiff/abi-compliance-checker operate on compiled binaries and debug
info; neither has an equivalent to abicheck's source-graph reconciliation
machinery, so this ambiguity-detection behavior โ and the deliberate
refusal to guess โ is unique to abicheck's L5 build-source evidence layer
(ADR-045, ADR-048). Contrast with
case194, the matching
positive case where an unambiguous single rename does reconcile.
Source files¶
new.jsonold.json
See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.