fix(analysis-view): preserve artifact parent schema diagnostics (#6361)

* fix(analysis-view): preserve artifact parent schema diagnostics

* fix(artifacts): locate nested Evidence validation failures
This commit is contained in:
Mohamed Ayoub Eleuch authored and GitHub committed 2026-10-23 01:59:49 +08:00
1 parent 9a426033ff
commit 295e7efddf
4 files changed
+195 -7

No files matched your search

@@ -35,6 +35,7 @@ import {
completeWithinViewCoverage,
pageViewCoverage,
} from "./analysisViewCoverage.js";
import { analysisViewResultInputError } from "./analysisViewParent.js";
const page = {
kind: z.literal("inventory"),
@@ -172,7 +173,7 @@ const parseSource = (
cached.source.inventory.manifest,
);
} catch (cause: unknown) {
return sourceFailure(cause);
return sourceFailure(parent.normalizedResult, cause);
}
return ok(cached.source);
}
@@ -208,13 +209,17 @@ const parseSourceUncached = (
inspection: parsed.inspection,
});
} catch (cause: unknown) {
return sourceFailure(cause);
return sourceFailure(parent.normalizedResult, cause);
}
};
const sourceFailure = (cause: unknown): Result<never, AnalysisError> => {
if (!(cause instanceof TypeError) && !(cause instanceof z.ZodError))
throw cause;
const sourceFailure = (
input: unknown,
cause: unknown,
): Result<never, AnalysisError> => {
if (cause instanceof z.ZodError)
return err(analysisViewResultInputError(input, cause));
if (!(cause instanceof TypeError)) throw cause;
return err(inputError(cause.message));
};
@@ -1,5 +1,6 @@
import { freezeJsonSnapshot } from "./immutableJson.js";
import { expect, it } from "vitest";
import { z } from "zod";
import { ARTIFACT_COMPARISON_EXAMPLE } from "../contracts/artifactComparisonExample.js";
import { createArtifactInspection } from "./artifactInspection.js";
@@ -86,3 +87,28 @@ it("retains the authenticated parent's nested facts without a second inventory c
owned.substeps[0]!.evidence.normalized_result.nodes[0],
);
});
it.each([0, 1])(
"locates a stale nested Evidence ID at substep %i and retains its cause",
(index) => {
const input = structuredClone(inspection);
input.substeps.push(structuredClone(input.substeps[0]!));
input.coverage.substeps_completed = input.substeps.length;
expect(
parseArtifactInspectionInventoryResult(input, target).inventory.complete,
).toBe(true);
input.substeps[index]!.evidence.evidence_id = `ev_${"f".repeat(64)}`;
expect(() =>
parseArtifactInspectionInventoryResult(input, target),
).toThrowError(
expect.objectContaining({
issues: [
expect.objectContaining({
path: ["substeps", index, "evidence", "evidence_id"],
}),
],
cause: expect.any(z.ZodError),
}),
);
},
);
+16 -1
View File
@@ -23,6 +23,7 @@ import {
occurrenceIdForLocation,
} from "./artifactIdentity.js";
import { parseEvidence, type Evidence } from "./evidence.js";
import { z } from "zod";
import {
artifactInspectionResultSchema,
type ArtifactInspectionResult,
@@ -124,7 +125,21 @@ const parseInspectionInventoryPayload = (
);
const sourceEvidence =
immutableJsonSnapshotMember(sealedStep, "evidence") ?? step.evidence;
const parsedEvidence = parseEvidence(sourceEvidence);
let parsedEvidence: Evidence;
try {
parsedEvidence = parseEvidence(sourceEvidence);
} catch (cause: unknown) {
if (!(cause instanceof z.ZodError)) throw cause;
throw Object.assign(
new z.ZodError(
cause.issues.map((issue) => ({
...issue,
path: ["substeps", index, "evidence", ...issue.path],
})),
),
{ cause },
);
}
const evidence = canBorrowParsedJsonSnapshot(sourceEvidence, parsedEvidence)
? sourceEvidence
: parsedEvidence;
+143 -1
View File
@@ -29,7 +29,14 @@ import {
environmentManifestCaptureFixture,
environmentStateCaptureFixture,
} from "../../fixtures/analysisView.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import {
createEvidence,
parseEvidence,
type Evidence,
} from "../../../src/domain/evidence.js";
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
import { z } from "zod";
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { inspectEvidenceBundle } from "../../../src/application/investigation/InspectEvidenceBundle.js";
@@ -294,6 +301,141 @@ cliTest(
},
);
cliTest(
"preserves artifact parent schema diagnostics through the application and CLI",
async ({ cli }) => {
const root = await createTestTempDirectory(
"rea-inventory-diagnostics-cli-",
);
const archive = join(root, "fixture.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("main.js", new TextReader("console.log(1);\n"));
await writeFile(archive, await writer.close());
const inspected = await cli.run({
arguments: ["inspect-artifact", archive, "--json"],
});
expect(inspected.exitCode).toBe(0);
const inspection = parseEvidence(inspected.json);
const inventory = artifactInspectionResultSchema.parse(
inspection.normalized_result,
).substeps[0]!.evidence;
const path = join(root, "view.json");
const reseal = (carrier: Evidence, result: JsonValue) => {
const subject = carrier.subject!;
return createEvidence(
{
path: subject.local_path,
sha256: subject.digest.sha256,
format: subject.format,
},
carrier.provider,
{ operation: carrier.operation, parameters: {}, result },
);
};
for (const [carrier, group, field] of [
[inventory, "manifest", "node_count"],
[inspection, "coverage", "substeps_completed"],
] as const) {
for (const variant of ["valid", "type", "missing"] as const) {
const payload = structuredClone(carrier.normalized_result) as Record<
string,
Record<string, JsonValue>
>;
if (variant === "type") payload[group]![field] = "bad";
if (variant === "missing") delete payload[group]![field];
const evidence = reseal(carrier, payload);
const request = {
source: { kind: "inline" as const, evidence },
view: { kind: "summary" as const },
};
const application = inspectAnalysisView(request);
await writeFile(path, JSON.stringify(request));
const reply = await cli.run({
arguments: ["inspect-analysis-view", path, "--json"],
});
expect(reply.stderr).toBe("");
if (variant === "valid") {
expect(application.ok).toBe(true);
expect(reply.exitCode).toBe(0);
expect(parseEvidence(reply.json).normalized_result).toMatchObject({
kind: "summary",
});
continue;
}
expect(application.ok).toBe(false);
if (application.ok) throw new Error("Expected an invalid parent");
expect(application.error.cause).toBeInstanceOf(z.ZodError);
expect(reply.exitCode).toBe(1);
expect(reply.json).toMatchObject({
code: "invalid_request",
category: "invalid_input",
retryable: false,
recovery: {
kind: "inspect_input_contract",
operation: "inspect_analysis_view",
},
details: {
issues: [
{
path: ["source", "normalized_result", group, field],
reason:
variant === "type" ? "invalid_type" : "missing_argument",
expected: "number",
},
],
},
});
}
}
const nested = artifactInspectionResultSchema.parse(
inspection.normalized_result,
);
nested.substeps.push(structuredClone(nested.substeps[0]!));
nested.coverage.substeps_completed = nested.substeps.length;
nested.substeps[1]!.evidence.evidence_id = `ev_${"f".repeat(64)}`;
const request = {
source: { kind: "inline" as const, evidence: reseal(inspection, nested) },
view: { kind: "summary" as const },
};
const application = inspectAnalysisView(request);
expect(application.ok).toBe(false);
if (application.ok) throw new Error("Expected invalid nested Evidence");
expect(application.error.cause).toMatchObject({
cause: expect.any(z.ZodError),
issues: [
expect.objectContaining({
path: ["substeps", 1, "evidence", "evidence_id"],
}),
],
});
await writeFile(path, JSON.stringify(request));
const refused = await cli.run({
arguments: ["inspect-analysis-view", path, "--json"],
});
expect(refused.exitCode).toBe(1);
expect(refused.stderr).toBe("");
expect(refused.json).toMatchObject({
code: "invalid_request",
details: {
issues: [
{
path: [
"source",
"normalized_result",
"substeps",
1,
"evidence",
"evidence_id",
],
reason: "invalid_value",
message: "Evidence semantic identifier does not match its record",
},
],
},
});
},
);
cliTest(
"admits portable captures before parsing and preserves capture caveats",
async ({ cli }) => {