ADR-069: Name Shape Is Not Contract Membership¶
Date: 2026-09-12
Status: Accepted — implemented. Records one product rule and the three
behaviour changes it forces, of which one (v0 leaving
DEFAULT_EXPERIMENTAL_NAMESPACES) is a change to an existing default and is
the reason this ADR exists at all: AGENTS.md's "Authority" rule requires an
ADR and a migration path before an established default moves. Amends
044 (adds the
experimental_namespaces: policy key beside its internal_namespaces:) and
constrains what 049's
consumers may claim without it. Owners: abicheck/model/symbol_ownership.py,
abicheck/diff_namespaces.py, abicheck/report/render_markdown.py,
abicheck/policy/policy_file_namespaces.py.
Context¶
A user comparison of a real C++ library produced a report that was correct in its detection and wrong in its explanation, three separate ways. All three traced to one habit: abicheck answered "is this part of the public compatibility contract?" from how a symbol or namespace is spelled.
- The Markdown surface breakdown said RTTI and internal-namespace breaking
findings were "typically … not public-API breaks" and labelled the
remainder the "genuine public-surface" count. The library's real break was
a vtable layout change: virtual functions added to a public base shifted
the dispatch slots of a derived class's own virtuals, confirmed by both
GCC and Clang emitting
jmp *0x60(%rax)before andjmp *0x70(%rax)after. The report told the user to disregard it. symbol_originscanned the whole mangled string for an internal-namespace component. A mangled name embeds its parameter types, sosvs::consume(const svs::detail::Token&)—_ZN3svs7consumeERKNS_6detail5TokenE— was classified internal on the strength of a6detailbelonging to the parameter.v0was an unconditional entry inDEFAULT_EXPERIMENTAL_NAMESPACES, so a library whose current public API lives in an inline-versionednamespace v0had every removal from that API reported as an experimental-API removal.
Decision¶
D1. Symbol representation and naming convention are not contract
membership. A vtable, typeinfo or VTT symbol says how an entity is
represented; a detail/impl segment says what convention a project
follows; a version segment says which API version a declaration belongs to.
None is evidence about the compatibility contract. Contract membership is
ADR-049's question, answered from declaration identity, provenance and
reachability against a resolved --contract.
D2. A name-derived bucket may be counted, never used to discount a
finding. Reporting "N of these findings are vtable/RTTI artifacts" is
useful. Concluding "therefore they are not public breaks" is not supported by
that evidence and must not appear in any output. Where a report shows such a
breakdown it states what the counts do not establish and points at
--contract.
D3. Only the scope that owns an entity determines its scope-convention classification. Neither a parameter type's namespace nor the entity's own leaf name may. This is resolved structurally (the Itanium nested-name parser), not textually; where the parser does not model a production, the fallback is restricted to the nested-name region and errs toward under-detecting the convention.
D4. A version namespace is not a stability promise. v0 leaves the
built-in experimental set. Projects that do use a version segment to mean
"experimental" state it explicitly.
The default change and its migration (D4)¶
This is the part AGENTS.md requires this record for.
What changes. DEFAULT_EXPERIMENTAL_NAMESPACES goes from
("experimental", "preview", "v0") to ("experimental", "preview").
Who is affected. Only a project with a namespace segment spelled exactly
v0, and only for the findings DetectNamespacePatterns produces
(EXPERIMENTAL_*). No other project's output changes at all.
EXPERIMENTAL_* is an overlay, not a relabelling. This ADR's first draft
said the finding was "still emitted, only under a different kind"; that was
wrong, and the correction matters for reading the rest of this section.
DetectNamespacePatterns appends to what the ordinary detectors already
produced — it never replaces it. Measured on a removed ns::v0::foo:
| Configuration | Findings | Verdict |
|---|---|---|
v0 not experimental (this change's default) |
func_removed (BREAKING) |
BREAKING |
v0 configured experimental |
func_removed (BREAKING) + experimental_removed_without_replacement (API_BREAK) |
BREAKING |
So the default change removes one additional contextual finding; it does not alter, downgrade, or hide the underlying removal.
Is this a loosening? No, and the overlay structure is why. The plain
FUNC_REMOVED is emitted either way and is already BREAKING, which
dominates the overlay's API_BREAK, so the verdict and the exit code are
identical under both configurations — a break cannot be hidden by this change,
because the finding that carries the break was never the one being removed.
What is dropped is only the annotation asserting the removal was expected —
the assertion this ADR's D4 says a version segment cannot support in the first
place. A project that wants the annotation back states it and gets both
findings, exactly as before.
Migration. One line in a --policy document restores the previous
behaviour exactly, overlay included:
The key is new in this change (PolicyFile.experimental_namespaces, parsed by
policy/policy_file_namespaces.py, threaded through
PipelineContext.experimental_namespaces), and it is the mechanism this ADR
offers in exchange for the default: the behaviour is not withdrawn, it stops
being assumed. It is deliberately a policy key rather than a CLI flag —
which segments a project treats as unstable is a stable property of the
project, not a per-run operand (ADR-068's operand-vs-property split).
Why not a deprecation cycle. There is no spelling to deprecate: the old
behaviour was an unnamed default, so there is nothing a warning could tell a
user to stop writing. Emitting a warning on every comparison that happens to
contain a v0 segment would fire mostly for projects the default was wrong
about, which inverts the signal. Hard change plus a documented one-line
restoration, consistent with ADR-068's "hard removal, no deprecation aliases".
Consequences¶
- The
experimental_namespaces:key becomes public policy surface, registered indocs/_meta/topics.yamland documented indocs/use/policies.md, and contributessurface.experimental_namespacesto the effective-config digest — two runs differing only in this key must not report identical effective configuration, since it changes which findings are emitted. - The JSON
abi_surface_breakdownblock keeps itsrtti_churn/internal_churnkey names for report-schema stability. They now read against D2's prose, which is an accepted wart: renaming them is a schema break for a cosmetic gain, and the block's counts were never the defect — the conclusion drawn from them was. - D1/D2 are enforced negatively (outputs stop claiming what they cannot
establish). The positive half — classifying these findings against a
resolved contract — is ADR-049's
--contractmachinery, which the surface breakdown does not consult. That gap is recorded against the bug class rather than closed here. - A fourth defect from the same report is not addressed by this ADR:
Visibility.PUBLICused as a proxy for "declared in the public API", which conflates source presence with dynamic export. It is an evidence-selection problem, not a naming one, and needs anAbiSnapshotschema change inside ADR-050's comparability contract. Recorded indocs/contribute/known-gaps.md.
Alternatives considered¶
- Keep
v0and special-case inline namespaces. Rejected: inline-ness is not reliably observable from every evidence tier, and the rule would still be a guess about intent from spelling — the thing D1 rejects. - Treat
v0as experimental only when nov1+ exists. Rejected for the same reason, with an added failure mode: a project's first stable release would silently reclassify its whole history. - Drop the RTTI/internal breakdown entirely. Rejected: the counts are
genuinely useful for diagnosing a missing
-fvisibility=hidden. D2 keeps the observation and removes only the unsupported conclusion. - Fix the mangled-name scan by lengthening the component list. Rejected: the defect is positional, not lexical — no list of components distinguishes a parameter's namespace from the owner's.