fix(managed): pair members by declared identity before name-free remaps (#2116)

* fix(managed): pair members by declared identity before name-free remaps

The first matching round keyed methods on signature and normalized CIL,
and fields on signature, without the declared type or name. Identical
members of different types fell into one ambiguous group, so a file
compared with itself reported unknowns, and a lone match across types
was reported as an unchanged exact pair while the member's own
counterpart went unpaired.

Pair by declared type, name, and exact CIL/signature first, then by
declared type, name, and raw signature. The name-free CIL/signature,
field-signature, and structural rounds now run last, as remaps, and
skip a pair across declared identities while either member's own type
and name remain unpaired on the other side.

Nested types that share a bare name still share one declared identity,
so their identical members stay ambiguous in a self-comparison.

* fix(managed): keep members with kept identities out of remap rounds

The cross-identity check applied only to one-to-one groups. A member
whose own declared type and name were still unpaired on the other side
could fall into a larger remap group and be reported ambiguous, and a
blocked candidate could make an otherwise unique remap ambiguous.

A remap round now drops such members before grouping, along with
members whose declaring type is unresolved. Same-identity structural
pairs get their own round before the remaps. Each member's declared
identity is computed once.

Ambiguity limitations now name the evidence a group shares, which
stays true whether the round keyed on names or not.

* fix(managed): keep names in unresolved identities and hold ambiguous ones

A member of an unresolved declaring type had no declared identity, so
the declared-identity rounds keyed it by CIL or signature alone. Two
unresolved members with different names paired as an exact match, and
a file compared with itself reported unresolved members ambiguous.

The declared identity now always includes the name, with a null type
for an unresolved one. Remap rounds still leave such members out.

A remap round now also leaves out a member whose own identity sits in
an ambiguous group on the other side, not only one left one-sided.

The undecoded-counterpart check uses the same declared identity.

* fix(managed): keep remap groups with unresolved owners ambiguous

Remap rounds dropped members of an unresolved declaring type before
grouping. Such a member can be another member's own counterpart, so a
resolved member could remap to a copy in another type while its
unresolved counterpart was reported added.

Unresolved members now join remap grouping, and a group that contains
one stays ambiguous instead of pairing.

The undecoded-counterpart set reuses each member's computed identity.

* fix(managed): keep ownerless members out of declared rounds when shared

A member whose declaring type did not resolve has a declared identity
of a null type and its name. The declared rounds paired two such
same-named members even when the other side also held a resolved
member with the same CIL and signature, which could be the owner. The
old name-free first round reported that group as ambiguous.

An ownerless member now enters the declared rounds only when no other
member on either side shares its exact CIL/signature key, and never
the declared structural round. Otherwise it reaches the remap rounds,
which keep any group with an unresolved member ambiguous.

* fix(managed): keep competitors of an ownerless member out of the declared round

When an ownerless member shares its exact CIL/signature key with another
member on the same side, its owner may be that member. The resolved
members holding the key now stay out of the declared round too, so they
cannot pair and consume the candidate the ownerless member may own, and
the remap round keeps the whole group ambiguous as main does. A lone
ownerless member still pairs with its lone counterpart.

* test(managed): split the ownerless comparison tests under the function length limit

The ownerless describe block reached 181 lines, past oxlint's
max-lines-per-function limit of 150. Move the structural-competitor
case into its own block.

* fix(managed): contest signatures and remap only open candidates

* fix(managed): retain blocked raw-signature choices as unknown

* fix(managed): keep member names in signature contention

* fix(managed): scope residual signature contention by name once

ownerlessContested applies competitionIdentityKey itself, but
unresolvedSignatureAmbiguities passed it keys that were already
name-scoped, so the contested set never matched the groups and the
same-signature, different-body case reported additions and removals
instead of one unknown group. Pass the raw signature key.

* fix(managed): constrain matchObserved to declared members

matchObserved passes its items to matchByKeys, which needs Declared, so
its looser { declaring_type } bound failed typecheck (TS2344, TS2322).

* fix(managed): retain held ownership contention for residual matches

* fix(test): bind workspace fixture in Hopper launch cases

* fix(ci): prepare worker tests and preserve Hopper cleanup ordering

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
Jeremy Levartovsky
2026-10-14 09:36:43 +11:00
committed by GitHub
parent c86b09a109
commit 1ebbc3b371
7 changed files with 1192 additions and 58 deletions
+21 -13
View File
@@ -209,19 +209,27 @@ native/runtime questions retain their own authority and coverage.
Names and semantic labels do not establish durable member identity. Comparison
preserves observed differences, structural inferences, and unknowns separately.
Cross-version matching first prefers exact raw CIL/signature identity, with
equal decoded fingerprints. Equal decoded fingerprints alone cannot establish
an exact match: they omit switch targets and collapse distinct NaN encodings.
When that does not match, it pairs an exact declared type, method name, and raw
signature before trying structural body shape. The exact-signature key uses names only
as part of that full tuple; names alone never select a pair. Duplicate tuples
remain ambiguous rather than being paired by token order. Fields follow the
same exact tiers. The raw signature is exact whether or not REA decoded it, so
undecoded signatures pair by that tuple too, but they never enter structural
rounds. An unmatched member is therefore reported as `unknown`, not added or
removed, when its own signature was not decoded or when the other side has an
unpaired member, one-sided or ambiguous, with the same declared type and name
whose signature was not decoded.
Cross-version matching first pairs members of the same declared type and name:
methods by exact CIL/signature identity, then by raw signature, then by
structural body shape; fields by raw signature. Exact CIL/signature identity
requires equal raw CIL digests and decoded fingerprints. Equal decoded
fingerprints alone cannot establish an exact match: they omit switch targets
and collapse distinct NaN encodings. Only then may a remap pair a
renamed or moved member across declared types or names: a method by exact
CIL/signature identity or structural body shape, a field by exact decoded
signature. A remap never takes a member whose own declared type and name remain
unpaired on the other side, so a member is never taken from its own changed
counterpart. A member whose declaring type is unresolved could belong to any
type, so it pairs by name only when no other member shares its exact
CIL/signature key, never by structural shape alone, and any remap group it
joins stays ambiguous. The exact-signature key uses names only as part of that
full tuple; names alone never select a pair. Duplicate tuples remain ambiguous
rather than being paired by token order. The raw signature is exact whether or
not REA decoded it, so undecoded signatures pair by that tuple too, but they
never enter structural rounds. An unmatched member is therefore reported as
`unknown`, not added or removed, when its own signature was not decoded or when
the other side has an unpaired member, one-sided or ambiguous, with the same
declared type and name whose signature was not decoded.
For a matched method, an unavailable or partial body makes body-shape facets
unknown while preserving observed signature differences. For complete bodies,