Skip to content

Four more ways to oblige an implementer that the interface-addition gate does not see (measured) #3489

Closed

Description

@rbuergi

#3465 shipped Interface additions (implementers declared) (PR #3487), which fires when a diff adds
a member an outside implementer would have to write. While building it, four other ways to oblige
an implementer were measured against the detector and found invisible. The control in the same
run — public abstract void B(); added to a public abstract class — reports
implementer-obliging-added correctly, so each blank below is a real blind spot rather than a broken
probe.

# the change why an implementer breaks why the detector is blind cost to close
a an interface gains a base interface (interface IFoo : IBar) every implementer of IFoo must now supply IBar's members surface_additions diffs member SETS per type; it never diffs base lists small — index the base list per public type and report a growth
b an interface gains an overload of a name it already declares (void M(string) beside void M(int)) it is a member every implementer must write member granularity is the NAME, deliberately large — it would change what "member" means across the whole report, the removal half included, and the current granularity is a documented choice (#3103)
c an existing default member is made abstract the body implementers relied on is gone nothing is added or removed undetectable by a surface detector — this is shape 7 wearing an interface, and #3276 already records why
d a protected abstract member is added to a public abstract class CS0534 in an external subclass, exactly as for a public one only public members are indexed, so it never appears as an addition small — a separate protected-abstract index, kept out of publicMembersAtBase so the removal half's numbers do not move

(a) and (d) are worth doing and are independent of each other. (b) is a real gap but its fix is
disproportionate and touches the removal gate; (c) is out of reach by construction and should stay
documented rather than attempted.

Whatever lands must be falsified the same way #3487 was: a RED arm on a reintroduced instance and a
GREEN arm with the declaration present, with the denominator printed and a floor that fails on zero.
scripts/check-type-forwards.py --self-test is the harness; each mechanism must be shown to redden
cases when disabled.

Reference: Doc/Architecture/CrossRepoPairGate → "Four ways to oblige an implementer that this gate
does NOT see"
, which carries the same table and the measurement.

Related but distinct: #3454 (satellite pin-consistency gates), #2689 (the dependent-compile canary).

Activity

  1. rbuergi commented on Sep 6, 2026

    @rbuergi
    ContributorAuthor

    Closed by hand — #3503 merged as 8ef74ed66, verified an ancestor of origin/main

    The merge queue does not fire Closes links, so this is manual and the ancestry was measured.

    (a) and (d) are closed; (b) and (c) stay open with their reasons, exactly as this issue scoped it.

    # the change now
    a an interface gains a base interface ✅ implementer-obliging-base-added
    d a protected abstract member on a public abstract class ✅ implementer-obliging-protected-added
    b an overload of a name an interface already declares ⛔ open — the fix changes what "member" means across the removal half too, and would fire on every overload added anywhere
    c an existing default member made abstract ⛔ out of reach by construction — shape 7 wearing an interface (#3276)

    All three trigger categories are one verdict in three grammars, so they share one Implementers:
    declaration and one gate.

    The part of this worth reading later

    🚨 The (d) scan measured ZERO over a tree that has five, and the zero was blindness.
    MEMBER_MODIFIERS deliberately holds no access keyword but public, so _leading_modifiers stops
    dead at protected and answers set() — every downstream test then reads false and the shape stays
    invisible. Caught by cross-checking the denominator against grep rather than by trusting the count.
    The fix strips access keywords inside the new scan: widening the shared frozenset would have moved
    publicMembersAtBase and recalibrated the removal half's floors and history — the cure breaking the
    patient. Verified byte-identical before and after: 1 974 / 12 345 / 132 / 452.

    🚨 And the second zero-discrimination went the other way. grep finds six protected abstract
    declarations; the scan reports five, and the scan is RIGHT — RoutingServiceBase is internal abstract class, so nothing outside can subclass it. The crude instrument was the grep. A discrepancy
    is a question, not a verdict, and it resolves in whichever direction the evidence points.

    Verification

    check-type-forwards.py --self-test 76/76 (was 68; +8 surface cases). Four falsification arms,
    each watched failing and then restored: _base_list → set() reds 3; _obliges_external_subclass → False reds 1; the where-clause guard disabled reds 1; the consumer's trigger set narrowed to
    #3465's category alone reds 2.

    One structural change worth knowing about: parse_members and the self-test's _index now return
    dataclasses instead of widening tuples. Not tidying — the harness's tuple silently dropped both new
    indexes
    while the production readers carried them, so the suite would have scored a new mechanism
    green having never passed it in. Surface's own docstring already said why: "a positional fifth
    element is exactly how a new field gets dropped at one call site."

    Follow-up filed: #3508 — this pull request's own gate (#3492/#3500) ran on neither of its two
    builds, because the branch predated the gate and the queue build skips that job. No harm here (the
    delta was measured by hand and tabulated in the PR body) but that was authorship luck, not a property
    of the system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions