[PR #2116] [MERGED] fix(managed): pair members by declared identity before name-free remaps #5040

Closed
opened 2026-10-31 00:59:55 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/2116
Author: @JayOfTheKeyboard
Created: 10/12/2026
Status: ✅ Merged
Merged: 10/13/2026
Merged by: @morluto

Base: main ← Head: fix/managed-compare-declared-identity


📝 Commits (10+)

  • c69af4b fix(managed): pair members by declared identity before name-free remaps
  • 21c99ce fix(managed): keep members with kept identities out of remap rounds
  • 95909dc fix(managed): keep names in unresolved identities and hold ambiguous ones
  • 3f997a7 fix(managed): keep remap groups with unresolved owners ambiguous
  • 1d1a48a fix(managed): keep ownerless members out of declared rounds when shared
  • b62b912 fix(managed): keep competitors of an ownerless member out of the declared round
  • 33cd41a fix(managed): preserve structural competitors of unresolved owners
  • d3ff384 test(managed): split the ownerless comparison tests under the function length limit
  • 1a61f95 fix(managed): contest signatures and remap only open candidates
  • 2590243 fix(managed): retain blocked raw-signature choices as unknown

📊 Changes

8 files changed (+1200 additions, -77 deletions)

View changed files

📝 docs/managed-code-analysis.md (+21 -13)
📝 src/contracts/managed/managedWorkflowToolContracts.ts (+1 -1)
📝 src/domain/managed/managedMemberComparisonCoverage.ts (+2 -2)
📝 src/domain/managed/managedMemberComparisonItems.ts (+22 -16)
📝 src/domain/managed/managedMemberComparisonMatch.ts (+251 -23)
📝 tests/acceptance/providers/hopperStartupRecovery.test.ts (+2 -0)
📝 tests/boundary/javascript/nativeTypeBindings.test.ts (+8 -19)
📝 tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts (+893 -3)

📄 Description

Summary

compare-managed-members pairs members of different types. Comparing System.Collections.dll from .NET 8.0.31 with 10.0.7 pairs LinkedList`1.get_Count with OrderedDictionary`2.get_Count as unchanged with exact confidence. The real LinkedList`1.get_Count on the right is left in an ambiguous group. The same file compared with itself gives 573 unchanged and 39 unknown, where all should be unchanged. No issue is linked. #889 raised this as an open question ("Falling through to the exact (type, name, signature) tuple would resolve them without guessing from names alone"). I found it by comparing REA with a System.Reflection.Metadata dump over 208 real comparisons.

Problem and expected behavior

matchByKeys in src/domain/managed/managedMemberComparisonMatch.ts runs its first round on keys without the declared type or name. For methods the key is the raw signature and normalized CIL. For fields it is the raw signature only. Two things follow:

  • Identical members of different types land in one group. A group larger than 1:1 becomes ambiguous, and its members are marked used, so the next round, which does include the type and name, never sees them. Nested Enumerator types each have a Dispose with the same CIL, so a file compared with itself reports them as unknown.
  • A 1:1 group is paired as exact, even when it joins two different types. The item has no name or type dimension, so the wrong pair reports unchanged.
$ rea compare-managed-members 8.0.31/System.Collections.dll 10.0.7/System.Collections.dll --format json
LinkedList`1.get_Count (0x06000061) -> OrderedDictionary`2.get_Count (0x06000086)  unchanged  exact-il-signature  exact
LinkedList`1.get_Count (0x06000047)                                                in an ambiguous group
$ rea compare-managed-members 8.0.31/System.Collections.dll 8.0.31/System.Collections.dll --format json
summary: unchanged 573, unknown 39

Expected: the CLI description says members are compared "by exact declared type, name, and signature; names alone never form a match". A member should meet its own declared type and name first. Matching a renamed or moved member is a separate, later step.

Change and scope

  • Rounds now run in this order. First come the rounds within one declared type and name: exact CIL/signature, then raw signature (unchanged), then structural shape for methods. Then come the name-free remap rounds: exact CIL/signature and structural shape for methods, exact signature for fields. The bases and confidence values are unchanged.
  • The name-free rounds still remap renamed members. remaps renamed methods by exact CIL/signature without using names still passes unchanged. That test renames the type and the method together.
  • A remap round leaves out any member whose own type and name are still unpaired on the other side. Without this, a member whose own counterpart changed signature is still taken by another type's identical member. Members are left out before grouping, so such a member neither joins an ambiguous group nor makes another remap ambiguous. The sets are taken when the round starts, so group order does not decide anything.
  • "Unpaired" includes members held in an ambiguous group. A member whose own identity is ambiguous on the other side may still be that member, so it is not remapped.
  • A remap group that contains a member of an unresolved declaring type (declaring_type null) stays ambiguous and does not pair. That member may be another member's own counterpart, so REA cannot tell which pair is right. This is what main reports for such a group.
  • The declared identity always includes the name, with a null type when the type is unresolved. So unresolved members pair only under the same name, and never by CIL or signature alone.
  • An unresolved member's owner could be any type. So it 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, where its group stays ambiguous. When an ownerless member shares its key with another member on its own side, the resolved members holding that key stay out of the declared rounds as well, so they cannot pair and take the candidate the ownerless member may own (added after review). This is the result main gives, because its first round grouped every member with that key. An ownerless member never gets a more confident pair here than on main.
  • Each member's declared identity is computed once. The undecoded-counterpart check now uses the same identity.
  • The check keys on declared type and name, which overloads share. So a rename is now skipped when an overload that keeps the old name is unpaired on the other side. Example: left T.Foo(int), right T.Bar(int) with the same CIL plus a new T.Foo(): string. main pairs Foo with Bar. This branch reports Foo removed and both right members added. I took this trade because the same check stops the steals in the table below. Reporting a skipped remap as unknown, rather than removed and added, is an option. I left it out to keep this change small.
  • The limitation strings, the compare_managed_members description and docs/managed-code-analysis.md now describe this order.
  • An ambiguous group's limitation now names the evidence its members share: "the same CIL and signature", "the same structural shape", or "the same signature" for fields. This stays true whether the group came from a round keyed with names or without. The exact-signature message is unchanged. One existing test asserts the new CIL wording.
  • does not guess ambiguous field signature matches needed one change. On main, its two same-signature fields formed one ambiguous group because the first round ignored names. Now f pairs with its own f. I renamed the right-hand f too, so the test still checks that two fields linked only by a shared signature stay ambiguous.

Not changed, and yours to decide:

  • Raw signatures name types by metadata row, and rows are build-local. So a signature that names a type never pairs across builds. That causes most of the removed and added pairs in the counts below. It is a separate bug with its own fix, which I will send as a follow-up PR. It touches the same file. Rebased onto this one, it has five conflict hunks, one per file. Four are wording: docs, tool description, limitation strings, ambiguity messages. One is in the test file, where both PRs append tests and helpers at the end.
  • The inventory names a nested type by its bare name (documented). Several nested Enumerator types therefore share one declared identity. All of the 209 unknowns left in the self-comparisons below are such groups. Naming nested types with their enclosing type would fix them, but that changes the inventory, so I left it.
  • The matching.exact_il_signature and field_signature counters count same-identity pairs and remaps together, as they did before. Separating them would add a basis value.
  • Two ownerless members with the same name and signature, and no other member sharing their key, still pair as they do on main. They might belong to two different real types. Refusing that would pair less than main does, so I left it.
  • The undecoded-counterpart check compares declared identities, so it misses an undecoded counterpart whose owner did not resolve. That only shows when ownership fails silently.
  • The algorithm descriptor still reports name_matching as exact-signature-fallback, with no version marker, although the pairing order changed. Versioning it is a contract change.
  • A remap across types or names has no dimension that records the move or rename. That is how main behaves, but more remaps now carry their own identity, so it is easier to notice.
  • A name-free remap still carries exact confidence, and fields may still remap by signature alone across types. Of the 154 cross-identity pairs left in the cross-build counts, 30 are exact. Some are real renames (<>c__12`2..cctor to <>c__15`2..cctor, async state machines renumbered d__75 to d__76). Others are coincidences, such as HttpConnection._chunked to HttpListenerWrappedWebSocket._disposed. In the System.Collections pair above, BitArray.m_array still remaps to OrderedDictionary`2._buckets, because BitArray left that assembly in .NET 10. Lowering that confidence, or limiting field remaps, would change behavior that a test pins.

Contract and boundary impact

  • Semantic owner and earliest changed stage: member pairing in matchByKeys (src/domain/managed/managedMemberComparisonMatch.ts).
  • CLI and MCP/tool-catalog contract: no schema change. The compare_managed_members description and two limitation strings now give the new round order. Three ambiguity messages are reworded.
  • Provider, bridge, target-format, or platform compatibility: none.
  • Evidence, artifact, provenance, or reconstruction contract: the same inputs can produce different pairs. No field, basis, or status value is added or removed.
  • Process execution, authorization, cleanup, or containment impact: none.
  • Generated metadata (docs/public/product-catalog.json), package, or installation impact: the generated catalog picks up the new description. Nothing generated is committed.

Evidence and regression coverage

  • Tests added in tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts:
    • A file whose method and field are copied into a second type pairs with itself: 4 unchanged, 0 ambiguous, every pair keeps its token.
    • A method whose CIL changed pairs with its own changed counterpart as exact-signature / changed, beside 1 or 2 identical members of other types, which are reported added. Its unchanged field pairs with its own field.
    • A method and a field whose signature changed are not remapped to another type's identical member, in either direction.
    • The same holds for the structural round, with a twin that has the same body shape and a different CIL digest.
    • A method and field whose signature changed, beside two identical members of other types, are reported removed, not hidden in an ambiguous group.
    • A method whose own identity is gone remaps to the one candidate whose identity is not kept on the other side, instead of becoming ambiguous.
    • A method of an unresolved type is not remapped, in either direction.
    • A method is not remapped to another type's copy while an unresolved member of the same name and CIL may be its counterpart. Nothing is reported added or removed.
    • Two unresolved members with different names and the same CIL and signature do not pair.
    • Ownerless members that share a body stay ambiguous in a self-comparison, as on main.
    • An ownerless Foo stays ambiguous when the other side holds an ownerless Foo and a resolved copy with the same CIL and signature, as on main.
    • An ownerless method stays ambiguous beside a resolved method of the same structural shape.
    • A resolved T.Foo and an ownerless Foo with the same CIL and signature, against only the same T.Foo, stay one ambiguous group in both directions, as on main. Both fail on the commit before the review fix.
    • A lone ownerless member still pairs with its lone counterpart.
    • A method whose own identity is held in an ambiguous group on the other side is not remapped to another type's copy, in either direction.
    • An ambiguous structural remap group names its shared structural shape. Existing ambiguity tests now assert the CIL and field wording.
  • Base reproduction: on main the first 5 behavior cases fail. The self-compare reports 2 ambiguous groups. The changed method pairs with Other.Twin.Main as unchanged / exact-il-signature. The other three report a matched cross-type pair. The next 3 behavior cases and the 3 wording assertions also fail on the first version of this branch, each on its assertion. The new unresolved-counterpart case fails on the third version: Main remaps to Other.Twin.Copy and the unresolved Main is reported added. The Renamed and held-group cases fail on the second version: the unresolved Main pairs with Renamed as exact, and the decoded Main remaps to Other.Twin.Main. The 3 ownerless cases fail on the fourth version, each as a pair where main reports one ambiguous group. I ran those 3 cases against main's matcher too, and they pass.
  • Each part of the fix is needed. I broke each one separately and ran the managed tests:
    • Dropping the declared identity from the first method round fails 10 tests. For the field round it fails 6.
    • Moving the declared signature round after the remaps fails 3.
    • Not leaving out kept identities fails 5. Leaving them out on one side only fails 5 or 4.
    • Letting unresolved types into remaps fails 2.
    • Dropping the declared structural round fails an existing partial-body test.
    • Each of the three ambiguity messages, reverted, fails 1.
    • Dropping the name from an unresolved identity fails 2.
    • Building the held set from one-sided members only fails 1, on either side.
    • Pairing a remap group that contains an unresolved member fails 2. Checking only one side of the group fails 1.
    • Letting ownerless members with a shared exact key into the declared rounds fails 2. Letting them into the declared structural round fails 1. Filtering them out of the remap rounds fails 3. Letting the resolved members that share an ownerless member's key into the declared rounds, or keeping them out of the remap rounds, fails the 2 review cases.
  • User-visible CLI output, built CLI, with the fix: the pair above becomes LinkedList`1.get_Count (0x06000061) to LinkedList`1.get_Count (0x06000047), changed, exact-signature. System.Collections.dll compared with itself gives 732 unchanged and 12 unknown. All 12 are nested types that share a bare name.
  • Real files, built CLI, main (79156e5b) against this branch. No source file this PR touches changed between 79156e5b and the current base, 4276aaf1. None of these DLLs has an ownerless member (0 of 277,486 member sides), so the last two commits leave the table as it is. Self-compares are every DLL in Microsoft.NETCore.App/8.0.31. Cross-build compares are 40 Microsoft.NETCore.App 8.0.31 to 10.0.7 pairs. "Cross-identity" means a matched pair whose declared type or name differs. "Own identity present" means the member's own type and name also appear on the other side of the result.
168 self-compares, main with fix 40 cross-build, main with fix
unchanged 68,043 102,583 2,054 6,688
ambiguous items (members on the left) 4,501 (35,261) 209 (721) 848 (7,234) 322 (1,282)
files with any unknown 84 39 23 15
cross-identity pairs 0 0 490 154
of those exact 0 0 263 30
of those with own identity present 0 0 299 0

Without leaving out kept identities, the cross-build column would read 336 cross-identity pairs, 127 exact, and 185 with own identity present. Of the 154 left, 124 are the structural round at high confidence.

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

  • vitest over the 11 files that cover managed comparison and the compare_managed_members MCP tool: 144 tests passed.
  • npm run test:fast: 394 files, 5,114 tests passed, 3 skipped.
  • npm run check:fast: passed.
  • npm run verify:test-discovery: "Discovered all 915 test files exactly once."
  • npm run docs:check: passed.
  • Real files: the 208 comparisons in the table, all exit code 0 before and after. I checked the 209 remaining self-compare ambiguous groups against a System.Reflection.Metadata dump. Every group is members of distinct nested types with the same bare type name and member name.

Compatibility, safety, and release

  • Breaking changes or migration steps: none. Results for the same inputs can change, toward pairing each member with its own declared identity.
  • Real Hopper/Ghidra, browser, or OS coverage: Linux x64 (Fedora 44) only. No provider runs.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: matching only. One set of identities per remap round, linear in member count. Each identity is hashed once per member.

Review checklist

  • This PR addresses a concrete problem or an agreed enhancement.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/2116 **Author:** [@JayOfTheKeyboard](https://github.com/JayOfTheKeyboard) **Created:** 10/12/2026 **Status:** ✅ Merged **Merged:** 10/13/2026 **Merged by:** [@morluto](https://github.com/morluto) **Base:** `main` ← **Head:** `fix/managed-compare-declared-identity` --- ### 📝 Commits (10+) - [`c69af4b`](https://github.com/morluto/rea/commit/c69af4bb2d8599ffc9183f7c4012e588665a6763) fix(managed): pair members by declared identity before name-free remaps - [`21c99ce`](https://github.com/morluto/rea/commit/21c99cec05b47ef07dd5ad2715d437cf683403a9) fix(managed): keep members with kept identities out of remap rounds - [`95909dc`](https://github.com/morluto/rea/commit/95909dc2bb802cc42f979eac03d339c27ddd4d20) fix(managed): keep names in unresolved identities and hold ambiguous ones - [`3f997a7`](https://github.com/morluto/rea/commit/3f997a7c2d49ae1d54487a867d45e02468987fac) fix(managed): keep remap groups with unresolved owners ambiguous - [`1d1a48a`](https://github.com/morluto/rea/commit/1d1a48ae62240be80a76179428e4e7fa6f15c8aa) fix(managed): keep ownerless members out of declared rounds when shared - [`b62b912`](https://github.com/morluto/rea/commit/b62b9123e7a643122d43c829e174d49683dcf2f1) fix(managed): keep competitors of an ownerless member out of the declared round - [`33cd41a`](https://github.com/morluto/rea/commit/33cd41a2ba9c594275425e565a1a4d3e804b7930) fix(managed): preserve structural competitors of unresolved owners - [`d3ff384`](https://github.com/morluto/rea/commit/d3ff38425c8fe5ba138c36b9332e4ebf30d00056) test(managed): split the ownerless comparison tests under the function length limit - [`1a61f95`](https://github.com/morluto/rea/commit/1a61f95a58ef00007aaacace58ee1f6f29da1296) fix(managed): contest signatures and remap only open candidates - [`2590243`](https://github.com/morluto/rea/commit/2590243dea5792f86b0e16768a0c1f89a0414579) fix(managed): retain blocked raw-signature choices as unknown ### 📊 Changes **8 files changed** (+1200 additions, -77 deletions) <details> <summary>View changed files</summary> 📝 `docs/managed-code-analysis.md` (+21 -13) 📝 `src/contracts/managed/managedWorkflowToolContracts.ts` (+1 -1) 📝 `src/domain/managed/managedMemberComparisonCoverage.ts` (+2 -2) 📝 `src/domain/managed/managedMemberComparisonItems.ts` (+22 -16) 📝 `src/domain/managed/managedMemberComparisonMatch.ts` (+251 -23) 📝 `tests/acceptance/providers/hopperStartupRecovery.test.ts` (+2 -0) 📝 `tests/boundary/javascript/nativeTypeBindings.test.ts` (+8 -19) 📝 `tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts` (+893 -3) </details> ### 📄 Description ## Summary `compare-managed-members` pairs members of different types. Comparing `System.Collections.dll` from .NET 8.0.31 with 10.0.7 pairs ``LinkedList`1.get_Count`` with ``OrderedDictionary`2.get_Count`` as `unchanged` with `exact` confidence. The real ``LinkedList`1.get_Count`` on the right is left in an ambiguous group. The same file compared with itself gives 573 unchanged and 39 unknown, where all should be unchanged. No issue is linked. #889 raised this as an open question ("Falling through to the exact (type, name, signature) tuple would resolve them without guessing from names alone"). I found it by comparing REA with a System.Reflection.Metadata dump over 208 real comparisons. ## Problem and expected behavior `matchByKeys` in `src/domain/managed/managedMemberComparisonMatch.ts` runs its first round on keys without the declared type or name. For methods the key is the raw signature and normalized CIL. For fields it is the raw signature only. Two things follow: - Identical members of different types land in one group. A group larger than 1:1 becomes ambiguous, and its members are marked used, so the next round, which does include the type and name, never sees them. Nested `Enumerator` types each have a `Dispose` with the same CIL, so a file compared with itself reports them as unknown. - A 1:1 group is paired as exact, even when it joins two different types. The item has no name or type dimension, so the wrong pair reports `unchanged`. ```text $ rea compare-managed-members 8.0.31/System.Collections.dll 10.0.7/System.Collections.dll --format json LinkedList`1.get_Count (0x06000061) -> OrderedDictionary`2.get_Count (0x06000086) unchanged exact-il-signature exact LinkedList`1.get_Count (0x06000047) in an ambiguous group $ rea compare-managed-members 8.0.31/System.Collections.dll 8.0.31/System.Collections.dll --format json summary: unchanged 573, unknown 39 ``` Expected: the CLI description says members are compared "by exact declared type, name, and signature; names alone never form a match". A member should meet its own declared type and name first. Matching a renamed or moved member is a separate, later step. ## Change and scope - Rounds now run in this order. First come the rounds within one declared type and name: exact CIL/signature, then raw signature (unchanged), then structural shape for methods. Then come the name-free remap rounds: exact CIL/signature and structural shape for methods, exact signature for fields. The bases and confidence values are unchanged. - The name-free rounds still remap renamed members. `remaps renamed methods by exact CIL/signature without using names` still passes unchanged. That test renames the type and the method together. - A remap round leaves out any member whose own type and name are still unpaired on the other side. Without this, a member whose own counterpart changed signature is still taken by another type's identical member. Members are left out before grouping, so such a member neither joins an ambiguous group nor makes another remap ambiguous. The sets are taken when the round starts, so group order does not decide anything. - "Unpaired" includes members held in an ambiguous group. A member whose own identity is ambiguous on the other side may still be that member, so it is not remapped. - A remap group that contains a member of an unresolved declaring type (`declaring_type` null) stays ambiguous and does not pair. That member may be another member's own counterpart, so REA cannot tell which pair is right. This is what `main` reports for such a group. - The declared identity always includes the name, with a null type when the type is unresolved. So unresolved members pair only under the same name, and never by CIL or signature alone. - An unresolved member's owner could be any type. So it 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, where its group stays ambiguous. When an ownerless member shares its key with another member on its own side, the resolved members holding that key stay out of the declared rounds as well, so they cannot pair and take the candidate the ownerless member may own (added after review). This is the result `main` gives, because its first round grouped every member with that key. An ownerless member never gets a more confident pair here than on `main`. - Each member's declared identity is computed once. The undecoded-counterpart check now uses the same identity. - The check keys on declared type and name, which overloads share. So a rename is now skipped when an overload that keeps the old name is unpaired on the other side. Example: left `T.Foo(int)`, right `T.Bar(int)` with the same CIL plus a new `T.Foo(): string`. `main` pairs `Foo` with `Bar`. This branch reports `Foo` removed and both right members added. I took this trade because the same check stops the steals in the table below. Reporting a skipped remap as unknown, rather than removed and added, is an option. I left it out to keep this change small. - The limitation strings, the `compare_managed_members` description and `docs/managed-code-analysis.md` now describe this order. - An ambiguous group's limitation now names the evidence its members share: "the same CIL and signature", "the same structural shape", or "the same signature" for fields. This stays true whether the group came from a round keyed with names or without. The `exact-signature` message is unchanged. One existing test asserts the new CIL wording. - `does not guess ambiguous field signature matches` needed one change. On `main`, its two same-signature fields formed one ambiguous group because the first round ignored names. Now `f` pairs with its own `f`. I renamed the right-hand `f` too, so the test still checks that two fields linked only by a shared signature stay ambiguous. Not changed, and yours to decide: - Raw signatures name types by metadata row, and rows are build-local. So a signature that names a type never pairs across builds. That causes most of the removed and added pairs in the counts below. It is a separate bug with its own fix, which I will send as a follow-up PR. It touches the same file. Rebased onto this one, it has five conflict hunks, one per file. Four are wording: docs, tool description, limitation strings, ambiguity messages. One is in the test file, where both PRs append tests and helpers at the end. - The inventory names a nested type by its bare name (documented). Several nested `Enumerator` types therefore share one declared identity. All of the 209 unknowns left in the self-comparisons below are such groups. Naming nested types with their enclosing type would fix them, but that changes the inventory, so I left it. - The `matching.exact_il_signature` and `field_signature` counters count same-identity pairs and remaps together, as they did before. Separating them would add a basis value. - Two ownerless members with the same name and signature, and no other member sharing their key, still pair as they do on `main`. They might belong to two different real types. Refusing that would pair less than `main` does, so I left it. - The undecoded-counterpart check compares declared identities, so it misses an undecoded counterpart whose owner did not resolve. That only shows when ownership fails silently. - The `algorithm` descriptor still reports `name_matching` as `exact-signature-fallback`, with no version marker, although the pairing order changed. Versioning it is a contract change. - A remap across types or names has no dimension that records the move or rename. That is how `main` behaves, but more remaps now carry their own identity, so it is easier to notice. - A name-free remap still carries `exact` confidence, and fields may still remap by signature alone across types. Of the 154 cross-identity pairs left in the cross-build counts, 30 are `exact`. Some are real renames (``<>c__12`2..cctor`` to ``<>c__15`2..cctor``, async state machines renumbered `d__75` to `d__76`). Others are coincidences, such as `HttpConnection._chunked` to `HttpListenerWrappedWebSocket._disposed`. In the `System.Collections` pair above, `BitArray.m_array` still remaps to ``OrderedDictionary`2._buckets``, because `BitArray` left that assembly in .NET 10. Lowering that confidence, or limiting field remaps, would change behavior that a test pins. ## Contract and boundary impact - Semantic owner and earliest changed stage: member pairing in `matchByKeys` (`src/domain/managed/managedMemberComparisonMatch.ts`). - CLI and MCP/tool-catalog contract: no schema change. The `compare_managed_members` description and two limitation strings now give the new round order. Three ambiguity messages are reworded. - Provider, bridge, target-format, or platform compatibility: none. - Evidence, artifact, provenance, or reconstruction contract: the same inputs can produce different pairs. No field, basis, or status value is added or removed. - Process execution, authorization, cleanup, or containment impact: none. - Generated metadata (`docs/public/product-catalog.json`), package, or installation impact: the generated catalog picks up the new description. Nothing generated is committed. ## Evidence and regression coverage - Tests added in `tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts`: - A file whose method and field are copied into a second type pairs with itself: 4 unchanged, 0 ambiguous, every pair keeps its token. - A method whose CIL changed pairs with its own changed counterpart as `exact-signature` / `changed`, beside 1 or 2 identical members of other types, which are reported added. Its unchanged field pairs with its own field. - A method and a field whose signature changed are not remapped to another type's identical member, in either direction. - The same holds for the structural round, with a twin that has the same body shape and a different CIL digest. - A method and field whose signature changed, beside two identical members of other types, are reported removed, not hidden in an ambiguous group. - A method whose own identity is gone remaps to the one candidate whose identity is not kept on the other side, instead of becoming ambiguous. - A method of an unresolved type is not remapped, in either direction. - A method is not remapped to another type's copy while an unresolved member of the same name and CIL may be its counterpart. Nothing is reported added or removed. - Two unresolved members with different names and the same CIL and signature do not pair. - Ownerless members that share a body stay ambiguous in a self-comparison, as on `main`. - An ownerless `Foo` stays ambiguous when the other side holds an ownerless `Foo` and a resolved copy with the same CIL and signature, as on `main`. - An ownerless method stays ambiguous beside a resolved method of the same structural shape. - A resolved `T.Foo` and an ownerless `Foo` with the same CIL and signature, against only the same `T.Foo`, stay one ambiguous group in both directions, as on `main`. Both fail on the commit before the review fix. - A lone ownerless member still pairs with its lone counterpart. - A method whose own identity is held in an ambiguous group on the other side is not remapped to another type's copy, in either direction. - An ambiguous structural remap group names its shared structural shape. Existing ambiguity tests now assert the CIL and field wording. - Base reproduction: on `main` the first 5 behavior cases fail. The self-compare reports 2 ambiguous groups. The changed method pairs with `Other.Twin.Main` as `unchanged` / `exact-il-signature`. The other three report a `matched` cross-type pair. The next 3 behavior cases and the 3 wording assertions also fail on the first version of this branch, each on its assertion. The new unresolved-counterpart case fails on the third version: `Main` remaps to ``Other.Twin.Copy`` and the unresolved `Main` is reported added. The `Renamed` and held-group cases fail on the second version: the unresolved `Main` pairs with `Renamed` as `exact`, and the decoded `Main` remaps to `Other.Twin.Main`. The 3 ownerless cases fail on the fourth version, each as a pair where `main` reports one ambiguous group. I ran those 3 cases against `main`'s matcher too, and they pass. - Each part of the fix is needed. I broke each one separately and ran the managed tests: - Dropping the declared identity from the first method round fails 10 tests. For the field round it fails 6. - Moving the declared signature round after the remaps fails 3. - Not leaving out kept identities fails 5. Leaving them out on one side only fails 5 or 4. - Letting unresolved types into remaps fails 2. - Dropping the declared structural round fails an existing partial-body test. - Each of the three ambiguity messages, reverted, fails 1. - Dropping the name from an unresolved identity fails 2. - Building the held set from one-sided members only fails 1, on either side. - Pairing a remap group that contains an unresolved member fails 2. Checking only one side of the group fails 1. - Letting ownerless members with a shared exact key into the declared rounds fails 2. Letting them into the declared structural round fails 1. Filtering them out of the remap rounds fails 3. Letting the resolved members that share an ownerless member's key into the declared rounds, or keeping them out of the remap rounds, fails the 2 review cases. - User-visible CLI output, built CLI, with the fix: the pair above becomes ``LinkedList`1.get_Count`` (0x06000061) to ``LinkedList`1.get_Count`` (0x06000047), `changed`, `exact-signature`. `System.Collections.dll` compared with itself gives 732 unchanged and 12 unknown. All 12 are nested types that share a bare name. - Real files, built CLI, `main` (79156e5b) against this branch. No source file this PR touches changed between 79156e5b and the current base, 4276aaf1. None of these DLLs has an ownerless member (0 of 277,486 member sides), so the last two commits leave the table as it is. Self-compares are every DLL in `Microsoft.NETCore.App/8.0.31`. Cross-build compares are 40 `Microsoft.NETCore.App` 8.0.31 to 10.0.7 pairs. "Cross-identity" means a matched pair whose declared type or name differs. "Own identity present" means the member's own type and name also appear on the other side of the result. | | 168 self-compares, `main` | with fix | 40 cross-build, `main` | with fix | | --- | --- | --- | --- | --- | | unchanged | 68,043 | 102,583 | 2,054 | 6,688 | | ambiguous items (members on the left) | 4,501 (35,261) | 209 (721) | 848 (7,234) | 322 (1,282) | | files with any unknown | 84 | 39 | 23 | 15 | | cross-identity pairs | 0 | 0 | 490 | 154 | | of those `exact` | 0 | 0 | 263 | 30 | | of those with own identity present | 0 | 0 | 299 | 0 | Without leaving out kept identities, the cross-build column would read 336 cross-identity pairs, 127 exact, and 185 with own identity present. Of the 154 left, 124 are the structural round at `high` confidence. For evidence-bearing changes: - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed - `vitest` over the 11 files that cover managed comparison and the `compare_managed_members` MCP tool: 144 tests passed. - `npm run test:fast`: 394 files, 5,114 tests passed, 3 skipped. - `npm run check:fast`: passed. - `npm run verify:test-discovery`: "Discovered all 915 test files exactly once." - `npm run docs:check`: passed. - Real files: the 208 comparisons in the table, all exit code 0 before and after. I checked the 209 remaining self-compare ambiguous groups against a System.Reflection.Metadata dump. Every group is members of distinct nested types with the same bare type name and member name. ## Compatibility, safety, and release - Breaking changes or migration steps: none. Results for the same inputs can change, toward pairing each member with its own declared identity. - Real Hopper/Ghidra, browser, or OS coverage: Linux x64 (Fedora 44) only. No provider runs. - Package or release metadata impact: none. - Security, privacy, process, or containment review: matching only. One set of identities per remap round, linear in member count. Each identity is hashed once per member. ## Review checklist - [x] This PR addresses a concrete problem or an agreed enhancement. - [x] The PR has one focused outcome and the title follows `type(scope): outcome`. - [x] Related issue is linked, or the reason for not linking one is stated above. - [x] Tests cover changed observable behavior and meaningful failure paths. - [x] Owning docs, contracts, and generated metadata are updated where needed. - [x] User-visible CLI/MCP changes include representative output. - [x] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-31 00:59:55 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#5040
No description provided.