fix(comparison): preserve call procedure endpoint identity

Share endpoint identity and repeated-name ambiguity across call and reference dimensions. Compare same-artifact procedure entries by canonical coordinates while retaining raw reference sites and source Evidence. Keep observed comparison status separate from reconstruction proof.

Fixes #6853
This commit is contained in:
morluto
2026-10-24 02:00:11 +08:00
parent 6911a609f1
commit 80d5c8dc80
6 changed files with 446 additions and 153 deletions
+12 -8
View File
@@ -1186,20 +1186,24 @@ unknown endpoint identity or kind facts remain `unknown`. Independent known
facts that contradict every possible pairing still establish `changed`.
Source Evidence retains the original addresses, names and unavailable facts.
The `calls` dimension uses the same endpoint identity owner. Within an artifact,
it compares procedure entry coordinates rather than display names; references
continue to compare exact source and target coordinates. Across artifacts,
caller and callee names are comparable only when each name identifies one
observed procedure entry per dossier. Procedure entries retained in callers,
callees and reference endpoints all contribute to this ambiguity check.
Repeated rows at the same entry retain their duplicate counts. An unresolved or
ambiguous call endpoint keeps the selected `calls` reconstruction claim unknown;
with resolved call endpoints, distinct entries in the same artifact establish a
changed call set.
Coordinate identity requires a common executable context: artifact identity,
selected image or architecture slice, address space, and applicable load mapping.
Equal coordinates in different slices of one container do not establish equal
endpoints. The current digest-only selection does not enforce that context and
can report references unchanged across distinct slices
can report references and calls unchanged across distinct slices
([#6502](https://github.com/morluto/rea/issues/6502)).
The `calls` dimension separately compares direction and procedure names without
retaining endpoint addresses. Distinct same-named procedures can therefore
compare unchanged ([#6853](https://github.com/morluto/rea/issues/6853)). Check
the source dossiers' endpoint identities before using either affected dimension
as a reconstruction proof; another dimension reporting a change does not repair
the selected dimension's false equality.
### Comparison input shapes
`compare_web_captures` accepts exactly one of two input shapes:
+127 -4
View File
@@ -494,7 +494,10 @@ describe("function collection collation ties", () => {
(name, index) => procedure(`0x${(0x2000 + index).toString(16)}`, name),
);
const canonicalProjection = callees
.map(({ name }) => ({ direction: "out", name }))
.map(({ name }) => ({
direction: "out",
endpoint: { kind: "name", name, ambiguous: false },
}))
.sort((left, right) =>
compareUnicodeCodePoints(canonicalJson(left), canonicalJson(right)),
);
@@ -514,12 +517,14 @@ describe("function collection collation ties", () => {
});
describe("function comparison ordering controls", () => {
const composed = procedure("0x2000", "caf\u00e9");
const decomposed = procedure("0x3000", "cafe\u0301");
it("preserves duplicate Unicode collections and nullable endpoints", () => {
const controlled = (base: "0x1000" | "0x2000", reverse: boolean) => {
const endpoint = base === "0x1000" ? "0x1010" : "0x2010";
const composed = procedure(endpoint, "caf\u00e9");
const decomposed = procedure(
base === "0x1000" ? "0x1020" : "0x2020",
"cafe\u0301",
);
const edges = [
{
source_address: base,
@@ -1390,6 +1395,124 @@ it.each([
},
);
describe("function call endpoint identity", () => {
const calling = (
direction: "callers" | "callees",
entries: readonly ReturnType<typeof procedure>[],
) =>
functionDossierSchema.parse({
...dossier("return helper();", "0x1000"),
[direction]: entries,
});
const calls = (
left: ReturnType<typeof dossier>,
right: ReturnType<typeof dossier>,
sameArtifact: boolean,
) =>
compareFunctions(
functionSnapshotFixture(observe("a", left)),
functionSnapshotFixture(observe(sameArtifact ? "a" : "b", right)),
).dimensions.find(({ dimension }) => dimension === "calls");
it.each(["callers", "callees"] as const)(
"uses known procedure entries rather than %s labels within an artifact",
(direction) => {
const left = calling(direction, [procedure("ram:0x1010", "helper")]);
expect(
calls(
left,
calling(direction, [procedure("ram:0x1020", "helper")]),
true,
),
).toMatchObject({ status: "changed" });
expect(
calls(
left,
calling(direction, [procedure("ram:0X001010", "renamed")]),
true,
),
).toMatchObject({ status: "unchanged" });
expect(
calls(
left,
calling(direction, [procedure("rom:0x1010", "helper")]),
true,
),
).toMatchObject({ status: "changed" });
expect(
calls(
left,
calling(direction, [procedure("unparsed", "helper")]),
true,
),
).toMatchObject({
status: "unknown",
limitations: [expect.stringContaining("could not be parsed")],
});
},
);
it.each(["callers", "callees"] as const)(
"keeps repeated cross-artifact %s labels ambiguous without reference edges",
(direction) => {
const left = calling(direction, [
procedure("0x1010", "helper"),
procedure("0x1020", "helper"),
]);
const right = calling(direction, [
procedure("0x2010", "helper"),
procedure("0x2020", "helper"),
]);
expect(calls(left, right, false)).toMatchObject({
status: "unknown",
left_count: 2,
right_count: 2,
left_digest: null,
right_digest: null,
limitations: [expect.stringContaining("does not uniquely identify")],
});
},
);
it("retains duplicate counts when a name resolves to one entry per artifact", () => {
const left = calling("callees", [
procedure("0x1010", "helper"),
procedure("0X001010", "helper"),
]);
const right = calling("callees", [
procedure("0x2010", "helper"),
procedure("0x2010", "helper"),
]);
expect(calls(left, right, false)).toMatchObject({
status: "unchanged",
left_count: 2,
right_count: 2,
});
expect(
calls(left, calling("callees", [procedure("0x2010", "helper")]), false),
).toMatchObject({ status: "changed" });
});
it("shares procedure-name ambiguity across call and reference observations", () => {
const observed = (base: "0x1000" | "0x2000") =>
functionDossierSchema.parse({
...dossierWithReference(base, "call"),
callees: [procedure(base === "0x1000" ? "0x1020" : "0x2020", "helper")],
});
const result = compareFunctions(
functionSnapshotFixture(observe("a", observed("0x1000"))),
functionSnapshotFixture(observe("b", observed("0x2000"))),
);
for (const dimension of ["calls", "references"])
expect(
result.dimensions.find((value) => value.dimension === dimension),
).toMatchObject({
status: "unknown",
limitations: [expect.stringContaining("does not uniquely identify")],
});
});
});
describe("function comparison unnamed reference endpoints", () => {
it("distinguishes unnamed reference targets by address within one artifact", () => {
const unnamedTarget = (target: string) =>
+44 -12
View File
@@ -1,5 +1,9 @@
import { canonicalJson } from "./comparisonSemantics.js";
import { compareFunctionReferences } from "./functionReferenceComparison.js";
import {
functionEndpointIdentity,
type FunctionEndpointIdentity,
} from "./functionEndpointIdentity.js";
import { diffLines } from "diff";
import type { FunctionSnapshot } from "./functionDossierEvidence.js";
@@ -33,6 +37,7 @@ export const compareDimensions = (
right: FunctionSnapshot,
context: ComparisonContext,
): FunctionDimension[] => {
const endpointIdentity = functionEndpointIdentity(left, right);
return [
compareIdentity(left, right, context),
compareText(
@@ -48,8 +53,8 @@ export const compareDimensions = (
context,
),
compareComments(left, right, context),
compareCalls(left, right, context),
compareReferences(left, right, context),
compareCalls(left, right, context, endpointIdentity),
compareReferences(left, right, context, endpointIdentity),
compareStringsAndNames(left, right, context),
compareCfg(left, right, context),
].map((result) => {
@@ -524,8 +529,9 @@ const compareReferences = (
left: FunctionSnapshot,
right: FunctionSnapshot,
context: ComparisonContext,
identity: FunctionEndpointIdentity,
): FunctionDimension => {
const compared = compareFunctionReferences(left, right);
const compared = compareFunctionReferences(left, right, identity);
if (compared.status === "unknown")
return unresolvedDimension({
dimension: "references",
@@ -572,9 +578,10 @@ const compareCalls = (
left: FunctionSnapshot,
right: FunctionSnapshot,
context: ComparisonContext,
identity: FunctionEndpointIdentity,
): FunctionDimension => {
const leftValues = callProjection(left);
const rightValues = callProjection(right);
const leftValues = callProjection(left, identity);
const rightValues = callProjection(right, identity);
const coverageLimitations = [
...left.callCoverageBlockers.map((reason) => `Left: ${reason}`),
...right.callCoverageBlockers.map((reason) => `Right: ${reason}`),
@@ -591,14 +598,36 @@ const compareCalls = (
rightCount: rightValues.length,
limitations: [...coverageLimitations, ...coverageDiagnostics],
});
if ([...leftValues, ...rightValues].some(({ name }) => isAutoName(name)))
const endpoints = [...leftValues, ...rightValues].map(
({ endpoint }) => endpoint,
);
const unknown = endpoints.filter((endpoint) => endpoint.kind === "unknown");
const ambiguous = endpoints.some(
(endpoint) => endpoint.kind === "name" && endpoint.ambiguous,
);
if (unknown.length > 0 || ambiguous)
return unresolvedDimension({
dimension: "calls",
links: context.links,
leftCount: leftValues.length,
rightCount: rightValues.length,
limitations: [
"Address-derived callee or caller names cannot be matched safely.",
...(unknown.some((endpoint) => endpoint.reason === "unparsed_address")
? [
"Call procedure entry coordinates could not be parsed, so their identity remains unknown.",
]
: []),
...(unknown.some((endpoint) => endpoint.reason === "generated_name")
? ["Address-derived callee or caller names cannot be matched safely."]
: []),
...(unknown.some((endpoint) => endpoint.reason === "missing_name")
? ["Empty callee or caller names cannot be matched safely."]
: []),
...(ambiguous
? [
"A callee or caller name does not uniquely identify an observed procedure entry, so matching names do not establish equal call endpoints.",
]
: []),
...coverageDiagnostics,
],
});
@@ -611,14 +640,17 @@ const compareCalls = (
);
};
const callProjection = (snapshot: FunctionSnapshot) => [
...snapshot.dossier.callers.map(({ name }) => ({
const callProjection = (
snapshot: FunctionSnapshot,
identity: FunctionEndpointIdentity,
) => [
...snapshot.dossier.callers.map((procedure) => ({
direction: "in" as const,
name,
endpoint: identity.endpoint(procedure, procedure.address),
})),
...snapshot.dossier.callees.map(({ name }) => ({
...snapshot.dossier.callees.map((procedure) => ({
direction: "out" as const,
name,
endpoint: identity.endpoint(procedure, procedure.address),
})),
];
+118
View File
@@ -0,0 +1,118 @@
import { canonicalJson } from "./comparisonSemantics.js";
import type { FunctionSnapshot } from "./functionDossierEvidence.js";
import { isAutoName } from "./functionComparisonNormalization.js";
import { nativeAddressInputSchema } from "./native/nativeAddress.js";
type Procedure = FunctionSnapshot["dossier"]["callers"][number];
export type FunctionEndpoint =
| { readonly kind: "address"; readonly address: string }
| {
readonly kind: "name";
readonly name: string;
readonly ambiguous: boolean;
}
| {
readonly kind: "unknown";
readonly reason: "unparsed_address" | "missing_name" | "generated_name";
readonly reported_address: string;
readonly reported_name: string | null;
};
export interface FunctionEndpointIdentity {
readonly sameArtifact: boolean;
readonly endpoint: (
procedure: Procedure | null,
address: string,
) => FunctionEndpoint;
}
/** Observe all retained procedure entries before using names as cross-artifact keys. */
const ambiguousProcedureNames = (
snapshots: readonly FunctionSnapshot[],
): Set<string> => {
const ambiguous = new Set<string>();
for (const snapshot of snapshots) {
const entries = new Map<string, string>();
const observe = (procedure: Procedure | null): void => {
if (procedure === null) return;
const address = nativeAddressInputSchema.safeParse(procedure.address);
if (!address.success) {
ambiguous.add(procedure.name);
return;
}
const previous = entries.get(procedure.name);
if (previous !== undefined && previous !== address.data)
ambiguous.add(procedure.name);
entries.set(procedure.name, address.data);
};
observe(snapshot.dossier.procedure);
for (const procedure of snapshot.dossier.callers) observe(procedure);
for (const procedure of snapshot.dossier.callees) observe(procedure);
for (const edge of snapshot.dossier.incoming_references) {
observe(edge.source_procedure);
observe(edge.target_procedure);
}
for (const edge of snapshot.dossier.outgoing_references) {
observe(edge.source_procedure);
observe(edge.target_procedure);
}
}
return ambiguous;
};
/** Calls use procedure entries; references supply their exact observed site coordinates. */
export const functionEndpointIdentity = (
left: FunctionSnapshot,
right: FunctionSnapshot,
): FunctionEndpointIdentity => {
const sameArtifact =
left.subject.digest.sha256 === right.subject.digest.sha256;
const ambiguousNames = sameArtifact
? new Set<string>()
: ambiguousProcedureNames([left, right]);
return {
sameArtifact,
endpoint: (procedure, address) => {
if (sameArtifact) {
const parsed = nativeAddressInputSchema.safeParse(address);
if (parsed.success) return { kind: "address", address: parsed.data };
return {
kind: "unknown",
reason: "unparsed_address",
reported_address: address,
reported_name: procedure?.name ?? null,
};
}
if (
procedure === null ||
procedure.name.length === 0 ||
isAutoName(procedure.name)
)
return {
kind: "unknown",
reason:
procedure !== null && isAutoName(procedure.name)
? "generated_name"
: "missing_name",
reported_address: address,
reported_name: procedure?.name ?? null,
};
return {
kind: "name",
name: procedure.name,
ambiguous: ambiguousNames.has(procedure.name),
};
},
};
};
/** Ambiguity blocks equality, but a known label can still contradict another label. */
export const functionEndpointKey = (value: FunctionEndpoint): string | null => {
if (value.kind === "unknown") return null;
return canonicalJson(
value.kind === "address"
? { address: value.address }
: { name: value.name },
);
};
+17 -120
View File
@@ -1,35 +1,26 @@
import { canonicalJson } from "./comparisonSemantics.js";
import type { FunctionSnapshot } from "./functionDossierEvidence.js";
import { isAutoName, sorted } from "./functionComparisonNormalization.js";
import { sorted } from "./functionComparisonNormalization.js";
import {
referenceFactsCompatible,
type ReferenceCompatibilityRow,
} from "./functionReferenceCompatibility.js";
import { nativeAddressInputSchema } from "./native/nativeAddress.js";
import {
functionEndpointKey,
type FunctionEndpoint,
type FunctionEndpointIdentity,
} from "./functionEndpointIdentity.js";
type Reference = FunctionSnapshot["dossier"]["outgoing_references"][number];
type KnownKind = Omit<
Extract<Reference["kind"], { available: true }>,
"available" | "provenance"
>;
type Endpoint =
| { readonly kind: "address"; readonly address: string }
| {
readonly kind: "name";
readonly name: string;
readonly ambiguous: boolean;
}
| {
readonly kind: "unknown";
readonly reason: "unparsed_address" | "missing_name" | "generated_name";
readonly reported_address: string;
readonly reported_name: string | null;
};
interface ReferenceFact {
readonly direction: "in" | "out";
readonly source: Endpoint;
readonly target: Endpoint;
readonly source: FunctionEndpoint;
readonly target: FunctionEndpoint;
readonly kind: KnownKind | null;
}
@@ -46,79 +37,9 @@ type ReferenceComparison = {
}
);
/** Names observed at different procedure entries do not uniquely identify an overload. */
const ambiguousProcedureNames = (
snapshots: readonly FunctionSnapshot[],
): Set<string> => {
const ambiguous = new Set<string>();
for (const snapshot of snapshots) {
const entries = new Map<string, string>();
const observe = (procedure: Reference["source_procedure"]): void => {
if (procedure === null) return;
const address = nativeAddressInputSchema.safeParse(procedure.address);
if (!address.success) {
ambiguous.add(procedure.name);
return;
}
const previous = entries.get(procedure.name);
if (previous !== undefined && previous !== address.data)
ambiguous.add(procedure.name);
entries.set(procedure.name, address.data);
};
observe(snapshot.dossier.procedure);
for (const edge of snapshot.dossier.incoming_references) {
observe(edge.source_procedure);
observe(edge.target_procedure);
}
for (const edge of snapshot.dossier.outgoing_references) {
observe(edge.source_procedure);
observe(edge.target_procedure);
}
}
return ambiguous;
};
const endpoint = (
procedure: Reference["source_procedure"],
address: string,
sameArtifact: boolean,
ambiguousNames: ReadonlySet<string>,
): Endpoint => {
if (sameArtifact) {
const parsed = nativeAddressInputSchema.safeParse(address);
if (parsed.success) return { kind: "address", address: parsed.data };
return {
kind: "unknown",
reason: "unparsed_address",
reported_address: address,
reported_name: procedure?.name ?? null,
};
}
if (
procedure === null ||
procedure.name.length === 0 ||
isAutoName(procedure.name)
)
return {
kind: "unknown",
reason:
procedure !== null && isAutoName(procedure.name)
? "generated_name"
: "missing_name",
reported_address: address,
reported_name: procedure?.name ?? null,
};
return {
kind: "name",
name: procedure.name,
ambiguous: ambiguousNames.has(procedure.name),
};
};
const project = (
snapshot: FunctionSnapshot,
sameArtifact: boolean,
ambiguousNames: ReadonlySet<string>,
identity: FunctionEndpointIdentity,
): ReferenceFact[] => {
const facts: ReferenceFact[] = [];
const collect = (
@@ -137,18 +58,8 @@ const project = (
}
facts.push({
direction,
source: endpoint(
edge.source_procedure,
edge.source_address,
sameArtifact,
ambiguousNames,
),
target: endpoint(
edge.target_procedure,
edge.target_address,
sameArtifact,
ambiguousNames,
),
source: identity.endpoint(edge.source_procedure, edge.source_address),
target: identity.endpoint(edge.target_procedure, edge.target_address),
kind,
});
}
@@ -158,23 +69,13 @@ const project = (
return facts;
};
/** Ambiguity blocks equality, but a known label can still contradict another label. */
const endpointKey = (value: Endpoint): string | null => {
if (value.kind === "unknown") return null;
return canonicalJson(
value.kind === "address"
? { address: value.address }
: { name: value.name },
);
};
const constraints = (
facts: readonly ReferenceFact[],
): ReferenceCompatibilityRow[] =>
facts.map((fact) => ({
direction: fact.direction,
source: endpointKey(fact.source),
target: endpointKey(fact.target),
source: functionEndpointKey(fact.source),
target: functionEndpointKey(fact.target),
kind: fact.kind === null ? null : canonicalJson(fact.kind),
}));
@@ -182,14 +83,10 @@ const constraints = (
export const compareFunctionReferences = (
left: FunctionSnapshot,
right: FunctionSnapshot,
identity: FunctionEndpointIdentity,
): ReferenceComparison => {
const sameArtifact =
left.subject.digest.sha256 === right.subject.digest.sha256;
const ambiguousNames = sameArtifact
? new Set<string>()
: ambiguousProcedureNames([left, right]);
const leftFacts = project(left, sameArtifact, ambiguousNames);
const rightFacts = project(right, sameArtifact, ambiguousNames);
const leftFacts = project(left, identity);
const rightFacts = project(right, identity);
let unknownIdentity = false;
let ambiguousIdentity = false;
let unknownKinds = false;
@@ -205,7 +102,7 @@ export const compareFunctionReferences = (
const limitations: string[] = [];
if (unknownIdentity)
limitations.push(
sameArtifact
identity.sameArtifact
? "Reference endpoint coordinates could not be parsed, so their identity remains unknown."
: "Unnamed reference endpoints and generated procedure names have no stable identity across different artifacts.",
);
@@ -90,6 +90,8 @@ const evidence = (
readonly incoming?: readonly Reference[];
readonly procedureAddress?: string;
readonly procedureName?: string;
readonly callers?: readonly ReturnType<typeof procedure>[];
readonly callees?: readonly ReturnType<typeof procedure>[];
} = {},
) =>
createEvidence(
@@ -111,6 +113,8 @@ const evidence = (
},
incoming_references: [...(options.incoming ?? [])],
outgoing_references: [...outgoing],
callers: [...(options.callers ?? dossier.callers)],
callees: [...(options.callees ?? dossier.callees)],
}),
confidence: "derived",
authority: "shipped-artifact",
@@ -159,12 +163,14 @@ const expectReferences = (
)
.toMatchObject({ status });
const verifyReferences = async (
const verifyDimension = async (
client: Client,
comparisonEvidenceId: string,
status: "pass" | "fail" | "unknown",
observedStatus: "unchanged" | "changed" | "unknown",
dimension: "references" | "calls" = "references",
) => {
const claimId = `references-${comparisonEvidenceId}`;
const claimId = `${dimension}-${comparisonEvidenceId}`;
const response = await client.callTool({
name: "verify_reconstruction",
arguments: {
@@ -174,9 +180,9 @@ const verifyReferences = async (
{
kind: "structural-function",
claim_id: claimId,
title: "Reference facts agree",
title: `${dimension} facts agree`,
comparison_evidence_id: comparisonEvidenceId,
dimension: "references",
dimension,
},
],
},
@@ -186,8 +192,6 @@ const verifyReferences = async (
const verification = reconstructionVerificationResultSchema.parse(
parseEvidence(response.structuredContent).normalized_result,
);
const observed =
status === "pass" ? "unchanged" : status === "fail" ? "changed" : "unknown";
expect.soft(verification).toMatchObject({
status,
summary: {
@@ -201,7 +205,7 @@ const verifyReferences = async (
expect.objectContaining({
claim_id: claimId,
status,
observed_status: observed,
observed_status: observedStatus,
}),
],
},
@@ -230,7 +234,12 @@ it("preserves reference identity uncertainty and same-artifact coordinates", asy
);
const namedToNull = await compare(client, named, unnamedElsewhere);
expectReferences(namedToNull, "unknown");
await verifyReferences(client, namedToNull.evidence.evidence_id, "unknown");
await verifyDimension(
client,
namedToNull.evidence.evidence_id,
"unknown",
"unknown",
);
const nullToNamed = await compare(client, unnamedElsewhere, named);
expectReferences(nullToNamed, "unknown");
@@ -346,6 +355,111 @@ it("keeps generated and ambiguous procedure names from proving cross-artifact id
expect.soft(generatedFunctions.result.status).toBe("unknown");
});
it("preserves call endpoint identity through public comparisons and selected reconstruction claims", async () => {
const { client, session } = await createHarness();
const observedCalls = (
id: string,
digest: string,
entry: string,
targets: readonly string[],
includeReferences = true,
) =>
evidence(
id,
digest,
includeReferences
? targets.map((target) =>
reference(entry, target, "call", { sourceName: "worker" }),
)
: [],
{
procedureAddress: entry,
procedureName: "worker",
callees: targets.map((target) => procedure(target, "helper")),
},
);
const original = observedCalls("original-calls", "1", "0x1000", [
"ram:0x2000",
]);
const cases = [
{
left: original,
right: original,
status: "unchanged",
verification: "unknown",
},
{
left: original,
right: observedCalls("distinct-helpers", "1", "0x1100", ["ram:0x3000"]),
status: "changed",
verification: "fail",
},
{
left: original,
right: observedCalls("same-helper-coordinate", "1", "0x1000", [
"ram:0X002000",
]),
status: "unchanged",
verification: "pass",
},
{
left: original,
right: observedCalls("cross-build-helper", "2", "0x1100", ["ram:0x3000"]),
status: "unchanged",
verification: "pass",
},
...[true, false].map((includeReferences) => ({
left: observedCalls(
`duplicate-helpers-left-${includeReferences}`,
"3",
"0x1000",
["0x2000", "0x3000"],
includeReferences,
),
right: observedCalls(
`duplicate-helpers-right-${includeReferences}`,
"4",
"0x1100",
["0x4000", "0x5000"],
includeReferences,
),
status: "unknown" as const,
verification: "unknown" as const,
})),
] as const;
for (const test of cases) {
const compared = await compare(client, test.left, test.right);
expect(
compared.result.dimensions.find((value) => value.dimension === "calls"),
).toMatchObject({
status: test.status,
evidence_links: [test.left.evidence_id, test.right.evidence_id],
...(test.status === "unknown"
? {
left_count: 2,
right_count: 2,
left_digest: null,
right_digest: null,
limitations: [
expect.stringContaining("does not uniquely identify"),
],
}
: {}),
});
await verifyDimension(
client,
compared.evidence.evidence_id,
test.verification,
test.status,
"calls",
);
for (const source of [test.left, test.right])
expect(
session.evidenceById(source.evidence_id)?.normalized_result,
).toEqual(source.normalized_result);
}
});
it("normalizes numeric coordinates but preserves address spaces and source sites", async () => {
const { client } = await createHarness();
const at = (id: string, address: string) =>
@@ -461,7 +575,12 @@ it("keeps known reference changes decisive beside unrelated unknown kinds", asyn
);
const compared = await compare(client, left, right);
expectReferences(compared, "changed");
await verifyReferences(client, compared.evidence.evidence_id, "fail");
await verifyDimension(
client,
compared.evidence.evidence_id,
"fail",
"changed",
);
});
it("keeps ambiguous duplicate edge kinds unknown independent of input order", async () => {