Files
rea/src/domain/managed/managedMemberComparisonCoverage.ts
T

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;
};