fix(dotnet): keep a decoded member unknown beside an undecoded counterpart
A field that changed from a decodable to an undecoded signature was reported as removed on the decoded side and unknown on the other. The undecoded member with the same declared type and name may be that field with a changed signature, so its decoded counterpart is now unknown too, with a counterpart-signature-not-decoded limitation. The compare_managed_members description now names the exact declared type, name, and raw signature tier and the undecoded handling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d52af3b321
commit
76b70203d9
|
|
@ -284,7 +284,13 @@ Cross-version matching first prefers exact CIL/signature identity. 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.
|
||||
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
|
||||
unmatched member 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. Structural identity
|
||||
|
|
|
|||
|
|
@ -663,9 +663,9 @@
|
|||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "676a0ae5ff16933e3a9b2db0e5a95fcecf57ac926d18ce8b7a31909e040a9972",
|
||||
"tools_sha256": "4d67b3d567f690604e29dae3c76d4bdfc7d9a332745302dd27af328642118c66",
|
||||
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
|
||||
"combined_sha256": "c32a78528d109b2c25582a74c02d25ccb779cb511f6e2477017a2e8805c07bac",
|
||||
"combined_sha256": "ca2f454e044d0095736974fa078383989ab848e420fcebb6127d2c6869c47f1c",
|
||||
"providers_sha256": "67251a90abf28e1cd9020cf68f579bf2460922d90dc113798a391039279db009"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"manifest_id": "ecm_df760a39c0bf1144d883fe0b80e22cc93364d23289b9a9a14d0efff72a1031bb",
|
||||
"manifest_id": "ecm_4e3e927be6545a5328c9e1c523da2504d22f1fae7b3d71ddc6c20e0c6faf34c3",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "e6e0645e84a6514c7d7d3e42d5b0a424bfb0110951eff41471f901de39dbec17"
|
||||
"sha256": "7f689acab3357db8f73aeaf4030e58a1b0c488dfe5a5c3238ce3bb5bcb119bd6"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
|
|||
metadata:
|
||||
version: "26"
|
||||
tool_count: 130
|
||||
catalog_digest: "c32a78528d109b2c25582a74c02d25ccb779cb511f6e2477017a2e8805c07bac"
|
||||
catalog_digest: "ca2f454e044d0095736974fa078383989ab848e420fcebb6127d2c6869c47f1c"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export const MANAGED_WORKFLOW_TOOL_CONTRACTS = [
|
|||
name: "compare_managed_members",
|
||||
...toolContractMetadata("compare_managed_members"),
|
||||
description:
|
||||
"Compare two authenticated inspect_managed_members Evidence records using unique-only decoded-CIL/signature and structural method-shape tiers. Names are reported as observations but are not used as a matching basis; metadata tokens remain build-local coordinates bound to each artifact SHA-256 and MVID, and the tuple digest does not itself remap them.",
|
||||
"Compare two authenticated inspect_managed_members Evidence records using unique-only tiers: exact CIL/signature, exact declared type, name, and raw signature (decoded or not), and decoded structural method shape. Names alone are never a matching basis; a member whose signature was not decoded, or whose same-named counterpart's was not, stays unknown rather than added or removed. Metadata tokens remain build-local coordinates bound to each artifact SHA-256 and MVID, and the tuple digest does not itself remap them.",
|
||||
kind: "application",
|
||||
inputSchema: compareManagedMembersReferenceInputSchema,
|
||||
outputSchema: comparisonOutputSchema,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ interface ComparisonItemContext {
|
|||
readonly rightComplete: boolean;
|
||||
}
|
||||
|
||||
/** Declared type and name of each unmatched member, per side, whose signature was not decoded. */
|
||||
interface UndecodedOneSidedNames {
|
||||
readonly left: ReadonlySet<string>;
|
||||
readonly right: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
const methodDimensions = (
|
||||
left: Method,
|
||||
right: Method,
|
||||
|
|
@ -113,13 +119,15 @@ const unmatchedComparison = (): UnmatchedComparison => ({
|
|||
const methodOnlyItem = (
|
||||
member: OneSided<Method>,
|
||||
context: ComparisonItemContext,
|
||||
undecoded: UndecodedOneSidedNames,
|
||||
): MethodItem => {
|
||||
if (member.left !== undefined) {
|
||||
const limitations = absenceLimitations(
|
||||
member.left,
|
||||
context.rightComplete,
|
||||
"The right method inventory is incomplete, so absence was not observed.",
|
||||
);
|
||||
const limitations = absenceLimitations(member.left, {
|
||||
oppositeComplete: context.rightComplete,
|
||||
oppositeUndecodedNames: undecoded.right,
|
||||
opposite: "right",
|
||||
inventory: "method",
|
||||
});
|
||||
return {
|
||||
item_id: `mmc_method_${sha256({ token: member.left.token, side: "left" })}`,
|
||||
status: limitations.length === 0 ? "removed" : "unknown",
|
||||
|
|
@ -132,11 +140,12 @@ const methodOnlyItem = (
|
|||
};
|
||||
}
|
||||
|
||||
const limitations = absenceLimitations(
|
||||
member.right,
|
||||
context.leftComplete,
|
||||
"The left method inventory is incomplete, so absence was not observed.",
|
||||
);
|
||||
const limitations = absenceLimitations(member.right, {
|
||||
oppositeComplete: context.leftComplete,
|
||||
oppositeUndecodedNames: undecoded.left,
|
||||
opposite: "left",
|
||||
inventory: "method",
|
||||
});
|
||||
return {
|
||||
item_id: `mmc_method_${sha256({ token: member.right.token, side: "right" })}`,
|
||||
status: limitations.length === 0 ? "added" : "unknown",
|
||||
|
|
@ -151,34 +160,66 @@ const methodOnlyItem = (
|
|||
|
||||
/**
|
||||
* Why a one-sided member's absence from the other inventory was not
|
||||
* observed: that inventory is incomplete, or the member's own signature was
|
||||
* not decoded, so no shape round could seek a renamed counterpart.
|
||||
* observed: that inventory is incomplete, the member's own signature was not
|
||||
* decoded, so no shape round could seek a changed counterpart, or the other
|
||||
* inventory has an unmatched same-named member whose signature was not
|
||||
* decoded, which may be this member with a changed signature.
|
||||
*/
|
||||
const absenceLimitations = (
|
||||
member: Method | Field,
|
||||
oppositeComplete: boolean,
|
||||
incomplete: string,
|
||||
opposite: {
|
||||
readonly oppositeComplete: boolean;
|
||||
readonly oppositeUndecodedNames: ReadonlySet<string>;
|
||||
readonly opposite: "left" | "right";
|
||||
readonly inventory: "method" | "field";
|
||||
},
|
||||
): string[] => [
|
||||
...(oppositeComplete
|
||||
...(opposite.oppositeComplete
|
||||
? []
|
||||
: [`unknown-within-incomplete-metadata: ${incomplete}`]),
|
||||
: [
|
||||
`unknown-within-incomplete-metadata: The ${opposite.opposite} ${opposite.inventory} inventory is incomplete, so absence was not observed.`,
|
||||
]),
|
||||
...(member.signature.parse_status === "decoded"
|
||||
? []
|
||||
: [
|
||||
`signature-not-decoded: The member's ${member.signature.parse_status} signature was compared only by exact declared type, name, and raw bytes, so a changed or renamed counterpart could not be sought.`,
|
||||
]),
|
||||
...(member.signature.parse_status === "decoded" &&
|
||||
opposite.oppositeUndecodedNames.has(declaredName(member))
|
||||
? [
|
||||
`counterpart-signature-not-decoded: The ${opposite.opposite} ${opposite.inventory} inventory has an unmatched member with the same declared type and name whose signature was not decoded, so it may be this member with a changed signature.`,
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const declaredName = (member: Method | Field): string =>
|
||||
JSON.stringify([member.declaring_type, member.name]);
|
||||
|
||||
const undecodedOneSidedNames = (members: {
|
||||
readonly leftOnly: readonly { readonly item: Method | Field }[];
|
||||
readonly rightOnly: readonly { readonly item: Method | Field }[];
|
||||
}): UndecodedOneSidedNames => {
|
||||
const names = (side: readonly { readonly item: Method | Field }[]) =>
|
||||
new Set(
|
||||
side
|
||||
.filter(({ item }) => item.signature.parse_status !== "decoded")
|
||||
.map(({ item }) => declaredName(item)),
|
||||
);
|
||||
return { left: names(members.leftOnly), right: names(members.rightOnly) };
|
||||
};
|
||||
|
||||
const fieldOnlyItem = (
|
||||
member: OneSided<Field>,
|
||||
context: ComparisonItemContext,
|
||||
undecoded: UndecodedOneSidedNames,
|
||||
): FieldItem => {
|
||||
if (member.left !== undefined) {
|
||||
const limitations = absenceLimitations(
|
||||
member.left,
|
||||
context.rightComplete,
|
||||
"The right field inventory is incomplete, so absence was not observed.",
|
||||
);
|
||||
const limitations = absenceLimitations(member.left, {
|
||||
oppositeComplete: context.rightComplete,
|
||||
oppositeUndecodedNames: undecoded.right,
|
||||
opposite: "right",
|
||||
inventory: "field",
|
||||
});
|
||||
return {
|
||||
item_id: `mmc_field_${sha256({ token: member.left.token, side: "left" })}`,
|
||||
status: limitations.length === 0 ? "removed" : "unknown",
|
||||
|
|
@ -190,11 +231,12 @@ const fieldOnlyItem = (
|
|||
};
|
||||
}
|
||||
|
||||
const limitations = absenceLimitations(
|
||||
member.right,
|
||||
context.leftComplete,
|
||||
"The left field inventory is incomplete, so absence was not observed.",
|
||||
);
|
||||
const limitations = absenceLimitations(member.right, {
|
||||
oppositeComplete: context.leftComplete,
|
||||
oppositeUndecodedNames: undecoded.left,
|
||||
opposite: "left",
|
||||
inventory: "field",
|
||||
});
|
||||
return {
|
||||
item_id: `mmc_field_${sha256({ token: member.right.token, side: "right" })}`,
|
||||
status: limitations.length === 0 ? "added" : "unknown",
|
||||
|
|
@ -273,10 +315,11 @@ export const buildMethodItems = (
|
|||
],
|
||||
});
|
||||
}
|
||||
const undecoded = undecodedOneSidedNames(matches);
|
||||
for (const item of matches.leftOnly)
|
||||
items.push(methodOnlyItem({ left: item.item }, context));
|
||||
items.push(methodOnlyItem({ left: item.item }, context, undecoded));
|
||||
for (const item of matches.rightOnly)
|
||||
items.push(methodOnlyItem({ right: item.item }, context));
|
||||
items.push(methodOnlyItem({ right: item.item }, context, undecoded));
|
||||
return items;
|
||||
};
|
||||
|
||||
|
|
@ -330,9 +373,10 @@ export const buildFieldItems = (
|
|||
"Multiple managed fields share the same signature; REA did not guess a token remap from names.",
|
||||
],
|
||||
});
|
||||
const undecoded = undecodedOneSidedNames(matches);
|
||||
for (const item of matches.leftOnly)
|
||||
items.push(fieldOnlyItem({ left: item.item }, context));
|
||||
items.push(fieldOnlyItem({ left: item.item }, context, undecoded));
|
||||
for (const item of matches.rightOnly)
|
||||
items.push(fieldOnlyItem({ right: item.item }, context));
|
||||
items.push(fieldOnlyItem({ right: item.item }, context, undecoded));
|
||||
return items;
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -307,6 +307,34 @@ describe("managed member comparison of undecoded signatures", () => {
|
|||
limitations: [expect.stringContaining("signature-not-decoded")],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a decoded member unknown beside a same-named undecoded counterpart", () => {
|
||||
const left = inspect(buildManagedPeFixture(), "/tmp/decoded-before.dll");
|
||||
const right = inspect(
|
||||
buildManagedPeFixture({
|
||||
methodSignature: Buffer.from([0x00, 0x00, 0x7f]),
|
||||
fieldSignature: Buffer.from([0x06, 0x15]),
|
||||
}),
|
||||
"/tmp/undecoded-after.dll",
|
||||
);
|
||||
expect(left.result.methods[0]?.signature.parse_status).toBe("decoded");
|
||||
expect(left.result.fields[0]?.signature.parse_status).toBe("decoded");
|
||||
expect(right.result.coverage.state).toBe("complete");
|
||||
const result = compareManagedMembers(left, right);
|
||||
expect(result.summary).toMatchObject({ added: 0, removed: 0, unknown: 4 });
|
||||
for (const item of [...result.methods, ...result.fields])
|
||||
expect(item).toMatchObject({
|
||||
status: "unknown",
|
||||
match: { status: "unmatched" },
|
||||
limitations: [
|
||||
expect.stringMatching(
|
||||
item.left === null
|
||||
? /^signature-not-decoded:/
|
||||
: /^counterpart-signature-not-decoded: The right (method|field) inventory/,
|
||||
),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const inspect = (bytes: Buffer, path: string) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue