fix(native): attribute deferred Evidence input failures
This commit is contained in:
1 parent
2721650884
commit
f8876e5405
5 files changed
+134
-10
No files matched your search
@@ -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 }) => {
|
||||
|
||||
Reference in new issue
Block a user