fix(native): own matching source validation and admission

Separate source corruption and selected-role errors from unexpected derivation failures. Normalize coordinates before body membership and conflict checks, reuse parsed requests, and account for outer observation arrays before validation copies.
This commit is contained in:
morluto committed 2026-10-23 07:27:15 +08:00
1 parent 0281d10130
commit 6b7aa124ed
12 files changed
+947 -282

No files matched your search

@@ -5,6 +5,7 @@ import { nativeFunctionMatchingSchema } from "../../domain/native/functionMatchi
import { createEvidence } from "../../domain/evidence.js";
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
import { functionDossierSchema } from "../../domain/hopperValues.js";
import { procedureInfoSchema } from "../../domain/native/procedureInfo.js";
import {
nativeMatchingInventory,
nativeMatchingDetails,
@@ -628,34 +629,92 @@ it("does not use generated symbols and preserves literal named FUN_ symbols when
);
});
it("rejects tampered, non-inventory and mixed-target observations with typed failures", async () => {
it("assigns source authentication, role selection, context, and lookup failures to their owning boundary", async () => {
const left = nativeMatchingInventory("1", [
{ address: "0x10", value: "main" },
]);
for (const input of [
{ left: { ...left, normalized_result: [] }, right: left },
{ left: nativeMatchingDetails("1", "0x10", "c3")[0], right: left },
const cases = [
{
left,
right: left,
left_observations: nativeMatchingDetails("2", "0x10", "c3"),
input: { left: { ...left, normalized_result: [] }, right: left },
tag: "EvidenceIntegrityError",
},
{
left,
right: left,
left_observations: [
{ kind: "retained-evidence", evidence_id: `ev_${"a".repeat(64)}` },
],
input: { left: nativeMatchingDetails("1", "0x10", "c3")[0], right: left },
tag: "AnalysisInputError",
path: ["left"],
},
]) {
{
input: {
left,
right: left,
left_observations: nativeMatchingDetails("2", "0x10", "c3"),
},
tag: "EvidenceIntegrityError",
},
{
input: {
left: nativeMatchingInventory("1", [
{ address: "not-an-address", value: "main" },
]),
right: left,
},
tag: "EvidenceIntegrityError",
},
{
input: { left, right: left, left_observations: [left] },
tag: "AnalysisInputError",
path: ["left_observations", 0],
},
{
input: {
left,
right: left,
left_observations: [
createEvidence(nativeMatchingTarget("1"), nativeMatchingProvider, {
operation: "procedure_info",
parameters: { procedure: "0x10" },
result: {
...procedureInfoSchema.parse(
nativeMatchingDetails("1", "0x10", "c3")[0]!.normalized_result,
),
entrypoint: "not-an-address",
},
}),
],
},
tag: "EvidenceIntegrityError",
},
{
input: {
left,
right: left,
left_observations: [
{ kind: "retained-evidence", evidence_id: `ev_${"a".repeat(64)}` },
],
},
tag: "EvidenceIntegrityError",
evidenceId: `ev_${"a".repeat(64)}`,
reason: "missing",
},
] as const;
for (const { input, tag, ...expected } of cases) {
const result = await matchNativeFunctions(input);
expect(result.ok).toBe(false);
if (!result.ok)
expect([
"AnalysisInputError",
"EvidenceIntegrityError",
"EvidenceReferenceError",
]).toContain(result.error._tag);
if (!result.ok) {
expect(result.error._tag).toBe(tag);
if ("path" in expected) {
expect(result.error).toMatchObject({
issues: expect.arrayContaining([
expect.objectContaining({ path: expected.path }),
]),
});
}
if ("evidenceId" in expected)
expect(result.error).toMatchObject({
evidenceId: expected.evidenceId,
reason: expected.reason,
});
}
}
});
@@ -705,3 +764,194 @@ it("keeps duplicate candidate storage linear and emits every input occurrence ex
1000,
);
});
it("normalizes accepted body range address spaces before matching byte coverage", async () => {
const body = {
available: true as const,
provenance: "synthetic mixed-space body",
ranges: [
{ start: "ram:0x10", end: "ram:0x10" },
{ start: "other%ab:0x20", end: "other%ab:0x20" },
],
total_bytes: 2,
span_bytes: null,
non_contiguous: true,
contains_entry: true,
};
const info = createEvidence(
nativeMatchingTarget("1"),
nativeMatchingProvider,
{
operation: "procedure_info",
parameters: { procedure: "ram:0x10" },
result: {
name: "main",
entrypoint: "ram:0x10",
basicblock_count: 1,
length: 2,
signature: null,
locals: [],
classification: null,
body,
},
},
);
const read = (address: string, value: string) =>
createEvidence(nativeMatchingTarget("1"), nativeMatchingProvider, {
operation: "read_bytes",
parameters: {},
result: {
address,
requested_bytes: 1,
returned_bytes: 1,
bytes_hex: value,
complete: true,
},
});
const { report } = await compare({
left: nativeMatchingInventory("1", [
{ address: "ram:0x10", value: "main" },
]),
right: nativeMatchingInventory("2", [{ address: "0x30", value: "main" }]),
left_observations: [
info,
read("ram:0x10", "01"),
read("other%AB:0x20", "02"),
],
});
expect(report.matches[0]?.left[0]?.body_sha256).toBe(
createHash("sha256").update(Buffer.from("0102", "hex")).digest("hex"),
);
});
it("compares repeated bodies and entry membership after address-space normalization", async () => {
const procedure = (entrypoint: string, rangeAddress: string) =>
createEvidence(nativeMatchingTarget("1"), nativeMatchingProvider, {
operation: "procedure_info",
parameters: { procedure: entrypoint },
result: {
name: "main",
entrypoint,
basicblock_count: 1,
length: 2,
signature: null,
locals: [],
classification: null,
body: {
available: true,
provenance: "synthetic escaped-space body",
ranges: [
{
start: `${rangeAddress}:0x10`,
end: `${rangeAddress}:0x11`,
},
],
total_bytes: 2,
span_bytes: 2,
non_contiguous: false,
contains_entry: true,
},
},
});
const first = procedure("ram%AB:0x10", "ram%ab");
const equivalent = procedure("ram%ab:0x10", "ram%AB");
const bytes = createEvidence(
nativeMatchingTarget("1"),
nativeMatchingProvider,
{
operation: "read_bytes",
parameters: { address: "ram%AB:0x10", length: 2 },
result: {
address: "ram%AB:0x10",
requested_bytes: 2,
returned_bytes: 2,
bytes_hex: "0102",
complete: true,
},
},
);
const { report } = await compare({
left: nativeMatchingInventory("1", [
{ address: "ram%AB:0x10", value: "main" },
]),
right: nativeMatchingInventory("2", [{ address: "0x20", value: "main" }]),
left_observations: [first, equivalent, bytes],
});
const member = report.matches[0]?.left[0];
expect(member?.body_sha256).toBe(
createHash("sha256").update(Buffer.from("0102", "hex")).digest("hex"),
);
expect(member?.limitations).not.toContain(
"Conflicting procedure observations; body and dossier remain unknown.",
);
expect(member?.evidence_links).toEqual(
expect.arrayContaining([first.evidence_id, equivalent.evidence_id]),
);
});
it("retains zero-byte reads with unavailable addresses when no request coordinate exists", async () => {
const info = createEvidence(
nativeMatchingTarget("1"),
nativeMatchingProvider,
{
operation: "procedure_info",
parameters: { procedure: "0x10" },
result: {
name: "main",
entrypoint: "0x10",
basicblock_count: 1,
length: 1,
signature: null,
locals: [],
classification: null,
body: {
available: true,
provenance: "synthetic body",
ranges: [{ start: "0x10", end: "0x10" }],
total_bytes: 1,
span_bytes: 1,
non_contiguous: false,
contains_entry: true,
},
},
},
);
const emptyRead = createEvidence(
nativeMatchingTarget("1"),
nativeMatchingProvider,
{
operation: "read_bytes",
parameters: {},
result: {
address: "unavailable",
requested_bytes: 1,
returned_bytes: 0,
bytes_hex: "",
complete: false,
},
},
);
const left = nativeMatchingInventory("1", [
{ address: "0x10", value: "main" },
]);
const right = nativeMatchingInventory("2", [
{ address: "0x20", value: "main" },
]);
const result = await matchNativeFunctions({
left,
right,
left_observations: [info, emptyRead],
});
if (!result.ok) throw result.error;
const report = nativeFunctionMatchingSchema.parse(
result.value.evidence.normalized_result,
);
expect(report.matches[0]?.left[0]).toMatchObject({
body_bytes: 1,
body_sha256: null,
});
expect(result.value.evidence.evidence_links).toContain(emptyRead.evidence_id);
expect(result.value.sources.map((source) => source.evidence_id)).toContain(
emptyRead.evidence_id,
);
});
@@ -4,26 +4,19 @@ import { RegularFileCleanupFailure } from "../../filesystem/OwnedRegularFile.js"
import { matchFunctionInventories } from "../../domain/native/matchFunctionInventories.js";
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { setImmediate } from "node:timers/promises";
import { matchNativeFunctionsInputSchema } from "../../contracts/native/functionMatchingContracts.js";
import { retainedEvidenceReferenceSchema } from "../../domain/evidenceReference.js";
import {
createEvidence,
parseEvidence,
type Evidence,
} from "../../domain/evidence.js";
matchNativeFunctionsInputSchema,
type MatchNativeFunctionsInput,
} from "../../contracts/native/functionMatchingContracts.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { readFunctionInventory } from "../../domain/native/functionInventory.js";
import { nativeFunctionMatchingSchema } from "../../domain/native/functionMatching.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisInputError,
} from "../../domain/analysisErrorCore.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../../domain/result.js";
import {
resolveEvidenceInput,
type EvidenceLookup,
} from "../EvidenceInputResolver.js";
import type { EvidenceLookup } from "../EvidenceInputResolver.js";
import { resolveNativeMatchingSources } from "./NativeMatchingSources.js";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import { FUNCTION_COMPARISON_PROVIDER } from "../../domain/comparisonEvidenceIdentity.js";
@@ -31,220 +24,168 @@ const OPERATION = "match_native_functions";
export interface NativeFunctionMatchingResult {
readonly evidence: Evidence;
readonly sources: readonly Evidence[];
readonly detail: MatchNativeFunctionsInput["detail"];
}
/** Pair existing observations through one source/analysis admission. */
export const matchNativeFunctions = (
export const matchNativeFunctions = async (
rawInput: unknown,
options?: ExecutionOptions,
lookup?: EvidenceLookup,
): Promise<Result<NativeFunctionMatchingResult, AnalysisError>> =>
matchWithAdmission(
rawInput,
options,
lookup,
new NativeMatchingAdmission(OPERATION, options?.signal),
);
const matchWithAdmission = async (
rawInput: unknown,
options: ExecutionOptions | undefined,
lookup: EvidenceLookup | undefined,
admission: NativeMatchingAdmission,
): Promise<Result<NativeFunctionMatchingResult, AnalysisError>> => {
const admission = new NativeMatchingAdmission(OPERATION, options?.signal);
try {
const checkpoint = admission.checkpoint;
await setImmediate();
await checkpoint();
await admission.checkpoint();
await admission.admit(rawInput);
const parsed = matchNativeFunctionsInputSchema.safeParse(rawInput);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(OPERATION, parsed.error.issues, rawInput),
);
const sources = new Map<string, Evidence>();
const resolve = async (input: unknown): Promise<Evidence> => {
const reference = retainedEvidenceReferenceSchema.safeParse(input);
let raw = input;
if (reference.success) {
const found = resolveEvidenceInput(reference.data, lookup);
if (!found.ok) throw found.error;
raw = found.value;
}
await admission.admit(raw);
const evidence = parseEvidence(raw);
const cached = sources.get(evidence.evidence_id);
if (cached !== undefined) return cached;
sources.set(evidence.evidence_id, evidence);
return evidence;
};
const left = await resolve(parsed.data.left),
right = await resolve(parsed.data.right);
const observations = async (inputs: readonly unknown[]) => {
const result: Evidence[] = [];
const seen = new Set<string>();
for (const input of inputs) {
const record = await resolve(input);
if (!seen.has(record.evidence_id)) {
seen.add(record.evidence_id);
result.push(record);
}
}
return result;
};
const leftObservations = await observations(parsed.data.left_observations);
const rightObservations = await observations(
parsed.data.right_observations,
);
const directLinks = [...sources.keys()].sort();
// Expand provenance separately from role observations: dependencies may be
// any Evidence operation and must not be interpreted as native body facts.
for (const source of sources.values())
for (const id of source.evidence_links)
if (!sources.has(id))
await resolve({ kind: "retained-evidence", evidence_id: id });
const leftFunctions = await readFunctionInventory(
left,
leftObservations,
checkpoint,
);
const rightFunctions = await readFunctionInventory(
right,
rightObservations,
checkpoint,
);
const compatible =
left.subject?.architecture != null &&
left.subject.architecture === right.subject?.architecture;
const { matches, summary } = await matchFunctionInventories(
leftFunctions,
rightFunctions,
{ identicalBytesComparable: compatible, checkpoint },
);
const limitations = [
...new Set([
...left.limitations.map((x) => `Left: ${x}`),
...right.limitations.map((x) => `Right: ${x}`),
"Coverage is restricted to the supplied provider-observed inventories; missing/truncated inventory facts never prove absence from either binary.",
"Only unique exact symbols and bytes are paired. Compatible observed CFG/call/string fingerprints propose unresolved candidates, never identity or equivalence. Missing structural coverage stays unknown.",
"Name or byte correspondence is not behavioral equivalence; use analyze_function for selected pairs, then compare_functions. Existing dossier pairs include directly usable retained-reference inputs.",
...(!compatible
? [
"Identical-byte matching unavailable: input architectures are missing or differ.",
]
: []),
]),
];
const report = nativeFunctionMatchingSchema.parse({
left: {
evidence_id: left.evidence_id,
sha256: left.subject!.digest.sha256,
functions: leftFunctions.length,
},
right: {
evidence_id: right.evidence_id,
sha256: right.subject!.digest.sha256,
functions: rightFunctions.length,
},
scope: "provider-observed-inputs",
source_coverage: {
status:
summary.only_left + summary.only_right + summary.unresolved > 0
? "partial"
: "complete-within-inputs",
left_inventory_completeness: "unknown",
right_inventory_completeness: "unknown",
left_byte_fingerprints: leftFunctions.filter(
(x) => x.fingerprint !== null,
).length,
left_structural_candidates: leftFunctions.filter(
(x) => x.member.structural_candidate_sha256 !== null,
).length,
right_structural_candidates: rightFunctions.filter(
(x) => x.member.structural_candidate_sha256 !== null,
).length,
right_byte_fingerprints: rightFunctions.filter(
(x) => x.fingerprint !== null,
).length,
},
summary,
matches,
limitations,
});
await checkpoint();
const evidence = createEvidence(undefined, FUNCTION_COMPARISON_PROVIDER, {
operation: OPERATION,
predicateType: "rea.native-function-matching",
confidence: "derived",
authority: "analyst-inference",
parameters: {
left_evidence_id: left.evidence_id,
right_evidence_id: right.evidence_id,
left_observation_ids: leftObservations.map((x) => x.evidence_id),
right_observation_ids: rightObservations.map((x) => x.evidence_id),
algorithm: "exact-symbol-body-bytes-and-structural-candidates-v1",
},
result: report,
limitations,
evidenceLinks: directLinks,
});
await checkpoint();
return ok({ evidence, sources: [...sources.values()] });
return ok(await matchWithAdmission(parsed.data, admission, lookup));
} catch (cause: unknown) {
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
if (cause instanceof AnalysisError) return err(cause);
return err(
new AnalysisInputError(OPERATION, { cause }, [
{
path: [],
reason: "invalid_value",
message:
cause instanceof Error
? cause.message
: "Invalid native matching observations",
},
]),
);
return matchingFailure(cause, options);
}
};
const matchWithAdmission = async (
input: MatchNativeFunctionsInput,
admission: NativeMatchingAdmission,
lookup: EvidenceLookup | undefined,
): Promise<NativeFunctionMatchingResult> => {
const checkpoint = admission.checkpoint;
const {
left,
right,
leftObservations,
rightObservations,
directLinks,
sources,
} = await resolveNativeMatchingSources(input, admission, lookup);
const leftFunctions = await readFunctionInventory(
left,
leftObservations,
checkpoint,
);
const rightFunctions = await readFunctionInventory(
right,
rightObservations,
checkpoint,
);
const compatible =
left.subject?.architecture != null &&
left.subject.architecture === right.subject?.architecture;
const { matches, summary } = await matchFunctionInventories(
leftFunctions,
rightFunctions,
{ identicalBytesComparable: compatible, checkpoint },
);
const limitations = [
...new Set([
...left.limitations.map((x) => `Left: ${x}`),
...right.limitations.map((x) => `Right: ${x}`),
"Coverage is restricted to the supplied provider-observed inventories; missing/truncated inventory facts never prove absence from either binary.",
"Only unique exact symbols and bytes are paired. Compatible observed CFG/call/string fingerprints propose unresolved candidates, never identity or equivalence. Missing structural coverage stays unknown.",
"Name or byte correspondence is not behavioral equivalence; use analyze_function for selected pairs, then compare_functions. Existing dossier pairs include directly usable retained-reference inputs.",
...(!compatible
? [
"Identical-byte matching unavailable: input architectures are missing or differ.",
]
: []),
]),
];
const report = nativeFunctionMatchingSchema.parse({
left: {
evidence_id: left.evidence_id,
sha256: left.subject!.digest.sha256,
functions: leftFunctions.length,
},
right: {
evidence_id: right.evidence_id,
sha256: right.subject!.digest.sha256,
functions: rightFunctions.length,
},
scope: "provider-observed-inputs",
source_coverage: {
status:
summary.only_left + summary.only_right + summary.unresolved > 0
? "partial"
: "complete-within-inputs",
left_inventory_completeness: "unknown",
right_inventory_completeness: "unknown",
left_byte_fingerprints: leftFunctions.filter(
(x) => x.fingerprint !== null,
).length,
left_structural_candidates: leftFunctions.filter(
(x) => x.member.structural_candidate_sha256 !== null,
).length,
right_structural_candidates: rightFunctions.filter(
(x) => x.member.structural_candidate_sha256 !== null,
).length,
right_byte_fingerprints: rightFunctions.filter(
(x) => x.fingerprint !== null,
).length,
},
summary,
matches,
limitations,
});
await checkpoint();
const evidence = createEvidence(undefined, FUNCTION_COMPARISON_PROVIDER, {
operation: OPERATION,
predicateType: "rea.native-function-matching",
confidence: "derived",
authority: "analyst-inference",
parameters: {
left_evidence_id: left.evidence_id,
right_evidence_id: right.evidence_id,
left_observation_ids: leftObservations.map((x) => x.evidence_id),
right_observation_ids: rightObservations.map((x) => x.evidence_id),
algorithm: "exact-symbol-body-bytes-and-structural-candidates-v1",
},
result: report,
limitations,
evidenceLinks: directLinks,
});
await checkpoint();
return { evidence, sources, detail: input.detail };
};
/** Preserve cleanup owners and expected failures; unexpected implementation failures escape. */
const matchingFailure = (
cause: unknown,
options: ExecutionOptions | undefined,
): Result<never, AnalysisError> => {
if (cause instanceof RegularFileCleanupFailure) throw cause;
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
if (cause instanceof AnalysisError) return err(cause);
throw cause;
};
/** Load selected portable records; do not retain unrelated records across bundles. */
export const matchNativeFunctionsFromBundles = async (
input: unknown,
input: MatchNativeFunctionsInput,
paths: readonly string[],
resources: ArtifactResourceScope,
options?: ExecutionOptions,
): Promise<Result<NativeFunctionMatchingResult, AnalysisError>> => {
try {
const parsed = matchNativeFunctionsInputSchema.safeParse(input);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(OPERATION, parsed.error.issues, input),
);
const admission = new NativeMatchingAdmission(OPERATION, options?.signal);
await admission.admit(input);
const loaded = await loadNativeMatchingBundleSources({
input: parsed.data,
input,
paths,
resources,
admission,
});
return await matchWithAdmission(
loaded.input,
options,
loaded.lookup,
admission,
);
await setImmediate();
await admission.checkpoint();
return ok(await matchWithAdmission(loaded.input, admission, loaded.lookup));
} catch (cause: unknown) {
// This marker carries the exact descriptor owner; the caller must finish
// that owner before classifying cancellation or unexpected I/O failures.
if (cause instanceof RegularFileCleanupFailure) throw cause;
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
if (cause instanceof AnalysisError) return err(cause);
return err(new AnalysisInputError(OPERATION, { cause }));
return matchingFailure(cause, options);
}
};
@@ -1,26 +1,25 @@
import { createHash } from "node:crypto";
import type { z } from "zod";
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
import { immutableEvidence, type Evidence } from "../../domain/evidence.js";
import type { Evidence } from "../../domain/evidence.js";
import { readImmutableEvidence } from "../../domain/evidenceValidation.js";
import {
EvidenceIntegrityError,
EvidenceReferenceError,
} from "../../domain/evidenceErrors.js";
import { digestCanonicalValue } from "../../domain/canonicalDigest.js";
import { retainedEvidenceReferenceSchema } from "../../domain/evidenceReference.js";
import type { matchNativeFunctionsInputSchema } from "../../contracts/native/functionMatchingContracts.js";
import type { MatchNativeFunctionsInput } from "../../contracts/native/functionMatchingContracts.js";
import { readImmutableEvidenceBundle } from "../EvidenceBundleFiles.js";
import type { EvidenceLookup } from "../EvidenceInputResolver.js";
import type { NativeMatchingAdmission } from "./NativeMatchingAdmission.js";
type Request = z.output<typeof matchNativeFunctionsInputSchema>;
interface SourceEntry {
readonly file: number;
readonly links: readonly string[];
}
interface Options {
readonly input: Request;
readonly input: MatchNativeFunctionsInput;
readonly paths: readonly string[];
readonly resources: ArtifactResourceScope;
readonly admission: NativeMatchingAdmission;
@@ -55,7 +54,10 @@ export const loadNativeMatchingBundleSources = async ({
paths,
resources,
admission,
}: Options): Promise<{ input: Request; lookup: EvidenceLookup }> => {
}: Options): Promise<{
input: MatchNativeFunctionsInput;
lookup: EvidenceLookup;
}> => {
const records = new Map<string, Evidence>(),
index = new Map<string, SourceEntry>();
const rootIds: string[] = [];
@@ -69,7 +71,9 @@ export const loadNativeMatchingBundleSources = async ({
if (parsed.success) rootIds.push(parsed.data.evidence_id);
else {
await admission.admit(value);
const record = immutableEvidence(value);
const checked = readImmutableEvidence(value);
if (!checked.ok) throw checked.error;
const record = checked.value;
rootIds.push(record.evidence_id);
records.set(record.evidence_id, record);
index.set(record.evidence_id, { file: -1, links: record.evidence_links });
@@ -0,0 +1,112 @@
import type { MatchNativeFunctionsInput } from "../../contracts/native/functionMatchingContracts.js";
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
import type { Evidence } from "../../domain/evidence.js";
import { readEvidence } from "../../domain/evidenceValidation.js";
import { retainedEvidenceReferenceSchema } from "../../domain/evidenceReference.js";
import {
resolveEvidenceInput,
type EvidenceLookup,
} from "../EvidenceInputResolver.js";
import type { NativeMatchingAdmission } from "./NativeMatchingAdmission.js";
const INVENTORY_OPERATIONS = ["list_procedures"] as const;
const OBSERVATION_OPERATIONS = [
"list_names",
"procedure_info",
"read_bytes",
"analyze_function",
] as const;
/** Authenticate selected roles and their closure without interpreting dependencies as observations. */
export const resolveNativeMatchingSources = async (
input: MatchNativeFunctionsInput,
admission: NativeMatchingAdmission,
lookup: EvidenceLookup | undefined,
): Promise<{
readonly left: Evidence;
readonly right: Evidence;
readonly leftObservations: readonly Evidence[];
readonly rightObservations: readonly Evidence[];
readonly directLinks: readonly string[];
readonly sources: readonly Evidence[];
}> => {
const sources = new Map<string, Evidence>();
const resolve = async (input: unknown): Promise<Evidence> => {
const reference = retainedEvidenceReferenceSchema.safeParse(input);
let raw = input;
if (reference.success) {
const found = resolveEvidenceInput(reference.data, lookup);
if (!found.ok) throw found.error;
raw = found.value;
}
await admission.admit(raw);
const checked = readEvidence(raw);
if (!checked.ok) throw checked.error;
const evidence = checked.value;
const cached = sources.get(evidence.evidence_id);
if (cached !== undefined) return cached;
sources.set(evidence.evidence_id, evidence);
return evidence;
};
const select = async (
raw: unknown,
path: readonly (string | number)[],
operations: readonly string[],
): Promise<Evidence> => {
const evidence = await resolve(raw);
if (
evidence.predicate_type !== "rea.analysis" ||
!operations.includes(evidence.operation)
) {
const message = `Expected rea.analysis ${operations.join(" or ")} Evidence, received ${evidence.predicate_type} ${evidence.operation}`;
throw new AnalysisInputError(
admission.operation,
{ userMessage: message },
[{ path, reason: "invalid_value", message }],
);
}
return evidence;
};
const left = await select(input.left, ["left"], INVENTORY_OPERATIONS);
const right = await select(input.right, ["right"], INVENTORY_OPERATIONS);
const observations = async (
inputs: readonly unknown[],
field: "left_observations" | "right_observations",
): Promise<Evidence[]> => {
const result: Evidence[] = [];
const seen = new Set<string>();
for (const [index, input] of inputs.entries()) {
const record = await select(
input,
[field, index],
OBSERVATION_OPERATIONS,
);
if (!seen.has(record.evidence_id)) {
seen.add(record.evidence_id);
result.push(record);
}
}
return result;
};
const leftObservations = await observations(
input.left_observations,
"left_observations",
);
const rightObservations = await observations(
input.right_observations,
"right_observations",
);
const directLinks = [...sources.keys()].sort();
for (const source of sources.values())
for (const id of source.evidence_links)
if (!sources.has(id))
await resolve({ kind: "retained-evidence", evidence_id: id });
return {
left,
right,
leftObservations,
rightObservations,
directLinks,
sources: [...sources.values()],
};
};
@@ -5,6 +5,10 @@ import { evidenceInputSchema } from "../../domain/evidenceReference.js";
// Evidence/reference union without a pre-admission recursive SDK clone.
const input = z.unknown().nonoptional();
input._zod.processJSONSchema = evidenceInputSchema._zod.processJSONSchema;
// Borrow the outer selections too: admission must account for their width before
// any array traversal/copy. The owner authenticates each selected carrier later.
const observations = z.custom<unknown[]>(Array.isArray);
observations._zod.processJSONSchema = z.array(input)._zod.processJSONSchema;
/** Inventories stay portable; optional observations avoid all-function decompilation. */
export const matchNativeFunctionsInputSchema = z.strictObject({
@@ -14,17 +18,19 @@ export const matchNativeFunctionsInputSchema = z.strictObject({
right: input.describe(
"Right list_procedures Evidence or exact retained reference",
),
left_observations: z
.array(input)
left_observations: observations
.default([])
.describe(
"Optional same-context list_names, procedure_info, read_bytes or analyze_function Evidence",
),
right_observations: z
.array(input)
right_observations: observations
.default([])
.describe(
"Optional same-context list_names, procedure_info, read_bytes or analyze_function Evidence",
),
detail: z.enum(["summary", "complete"]).default("summary"),
});
export type MatchNativeFunctionsInput = z.output<
typeof matchNativeFunctionsInputSchema
>;
+14 -2
View File
@@ -1,6 +1,6 @@
import { z } from "zod";
import { parseEvidence, type Evidence } from "./evidence.js";
import { parseEvidence, immutableEvidence, type Evidence } from "./evidence.js";
import { EvidenceIntegrityError } from "./evidenceErrors.js";
import { describeValidationFailure } from "./validationFailure.js";
import { err, ok, type Result } from "./result.js";
@@ -8,9 +8,21 @@ import { err, ok, type Result } from "./result.js";
/** Authenticate one source record, preserving unexpected implementation failures. */
export const readEvidence = (
input: unknown,
): Result<Evidence, EvidenceIntegrityError> =>
authenticateEvidence(input, parseEvidence);
/** Authenticate and seal source records retained independently of mutable input. */
export const readImmutableEvidence = (
input: unknown,
): Result<Evidence, EvidenceIntegrityError> =>
authenticateEvidence(input, immutableEvidence);
const authenticateEvidence = (
input: unknown,
parse: (input: unknown) => Evidence,
): Result<Evidence, EvidenceIntegrityError> => {
try {
return ok(parseEvidence(input));
return ok(parse(input));
} catch (cause: unknown) {
if (!(cause instanceof z.ZodError)) throw cause;
return err(integrityFailure(cause));
+3 -4
View File
@@ -3,7 +3,6 @@ import type { z } from "zod";
import type { functionBodySchema } from "../hopperValues.js";
import type { nativeByteReadSchema } from "./nativeByteRead.js";
import { addressCoordinate } from "./addressCoordinate.js";
import { parseNativeAddress } from "./nativeAddress.js";
export type NativeMatchingCheckpoint = (bytes?: number) => Promise<void>;
interface Interval {
@@ -94,7 +93,7 @@ export class FunctionByteCoverage {
"Cannot append byte observations after fingerprinting",
);
if (read.returned_bytes === 0) return;
const coordinate = addressCoordinate(parseNativeAddress(read.address))!;
const coordinate = addressCoordinate(read.address)!;
let group = this.#pieces.get(coordinate.space);
if (group === undefined) this.#pieces.set(coordinate.space, (group = []));
await this.checkpoint(192 + read.bytes_hex.length * 2);
@@ -162,8 +161,8 @@ export class FunctionByteCoverage {
lengths: string[] = [],
links = new Set<string>();
for (const range of body.ranges) {
const start = addressCoordinate(parseNativeAddress(range.start))!,
end = addressCoordinate(parseNativeAddress(range.end))!;
const start = addressCoordinate(range.start)!,
end = addressCoordinate(range.end)!;
const pieces = this.#pieces.get(start.space) ?? [];
lengths.push(String(end.offset - start.offset + 1n));
let index = firstOverlapping(pieces, start.offset),
+76 -24
View File
@@ -1,7 +1,8 @@
import { functionStructuralCandidate } from "./functionStructuralCandidate.js";
import type { z } from "zod";
import { z } from "zod";
import type { Evidence } from "../evidence.js";
import { EvidenceIntegrityError } from "../evidenceErrors.js";
import { readEvidencePayload } from "../evidenceValidation.js";
import { digestCanonicalValue } from "../canonicalDigest.js";
import {
functionBodySchema,
@@ -13,7 +14,7 @@ import {
addressedValueSchema,
nativeInventoryRowsSchemas,
} from "./nativeInventory.js";
import { parseNativeAddress } from "./nativeAddress.js";
import { nativeAddressInputSchema } from "./nativeAddress.js";
import { nativeByteReadSchema } from "./nativeByteRead.js";
import { procedureInfoSchema } from "./procedureInfo.js";
import type { NativeFunctionMatchMember } from "./functionMatching.js";
@@ -22,15 +23,52 @@ import {
type NativeMatchingCheckpoint,
} from "./functionByteCoverage.js";
const matchingRowsSchema = nativeInventoryRowsSchemas.names.element
.extend({ address: nativeAddressInputSchema })
.array();
const availableBodySchema = functionBodySchema.options[1];
const matchingBodySchema = z.discriminatedUnion("available", [
functionBodySchema.options[0],
availableBodySchema.safeExtend({
ranges: availableBodySchema.shape.ranges.element
.extend({
start: nativeAddressInputSchema,
end: nativeAddressInputSchema,
})
.array(),
}),
]);
const matchingProcedureInfoSchema = procedureInfoSchema
.safeExtend({
entrypoint: nativeAddressInputSchema,
body: matchingBodySchema,
})
.transform(({ entrypoint, body }) => ({ address: entrypoint, body }));
const matchingFunctionDossierSchema = functionDossierSchema.safeExtend({
procedure: functionDossierSchema.shape.procedure.safeExtend({
address: nativeAddressInputSchema,
body: matchingBodySchema,
}),
});
const sourcePayload = <Output>(
schema: z.ZodType<Output>,
input: unknown,
): Output => {
const result = readEvidencePayload(schema, input);
if (!result.ok) throw result.error;
return result.value;
};
type Body = z.infer<typeof functionBodySchema>;
type AddressedValue = z.infer<typeof addressedValueSchema>;
export interface MatchingFunction {
readonly member: NativeFunctionMatchMember;
readonly symbol: string | null;
readonly fingerprint: string | null;
}
interface Detail {
body?: Body;
body: Body;
dossier?: string | null;
candidate?: string | null;
conflict: boolean;
@@ -66,27 +104,30 @@ const requireContext = (inventory: Evidence, observation: Evidence): void => {
const addBody = (observation: Evidence, details: Facts["details"]): void => {
const dossier =
observation.operation === "analyze_function"
? functionDossierSchema.parse(observation.normalized_result)
? sourcePayload(
matchingFunctionDossierSchema,
observation.normalized_result,
)
: undefined;
const procedure =
dossier?.procedure ??
(() => {
const info = procedureInfoSchema.parse(observation.normalized_result);
return { address: info.entrypoint, body: info.body };
})();
const address = parseNativeAddress(procedure.address);
sourcePayload(matchingProcedureInfoSchema, observation.normalized_result);
const address = procedure.address;
if (!functionBodyEntryAgrees(procedure.body, address))
throw new EvidenceIntegrityError(
"Procedure body entry membership contradicts its address",
);
let detail = details.get(address);
if (detail === undefined)
details.set(address, (detail = { conflict: false, links: [] }));
if (
detail.body !== undefined &&
if (detail === undefined) {
details.set(
address,
(detail = { body: procedure.body, conflict: false, links: [] }),
);
} else if (
digestCanonicalValue(detail.body) !== digestCanonicalValue(procedure.body)
)
) {
detail.conflict = true;
}
detail.body = procedure.body;
if (observation.operation === "analyze_function") {
const candidate = functionStructuralCandidate(observation, dossier!);
@@ -106,11 +147,12 @@ const addNames = async (
symbols: Facts["symbols"],
checkpoint: NativeMatchingCheckpoint,
): Promise<void> => {
for (const symbol of nativeInventoryRowsSchemas.names.parse(
for (const symbol of sourcePayload(
matchingRowsSchema,
observation.normalized_result,
)) {
await checkpoint(128 + symbol.value.length * 2);
const address = parseNativeAddress(symbol.address);
const address = symbol.address;
let group = symbols.get(address);
if (group === undefined)
symbols.set(address, (group = { values: [], links: new Set() }));
@@ -122,11 +164,23 @@ const addBytes = async (
observation: Evidence,
bytes: FunctionByteCoverage,
): Promise<void> => {
const read = nativeByteReadSchema.parse(observation.normalized_result);
const parsed = sourcePayload(
nativeByteReadSchema,
observation.normalized_result,
);
const requestAddress =
typeof observation.parameters.address === "string"
? sourcePayload(nativeAddressInputSchema, observation.parameters.address)
: undefined;
const read = {
...parsed,
address:
requestAddress !== undefined || parsed.returned_bytes > 0
? sourcePayload(nativeAddressInputSchema, parsed.address)
: parsed.address,
};
if (
(typeof observation.parameters.address === "string" &&
parseNativeAddress(observation.parameters.address) !==
parseNativeAddress(read.address)) ||
(requestAddress !== undefined && requestAddress !== read.address) ||
(typeof observation.parameters.length === "number" &&
observation.parameters.length !== read.requested_bytes)
)
@@ -176,7 +230,7 @@ const functionFact = async (
facts: Facts,
checkpoint: NativeMatchingCheckpoint,
): Promise<MatchingFunction> => {
const address = parseNativeAddress(row.address),
const address = row.address,
detail = facts.details.get(address),
symbols = facts.symbols.get(address);
// Bound expansion before creating per-occurrence sets, arrays and subsequent
@@ -250,9 +304,7 @@ export const readFunctionInventory = async (
throw new EvidenceIntegrityError(
"Expected artifact-bound list_procedures Evidence",
);
const rows = nativeInventoryRowsSchemas.names.parse(
inventory.normalized_result,
);
const rows = sourcePayload(matchingRowsSchema, inventory.normalized_result);
const facts: Facts = {
details: new Map(),
symbols: new Map(),
-3
View File
@@ -18,6 +18,3 @@ export const nativeAddressInputSchema = z
.replace(/%[0-9a-fA-F]{2}/gu, (escape) => escape.toUpperCase());
return `${space}0x${BigInt(address.slice(separator)).toString(16)}`;
});
export const parseNativeAddress = (input: unknown): string =>
nativeAddressInputSchema.parse(input);
@@ -7,9 +7,7 @@ import { withAdmittedAnalysis } from "./analysisAdmission.js";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import { recordSessionEvidenceSources } from "./sessionEvidence.js";
import { matchNativeFunctionsInputSchema } from "../contracts/native/functionMatchingContracts.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { err, ok } from "../domain/result.js";
import { ok } from "../domain/result.js";
import type { Logger } from "pino";
/** Bind one named contract to the same retained-Evidence workflow used by CLI. */
@@ -26,27 +24,14 @@ export const registerNativeFunctionMatchingTool = (
const handler = createEvidenceToolHandler(
server,
async (input, options) => {
const parsed = matchNativeFunctionsInputSchema.safeParse(input);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(
contract.name,
parsed.error.issues,
input,
),
);
const result = await matchNativeFunctions(
parsed.data,
options,
evidenceById,
);
const result = await matchNativeFunctions(input, options, evidenceById);
if (!result.ok) return result;
const recorded = recordSessionEvidenceSources(
recordEvidence,
result.value.sources,
);
if (!recorded.ok) return recorded;
return parsed.data.detail === "summary"
return result.value.detail === "summary"
? summarizeRetainedAnalysis(result.value.evidence, {
evidenceById,
recordEvidence,
@@ -4,12 +4,15 @@ import { expect } from "vitest";
import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import {
nativeMatchingInventory,
nativeMatchingDetails,
nativeMatchingTarget,
} from "../../fixtures/nativeFunctionMatching.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { nativeMatchingProvider } from "../../fixtures/nativeFunctionMatching.js";
import { procedureInfoSchema } from "../../../src/domain/native/procedureInfo.js";
import {
createEvidenceBundle,
parseEvidenceBundle,
@@ -176,12 +179,15 @@ const verifyPortableCli = async (
});
};
cliTest(
"shared public CLI/MCP matching retains sources, pages exact facts, and feeds selected dossier pairs directly to compare_functions",
async ({ cli }) => {
cliTest.for([{ profile: "full" }, { profile: "compact" }] as const)(
"shared public CLI/MCP matching retains sources, pages exact facts, and feeds selected dossier pairs directly to compare_functions ($profile)",
{ timeout: 60000 },
async ({ profile }, { cli }) => {
const fixture = await createMatchingFixture();
const { left, right, leftDetails, rightDetails, importedPath } = fixture;
const { call } = await connectLocalToolsMcp();
const { call } = await connectLocalToolsMcp({
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
});
const imported = await call("import_evidence_bundle", {
path: importedPath,
});
@@ -272,5 +278,246 @@ cliTest(
});
expect(missing.isError).toBe(true);
},
);
cliTest(
"native matching rejects corrupt and misselected MCP sources without retaining them",
async () => {
const fixture = await createMatchingFixture();
const { left, right, leftDetails, root, importedPath } = fixture;
const { call } = await connectLocalToolsMcp();
const importedSources = await call("import_evidence_bundle", {
path: importedPath,
});
expect(importedSources.isError, JSON.stringify(importedSources)).not.toBe(
true,
);
const before = await call("get_evidence_bundle", {});
const corruptEnvelope = { ...left, normalized_result: [] };
const badCoordinate = nativeMatchingInventory("1", [
{ address: "not-an-address", value: "main" },
]);
const observationResult = procedureInfoSchema.parse(
nativeMatchingDetails("1", "0x10", "c3")[0]!.normalized_result,
);
const corruptObservation = createEvidence(
nativeMatchingTarget("1"),
nativeMatchingProvider,
{
operation: "procedure_info",
parameters: { procedure: "0x10" },
result: {
...observationResult,
entrypoint: "not-an-address",
},
},
);
for (const { request, expected } of [
{
request: { left: corruptEnvelope, right },
expected: { code: "evidence_integrity_mismatch" },
},
{
request: { left: badCoordinate, right },
expected: { code: "evidence_integrity_mismatch" },
},
{
request: { left, right, left_observations: [corruptObservation] },
expected: { code: "evidence_integrity_mismatch" },
},
{
request: { left: leftDetails[0], right },
expected: {
code: "invalid_request",
details: {
issues: [expect.objectContaining({ path: ["left"] })],
},
},
},
]) {
const rejected = await call("match_native_functions", {
...request,
detail: "complete",
});
expect(parseMcpToolError(rejected).error).toMatchObject(expected);
expect((await call("get_evidence_bundle", {})).structuredContent).toEqual(
before.structuredContent,
);
}
const corruptBundlePath = join(root, "corrupt-source.json");
await writeFile(
corruptBundlePath,
JSON.stringify(createEvidenceBundle([badCoordinate, right])),
);
const importedCorruptPayload = await call("import_evidence_bundle", {
path: corruptBundlePath,
});
expect(
importedCorruptPayload.isError,
JSON.stringify(importedCorruptPayload),
).not.toBe(true);
const retainedBefore = await call("get_evidence_bundle", {});
const retainedFailure = await call("match_native_functions", {
left: {
kind: "retained-evidence",
evidence_id: badCoordinate.evidence_id,
},
right: { kind: "retained-evidence", evidence_id: right.evidence_id },
detail: "complete",
});
expect(parseMcpToolError(retainedFailure).error).toMatchObject({
code: "evidence_integrity_mismatch",
details: {
reason: expect.stringContaining("Evidence validation failed"),
},
});
expect((await call("get_evidence_bundle", {})).structuredContent).toEqual(
retainedBefore.structuredContent,
);
const malformedOuterRequest = await call("match_native_functions", {
right,
detail: "complete",
});
expect(parseMcpToolError(malformedOuterRequest).error.code).toBe(
"invalid_request",
);
expect((await call("get_evidence_bundle", {})).structuredContent).toEqual(
retainedBefore.structuredContent,
);
},
60000,
);
cliTest(
"native matching CLI inline and portable sources report the same validation errors",
async ({ cli }) => {
const fixture = await createMatchingFixture();
const { left, right, leftDetails, root } = fixture;
const badCoordinate = nativeMatchingInventory("1", [
{ address: "not-an-address", value: "main" },
]);
const corruptBundlePath = join(root, "corrupt-source.json");
await writeFile(
corruptBundlePath,
JSON.stringify(createEvidenceBundle([badCoordinate, right])),
);
const requestPath = join(root, "wrong-role-request.json");
const portablePath = join(root, "wrong-role-sources.json");
const wrongRole = leftDetails[0]!;
const inlineRequest = { left: wrongRole, right, detail: "complete" };
const portableRequest = {
left: { kind: "retained-evidence", evidence_id: wrongRole.evidence_id },
right: { kind: "retained-evidence", evidence_id: right.evidence_id },
detail: "complete",
};
await writeFile(requestPath, JSON.stringify(inlineRequest));
const inline = await cli.run({
arguments: ["match-native-functions", requestPath, "--json"],
});
await writeFile(
portablePath,
JSON.stringify(createEvidenceBundle([wrongRole, right])),
);
await writeFile(requestPath, JSON.stringify(portableRequest));
const portable = await cli.run({
arguments: [
"match-native-functions",
requestPath,
"--source_bundle",
portablePath,
"--json",
],
});
expect(inline.exitCode).not.toBe(0);
expect(portable.exitCode).toBe(inline.exitCode);
const wrongRoleError = {
code: "invalid_request",
details: {
issues: [expect.objectContaining({ path: ["left"] })],
},
};
expect(inline.json).toMatchObject(wrongRoleError);
expect(portable.json).toMatchObject(wrongRoleError);
const corruptRequest = { left: badCoordinate, right, detail: "complete" };
await writeFile(requestPath, JSON.stringify(corruptRequest));
const corruptInline = await cli.run({
arguments: ["match-native-functions", requestPath, "--json"],
});
const corruptPortableRequest = {
left: {
kind: "retained-evidence",
evidence_id: badCoordinate.evidence_id,
},
right: { kind: "retained-evidence", evidence_id: right.evidence_id },
detail: "complete",
};
await writeFile(requestPath, JSON.stringify(corruptPortableRequest));
const corruptPortable = await cli.run({
arguments: [
"match-native-functions",
requestPath,
"--source_bundle",
corruptBundlePath,
"--json",
],
});
expect(corruptInline.json).toMatchObject({
code: "evidence_integrity_mismatch",
});
expect(corruptPortable.json).toMatchObject({
code: "evidence_integrity_mismatch",
});
const malformedBundlePath = join(root, "malformed-outer-frame.json");
await writeFile(malformedBundlePath, JSON.stringify({ records: [left] }));
const malformedBundle = await cli.run({
arguments: [
"match-native-functions",
requestPath,
"--source_bundle",
malformedBundlePath,
"--json",
],
});
expect(malformedBundle.exitCode).not.toBe(0);
expect(malformedBundle.json).toMatchObject({
code: "evidence_integrity_mismatch",
});
},
60000,
);
cliTest(
"native matching admits the request representation before authenticating observation carriers",
async () => {
const { call } = await connectLocalToolsMcp();
const left = nativeMatchingInventory("1", [
{ address: "0x10", value: "main" },
]);
const right = nativeMatchingInventory("2", [
{ address: "0x20", value: "main" },
]);
const before = await call("get_evidence_bundle", {});
const observations = Array.from({ length: 524288 }, () => ({}));
const response = await call("match_native_functions", {
left,
right,
left_observations: observations,
detail: "complete",
});
expect(parseMcpToolError(response).error).toMatchObject({
code: "resource_constraint",
details: {
operation: "match_native_functions",
resource: "capacity",
},
});
expect((await call("get_evidence_bundle", {})).structuredContent).toEqual(
before.structuredContent,
);
},
60000,
);
@@ -40,6 +40,13 @@ import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../src/contracts/jav
import { javascriptApplicationAnalysisResultSchema } from "../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { javaScriptSemanticQueryResultSchema } from "../../src/domain/javascript/javascriptSemanticQuerySchemas.js";
import { nativeFunctionMatchingSchema } from "../../src/domain/native/functionMatching.js";
import { FunctionByteCoverage } from "../../src/domain/native/functionByteCoverage.js";
import {
nativeMatchingInventory,
nativeMatchingDetails,
} from "../fixtures/nativeFunctionMatching.js";
/** Unexpected implementation failures retain the SDK text surface, without a typed caller error. */
const expectUnexpectedToolFailure = (
response: Awaited<ReturnType<Client["callTool"]>>,
@@ -509,3 +516,56 @@ describe("semantic query output failures through the public SDK", () => {
},
);
});
describe("native matching failure ownership through the public SDK", () => {
afterEach(() => vi.restoreAllMocks());
const matchingInput = () => ({
left: nativeMatchingInventory("1", [{ address: "0x10", value: "main" }]),
right: nativeMatchingInventory("2", [{ address: "0x20", value: "main" }]),
left_observations: nativeMatchingDetails("1", "0x10", "c3"),
right_observations: nativeMatchingDetails("2", "0x20", "c3"),
detail: "complete",
});
it.each(unexpectedResultFailures)(
"preserves an unexpected %s from native matching result validation",
async (_label, createFailure) => {
await withSdk(async ({ client, session }) => {
const input = matchingInput();
const failure = createFailure();
vi.spyOn(nativeFunctionMatchingSchema, "parse").mockImplementation(
() => {
throw failure;
},
);
const response = await client.callTool({
name: "match_native_functions",
arguments: input,
});
expectUnexpectedToolFailure(response, failure);
expect(session.exportEvidenceBundle().records).toEqual([]);
});
},
);
it.each(unexpectedResultFailures)(
"preserves an unexpected %s from native byte fingerprint derivation",
async (_label, createFailure) => {
await withSdk(async ({ client, session }) => {
const input = matchingInput();
const failure = createFailure();
vi.spyOn(
FunctionByteCoverage.prototype,
"fingerprint",
).mockRejectedValue(failure);
const response = await client.callTool({
name: "match_native_functions",
arguments: input,
});
expectUnexpectedToolFailure(response, failure);
expect(session.exportEvidenceBundle().records).toEqual([]);
});
},
);
});