mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
199 lines
8.4 KiB
TypeScript
199 lines
8.4 KiB
TypeScript
import type {
|
|
ManagedMemberComparisonResult,
|
|
ManagedMemberComparisonSide,
|
|
} from "./managedMemberComparison.js";
|
|
import type { ManagedMemberInspection } from "./managedArtifact.js";
|
|
import { hasSignatureResolution } from "./managedMemberSignatureIdentity.js";
|
|
|
|
interface ComparisonCoverageInput {
|
|
readonly left: ManagedMemberInspection;
|
|
readonly right: ManagedMemberInspection;
|
|
}
|
|
|
|
export const buildComparisonCoverage = ({
|
|
left,
|
|
right,
|
|
}: ComparisonCoverageInput): ManagedMemberComparisonResult["coverage"] => {
|
|
const leftStatus = sideCoverageStatus(left);
|
|
const rightStatus = sideCoverageStatus(right);
|
|
const unknownInput =
|
|
left.coverage.state !== "complete" ||
|
|
right.coverage.state !== "complete" ||
|
|
!sameSelection(left, right) ||
|
|
!sameMethodRows(left, right);
|
|
return {
|
|
status:
|
|
!unknownInput && leftStatus === "complete" && rightStatus === "complete"
|
|
? "complete-within-inputs"
|
|
: "partial",
|
|
left_status: leftStatus,
|
|
right_status: rightStatus,
|
|
};
|
|
};
|
|
|
|
/** Full MethodDef coverage is required before inferring cross-build absence. */
|
|
export const hasCompleteMethodRows = (
|
|
result: ManagedMemberInspection,
|
|
): boolean =>
|
|
result.method_rows !== null &&
|
|
result.method_rows.offset === 0 &&
|
|
result.method_rows.count === result.method_rows.total;
|
|
|
|
const sameMethodRows = (
|
|
left: ManagedMemberInspection,
|
|
right: ManagedMemberInspection,
|
|
): boolean =>
|
|
(hasCompleteMethodRows(left) && hasCompleteMethodRows(right)) ||
|
|
(left.method_rows !== null &&
|
|
right.method_rows !== null &&
|
|
left.method_rows.offset === right.method_rows.offset &&
|
|
left.method_rows.count === right.method_rows.count &&
|
|
left.method_rows.total === right.method_rows.total);
|
|
|
|
/** Returned type facts, rather than request spelling, own the observed scope. */
|
|
export const selectedDeclaringTypes = (
|
|
result: ManagedMemberInspection,
|
|
): ReadonlySet<string> | null =>
|
|
result.selection === null
|
|
? null
|
|
: new Set(result.types.map(({ full_name }) => full_name));
|
|
|
|
/** Whether both observations inspected the same declaring types. */
|
|
const sameSelection = (
|
|
left: ManagedMemberInspection,
|
|
right: ManagedMemberInspection,
|
|
): boolean => {
|
|
if (left.selection === null || right.selection === null)
|
|
return left.selection === right.selection;
|
|
const rightNames = new Set(right.selection.type_names);
|
|
return (
|
|
rightNames.size === new Set(left.selection.type_names).size &&
|
|
left.selection.type_names.every((name) => rightNames.has(name))
|
|
);
|
|
};
|
|
|
|
const sideCoverageStatus = (
|
|
result: ManagedMemberInspection,
|
|
): ManagedMemberComparisonResult["coverage"]["left_status"] =>
|
|
result.coverage.state === "unavailable"
|
|
? "unavailable"
|
|
: result.coverage.state === "complete"
|
|
? "complete"
|
|
: "partial";
|
|
|
|
export const sideManifest = (
|
|
side: ManagedMemberComparisonSide,
|
|
): ManagedMemberComparisonResult["left"] => ({
|
|
evidence_id: side.evidenceId,
|
|
artifact_sha256: side.result.artifact.sha256,
|
|
mvid: side.result.module?.mvid ?? null,
|
|
module_name: side.result.module?.name ?? null,
|
|
metadata_status: side.result.metadata.status,
|
|
methods_total: side.result.methods.length,
|
|
fields_total: side.result.fields.length,
|
|
});
|
|
|
|
export const buildComparisonSummary = (
|
|
items: readonly (
|
|
| ManagedMemberComparisonResult["methods"][number]
|
|
| ManagedMemberComparisonResult["fields"][number]
|
|
)[],
|
|
): ManagedMemberComparisonResult["summary"] => ({
|
|
unchanged: items.filter(({ status }) => status === "unchanged").length,
|
|
changed: items.filter(({ status }) => status === "changed").length,
|
|
added: items.filter(({ status }) => status === "added").length,
|
|
removed: items.filter(({ status }) => status === "removed").length,
|
|
unknown: items.filter(({ status }) => status === "unknown").length,
|
|
});
|
|
|
|
export const buildComparisonMatching = (
|
|
methodItems: readonly ManagedMemberComparisonResult["methods"][number][],
|
|
fieldItems: readonly ManagedMemberComparisonResult["fields"][number][],
|
|
): ManagedMemberComparisonResult["matching"] => ({
|
|
exact_il_signature: methodItems.filter(
|
|
({ match }) => match.basis === "exact-il-signature",
|
|
).length,
|
|
exact_signature: [...methodItems, ...fieldItems].filter(
|
|
({ match }) => match.basis === "exact-signature",
|
|
).length,
|
|
structural_method_shape: methodItems.filter(
|
|
({ match }) => match.basis === "structural-method-shape",
|
|
).length,
|
|
field_signature: fieldItems.filter(
|
|
({ match }) => match.basis === "field-signature",
|
|
).length,
|
|
ambiguous:
|
|
methodItems.filter(({ match }) => match.status === "ambiguous").length +
|
|
fieldItems.filter(({ match }) => match.status === "ambiguous").length,
|
|
unmatched: [...methodItems, ...fieldItems].filter(
|
|
({ match }) => match.status === "unmatched",
|
|
).length,
|
|
});
|
|
|
|
export const comparisonLimitations = (
|
|
left: ManagedMemberInspection,
|
|
right: ManagedMemberInspection,
|
|
): string[] => {
|
|
const limitations: string[] = [
|
|
"Metadata tokens are build-local coordinates; matched pairs are remaps, not persistent identities.",
|
|
"Methods pair within one declared type and name by exact CIL/signature, then signature, then structural shape; only then may exact CIL/signature or structural shape alone remap across declared types or names, never taking a member whose own declared type and name remain unpaired on the other side or whose declaring type is unresolved. Names alone are never a matching basis.",
|
|
"Fields pair by declared type, name, and signature, then remap by exact decoded signature alone under the same rule; an undecoded signature is compared only for an exact signature match, and an unmatched member with one stays unknown.",
|
|
];
|
|
const namesTypes = (result: ManagedMemberInspection) =>
|
|
result.methods.some(
|
|
({ signature }) =>
|
|
signature.resolved_sha256 !== null &&
|
|
signature.resolved_sha256 !== signature.raw_sha256,
|
|
) ||
|
|
result.fields.some(
|
|
({ signature }) =>
|
|
signature.resolved_sha256 !== null &&
|
|
signature.resolved_sha256 !== signature.raw_sha256,
|
|
);
|
|
if (
|
|
hasSignatureResolution(left) &&
|
|
hasSignatureResolution(right) &&
|
|
(namesTypes(left) || namesTypes(right))
|
|
)
|
|
limitations.push(
|
|
"Signature type operands compare by namespace, name, and enclosing names only; defining assemblies and CLR type identity are not established by this comparison.",
|
|
);
|
|
if (left.coverage.state !== "complete" || right.coverage.state !== "complete")
|
|
limitations.push("At least one managed member observation is partial.");
|
|
if (!sameSelection(left, right))
|
|
limitations.push(
|
|
"The observations select different declaring types; a member outside the other side's selection is unknown, not added or removed.",
|
|
);
|
|
else if (left.selection !== null)
|
|
limitations.push(
|
|
`Both observations cover only the selected declaring types (${left.selection.type_names.join(", ")}); member totals and statuses describe those types, not the whole assembly.`,
|
|
);
|
|
if (!hasCompleteMethodRows(left) || !hasCompleteMethodRows(right))
|
|
limitations.push(
|
|
"At least one observation lacks a complete MethodDef row inventory; paged or unrecorded method scope cannot establish cross-build method absence. Row intervals are reported in method_rows and describe build-local selections.",
|
|
);
|
|
if (!hasSignatureResolution(left) || !hasSignatureResolution(right))
|
|
limitations.push(
|
|
"At least one managed member observation does not attest signature type-name resolution, so signatures compare by raw bytes, which name types by build-local metadata row.",
|
|
);
|
|
if (left.nesting_read === null || right.nesting_read === null)
|
|
limitations.push(
|
|
"At least one managed member observation predates enclosing-name identity. The legacy bare-name comparison does not establish the enclosing owner of a nested type.",
|
|
);
|
|
if (left.exported_types === null || right.exported_types === null)
|
|
limitations.push(
|
|
"At least one managed member observation did not examine ExportedType rows; its exported and forwarded types are unknown.",
|
|
);
|
|
if (
|
|
(left.exported_types?.length ?? 0) > 0 ||
|
|
(right.exported_types?.length ?? 0) > 0
|
|
)
|
|
limitations.push(
|
|
"ExportedType rows, including type forwarders, are observed but not compared; an unchanged comparison does not establish an unchanged exported or forwarded type set.",
|
|
);
|
|
limitations.push(
|
|
"ldstr literals are not compared; normalized CIL keeps #US tokens rather than decoded text, so an unchanged method does not establish unchanged string literals.",
|
|
);
|
|
return limitations;
|
|
};
|