fix(native): attribute deferred Evidence input failures

This commit is contained in:
morluto committed 2026-10-24 02:45:14 +08:00
1 parent 2721650884
commit f8876e5405
5 files changed
+134 -10

No files matched your search

+2
View File
@@ -128,3 +128,5 @@ It includes unchanged/changed code, a byte-identical rename, a removed/added
observation, an inlined function, identical-code aliases and a renamed changed
function with an observed call/string signature. Analyze without executing the
fixture. Full-provider verification still needs caller-supplied Ghidra and Java.
Malformed inline Evidence is rejected after admission with its selected input field and observation index. A structurally valid record whose authenticated contents or digest disagree remains an integrity failure; changing input diagnostics does not make that record trustworthy.
@@ -14,6 +14,8 @@ import { readImmutableEvidenceBundle } from "../EvidenceBundleFiles.js";
import type { EvidenceLookup } from "../EvidenceInputResolver.js";
import type { NativeMatchingAdmission } from "./NativeMatchingAdmission.js";
import { nativeMatchingInputError } from "./NativeMatchingInputError.js";
interface SourceEntry {
readonly file: number;
readonly links: readonly string[];
@@ -61,18 +63,32 @@ export const loadNativeMatchingBundleSources = async ({
const records = new Map<string, Evidence>(),
index = new Map<string, SourceEntry>();
const rootIds: string[] = [];
for (const value of [
input.left,
input.right,
...input.left_observations,
...input.right_observations,
]) {
const selected: readonly (readonly [
unknown,
readonly (string | number)[],
])[] = [
[input.left, ["left"]],
[input.right, ["right"]],
...input.left_observations.map(
(value, index) => [value, ["left_observations", index]] as const,
),
...input.right_observations.map(
(value, index) => [value, ["right_observations", index]] as const,
),
];
for (const [value, path] of selected) {
const parsed = retainedEvidenceReferenceSchema.safeParse(value);
if (parsed.success) rootIds.push(parsed.data.evidence_id);
else {
await admission.admit(value);
const checked = readImmutableEvidence(value);
if (!checked.ok) throw checked.error;
if (!checked.ok)
throw nativeMatchingInputError(
value,
path,
admission.operation,
checked.error,
);
const record = checked.value;
rootIds.push(record.evidence_id);
records.set(record.evidence_id, record);
@@ -0,0 +1,23 @@
import { evidenceInputSchema } from "../../domain/evidenceReference.js";
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
import { projectInputIssues } from "../../domain/inputIssueProjection.js";
/** Attribute malformed caller values after admission; authenticated facts keep integrity semantics. */
export const nativeMatchingInputError = (
input: unknown,
path: readonly (string | number)[],
operation: string,
failure: EvidenceIntegrityError,
): AnalysisInputError | EvidenceIntegrityError => {
const shape = evidenceInputSchema.safeParse(input);
if (shape.success) return failure;
return new AnalysisInputError(
operation,
{ cause: failure },
projectInputIssues(shape.error.issues, input).map((issue) => ({
...issue,
path: [...path, ...issue.path],
})),
);
};
@@ -10,6 +10,8 @@ import {
} from "../EvidenceInputResolver.js";
import type { NativeMatchingAdmission } from "./NativeMatchingAdmission.js";
import { nativeMatchingInputError } from "./NativeMatchingInputError.js";
const INVENTORY_OPERATIONS = ["list_procedures"] as const;
const OBSERVATION_OPERATIONS = [
"list_names",
@@ -32,7 +34,10 @@ export const resolveNativeMatchingSources = async (
readonly sources: readonly Evidence[];
}> => {
const sources = new Map<string, Evidence>();
const resolve = async (input: unknown): Promise<Evidence> => {
const resolve = async (
input: unknown,
path?: readonly (string | number)[],
): Promise<Evidence> => {
const reference = retainedEvidenceReferenceSchema.safeParse(input);
let raw = input;
if (reference.success) {
@@ -42,7 +47,15 @@ export const resolveNativeMatchingSources = async (
}
await admission.admit(raw);
const checked = readEvidence(raw);
if (!checked.ok) throw checked.error;
if (!checked.ok)
throw path !== undefined && !reference.success
? nativeMatchingInputError(
input,
path,
admission.operation,
checked.error,
)
: checked.error;
const evidence = checked.value;
const cached = sources.get(evidence.evidence_id);
if (cached !== undefined) return cached;
@@ -54,7 +67,7 @@ export const resolveNativeMatchingSources = async (
path: readonly (string | number)[],
operations: readonly string[],
): Promise<Evidence> => {
const evidence = await resolve(raw);
const evidence = await resolve(raw, path);
if (
evidence.predicate_type !== "rea.analysis" ||
!operations.includes(evidence.operation) ||
@@ -390,6 +390,76 @@ cliTest(
60000,
);
cliTest(
"native matching retains malformed inline field paths through MCP and CLI source admission",
async ({ cli }) => {
const left = nativeMatchingInventory("1", [
{ address: "0x10", value: "main" },
]);
const right = nativeMatchingInventory("2", [
{ address: "0x20", value: "main" },
]);
const root = await createTestTempDirectory("rea-matching-input-");
const bundlePath = join(root, "sources.json");
await writeFile(
bundlePath,
JSON.stringify(createEvidenceBundle([left, right])),
);
const { call } = await connectLocalToolsMcp();
for (const field of [
"left",
"right",
"left_observations",
"right_observations",
] as const) {
const observation = field.endsWith("observations");
const request = {
left,
right,
detail: "complete",
[field]: observation ? ["not-evidence"] : "not-evidence",
};
const expected = {
code: "invalid_request",
details: {
issues: [
expect.objectContaining({
path: observation ? [field, 0] : [field],
reason: "invalid_type",
}),
],
},
};
expect(
parseMcpToolError(await call("match_native_functions", request)).error,
).toMatchObject(expected);
const requestPath = join(root, "request.json");
await writeFile(requestPath, JSON.stringify(request));
for (const sourceArguments of [[], ["--source_bundle", bundlePath]]) {
const rejected = await cli.run({
arguments: [
"match-native-functions",
requestPath,
...sourceArguments,
"--json",
],
});
expect(rejected.exitCode).not.toBe(0);
expect(rejected.json).toMatchObject(expected);
}
}
const corrected = await call("match_native_functions", {
left,
right,
detail: "complete",
});
expect(corrected.isError).not.toBe(true);
expect(
parseEvidence(corrected.structuredContent).normalized_result,
).toMatchObject({ summary: { matched: 1 } });
},
);
cliTest(
"native matching CLI inline and portable sources report the same validation errors",
async ({ cli }) => {