refactor(artifacts): share inspection validation and prune legacy fixtures

This commit is contained in:
morluto committed 2026-10-15 03:43:29 +08:00
1 parent bfd76c5d16
commit 591c87c4b6
5 files changed
+101 -145

No files matched your search

@@ -4,6 +4,7 @@ import { expect, it } from "vitest";
import { functionDossierSchema } from "../hopperValues.js";
import { ghidraFunctionDossier } from "../ghidraValues.fixture.js";
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js";
import { ARTIFACT_COMPARISON_EXAMPLE } from "../../contracts/artifactComparisonExample.js";
import {
analysisViewBindJavaScriptGraphs,
analysisViewJavaScriptAnalysis,
@@ -25,6 +26,7 @@ import {
pageViewCoverage,
} from "./analysisViewCoverage.js";
import { sourceMapReference } from "../javascript/javascriptApplicationGraph.fixture.js";
import { createArtifactInspection } from "../artifactInspection.js";
import { javascriptModulePath } from "./javascriptApplicationView.js";
import {
inspectAnalysisViewInputSchema,
@@ -162,6 +164,28 @@ it("preserves native API limitations and unavailable residual unknowns", () => {
);
});
it("rejects artifact summary views when inspection substep coverage count disagrees", () => {
const source = ARTIFACT_COMPARISON_EXAMPLE.left;
const inspection = createArtifactInspection(source);
const parent: AnalysisViewParent = {
evidenceId: `ev_${"a".repeat(64)}`,
operation: "inspect_artifact",
normalizedResult: {
...inspection,
coverage: {
...inspection.coverage,
substeps_completed: inspection.substeps.length + 1,
},
},
subject: source.subject,
limitations: [],
};
expect(projectAnalysisView(parent, { kind: "summary" })).toMatchObject({
ok: false,
error: { _tag: "AnalysisInputError" },
});
});
it("pages every retained dossier collection and the native API record", () => {
const original = functionDossierSchema.parse(ghidraFunctionDossier());
const page = (
@@ -4,15 +4,11 @@ import type { AnalysisError } from "../analysisErrorBase.js";
import { uniqueSorted } from "../canonicalOrdering.js";
import { err, ok, type Result } from "../result.js";
import {
parseArtifactInventoryEvidence,
parseArtifactInspectionInventoryResult,
parseArtifactInventoryResult,
validateArtifactInventorySubject,
type InventorySet,
} from "../artifactInventoryEvidence.js";
import {
artifactInspectionResultSchema,
type ArtifactInspectionResult,
} from "../artifactInspection.js";
import type { ArtifactInspectionResult } from "../artifactInspection.js";
import type {
AnalysisViewParent,
AnalysisViewRequest,
@@ -52,36 +48,14 @@ const parseSource = (
evidenceIds: [parent.evidenceId],
inspection: null,
});
const inspection = artifactInspectionResultSchema.parse(
const parsed = parseArtifactInspectionInventoryResult(
parent.normalizedResult,
parent.subject ?? null,
);
for (const step of inspection.substeps) {
if (
step.evidence_id !== step.evidence.evidence_id ||
step.evidence.operation !== step.operation
)
throw new TypeError(
"Inspection substep identity differs from its source Evidence",
);
}
const parsed = parseArtifactInventoryEvidence(
inspection.substeps.map(({ evidence }) => evidence),
);
const manifest = parsed.inventory.manifest;
validateArtifactInventorySubject(parent.subject ?? null, manifest);
if (
inspection.subject.manifest_id !== manifest.manifest_id ||
inspection.subject.root_artifact_id !== manifest.root_artifact_id ||
inspection.subject.root_sha256 !== manifest.root_sha256 ||
inspection.subject.root_format !== manifest.root_format
)
throw new TypeError(
"Inspection subject differs from its inventory manifest",
);
return ok({
inventory: parsed.inventory,
evidenceIds: parsed.evidence.map(({ evidence_id }) => evidence_id),
inspection,
evidenceIds: parsed.evidenceIds,
inspection: parsed.inspection,
});
} catch (cause: unknown) {
if (!(cause instanceof TypeError) && !(cause instanceof z.ZodError))
@@ -138,6 +138,7 @@ const inventoryEvidence = (
occurrences: sortedOccurrences,
edges: [],
provenance: [],
integrity_contradictions: [],
coverage: { scope: "container-entries", status: "complete" },
limitations: [],
}),
+64 -33
View File
@@ -49,6 +49,12 @@ export interface ParsedInventorySet {
readonly inventory: InventorySet;
}
interface ParsedArtifactInspectionInventory {
readonly inspection: ArtifactInspectionResult;
readonly inventory: InventorySet;
readonly evidenceIds: readonly string[];
}
/** Parse, merge, and verify inventory Evidence pages. */
export const parseArtifactInventoryEvidence = (
input: unknown,
@@ -61,22 +67,10 @@ export const parseArtifactInventoryEvidence = (
throw new TypeError(
"Artifact comparison requires inspect_artifact Evidence",
);
const inspection = artifactInspectionResultSchema.parse(
return parseInspectionInventoryPayload(
carrier.normalized_result,
);
if (inspection.coverage.substeps_completed !== inspection.substeps.length)
throw new TypeError(
"Artifact inspection substep count does not match its coverage declaration",
);
const inventoryPages = inspection.substeps.map(
parseInspectionInventoryPage,
);
const firstPage = inventoryPages[0];
if (firstPage === undefined)
throw new TypeError("Artifact inspection requires inventory substeps");
validateInspectionIdentity(carrier, inspection, firstPage.manifest);
return inventoryPages;
carrier.subject,
).pages;
});
return {
evidence,
@@ -84,9 +78,58 @@ export const parseArtifactInventoryEvidence = (
};
};
/** Ensure both inspection declarations identify the inventory facts it carries. */
const validateInspectionIdentity = (
carrier: Evidence,
/** Parse and authenticate an already-owned inspection payload and its pages. */
export const parseArtifactInspectionInventoryResult = (
input: unknown,
subject: Evidence["subject"],
): ParsedArtifactInspectionInventory => {
const parsed = parseInspectionInventoryPayload(input, subject);
return {
inspection: parsed.inspection,
inventory: assembleInventoryResults(parsed.pages),
evidenceIds: parsed.evidenceIds,
};
};
/** Shared owner boundary for full Evidence and already-authenticated parents. */
const parseInspectionInventoryPayload = (
input: unknown,
subject: Evidence["subject"],
): {
readonly inspection: ArtifactInspectionResult;
readonly pages: readonly ArtifactInventoryResult[];
readonly evidenceIds: readonly string[];
} => {
const inspection = artifactInspectionResultSchema.parse(input);
if (inspection.coverage.substeps_completed !== inspection.substeps.length)
throw new TypeError(
"Artifact inspection substep count does not match its coverage declaration",
);
const substeps = inspection.substeps.map((step) => {
const evidence = parseEvidence(step.evidence);
if (step.evidence_id !== evidence.evidence_id)
throw new TypeError(
"Artifact inspection substep evidence ID does not match nested Evidence",
);
return {
evidenceId: evidence.evidence_id,
page: parseInventoryEvidencePage(evidence),
};
});
const firstPage = substeps[0]?.page;
if (firstPage === undefined)
throw new TypeError("Artifact inspection requires inventory substeps");
validateInspectionSubject(subject, inspection, firstPage.manifest);
return {
inspection,
pages: substeps.map(({ page }) => page),
evidenceIds: substeps.map(({ evidenceId }) => evidenceId),
};
};
/** Ensure the inspection declaration and carrier identify its inventory. */
const validateInspectionSubject = (
subject: Evidence["subject"],
inspection: ArtifactInspectionResult,
manifest: ArtifactInventoryResult["manifest"],
): void => {
@@ -100,9 +143,9 @@ const validateInspectionIdentity = (
"Artifact inspection subject does not match its inventory manifest",
);
if (
carrier.subject === null ||
carrier.subject.digest.sha256 !== manifest.root_sha256 ||
carrier.subject.format !== manifest.root_format
subject === null ||
subject.digest.sha256 !== manifest.root_sha256 ||
subject.format !== manifest.root_format
)
throw new TypeError(
"Artifact inspection Evidence subject does not match its root manifest",
@@ -138,18 +181,6 @@ const parseInventoryEvidencePage = (
return parseInventoryPage(evidence.normalized_result, evidence.subject);
};
/** Authenticate one inspection substep and parse its inventory exactly once. */
const parseInspectionInventoryPage = (
substep: ArtifactInspectionResult["substeps"][number],
): ArtifactInventoryResult => {
const evidence = parseEvidence(substep.evidence);
if (substep.evidence_id !== evidence.evidence_id)
throw new TypeError(
"Artifact inspection substep evidence ID does not match nested Evidence",
);
return parseInventoryEvidencePage(evidence);
};
/** Both inventory carriers require the same authenticated root identity. */
export const validateArtifactInventorySubject = (
subject: Evidence["subject"],
@@ -18,14 +18,8 @@ import {
parseEvidence,
type Evidence,
} from "../../../src/domain/evidence.js";
import {
artifactInspectionResultSchema,
createArtifactInspection,
} from "../../../src/domain/artifactInspection.js";
import {
artifactInventoryResultSchema,
type ArtifactInventoryResult,
} from "../../../src/domain/artifactGraph.js";
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js";
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
import {
analysisViewResultSchema,
@@ -242,72 +236,6 @@ const createInventoryViewReader = async (client: Client, parent: Evidence) => {
return readView;
};
const assertHistoricalInventory = async (
client: Client,
input: {
archive: string;
parent: Evidence;
inventoryEvidence: Evidence;
graph: ArtifactInventoryResult;
},
) => {
const { archive, parent, inventoryEvidence, graph } = input;
// Historical records may omit a later inventory field. Input parsing must
// not insert defaults inside that authenticated child Evidence.
const {
integrity_contradictions: _contradictions,
coverage: _coverage,
...legacyInventory
} = graph;
const legacyTarget = {
path: archive,
sha256: graph.manifest.root_sha256,
format: "zip" as const,
};
const legacySource = createEvidence(
legacyTarget,
inventoryEvidence.provider,
{
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse(legacyInventory),
},
);
const legacyInspection = createArtifactInspection(legacySource);
expect(legacyInspection.substeps[0]?.evidence).toEqual(legacySource);
expect(
legacyInspection.substeps[0]?.evidence.normalized_result,
).not.toHaveProperty("integrity_contradictions");
expect(
legacyInspection.substeps[0]?.evidence.normalized_result,
).not.toHaveProperty("coverage");
const legacyParent = createEvidence(legacyTarget, parent.provider, {
operation: "inspect_artifact",
parameters: {},
result: legacyInspection,
});
const legacyReply = await client.callTool({
name: "inspect_analysis_view",
arguments: {
source: { kind: "inline", evidence: legacyParent },
view: { kind: "summary" },
},
});
expect(
legacyReply.isError,
JSON.stringify(legacyReply.structuredContent),
).not.toBe(true);
expect(
parseEvidence(legacyReply.structuredContent).normalized_result,
).toMatchObject({
parent_evidence_id: legacyParent.evidence_id,
summary: {
source_complete: true,
inventory_evidence_ids: [legacySource.evidence_id],
},
});
};
const assertPortableRoundtrip = async (
client: Client,
directory: string,
@@ -437,6 +365,10 @@ it("keeps complete ZIP output and reads retained inventory views after the sourc
const graph = artifactInventoryResultSchema.parse(
inventoryEvidence.normalized_result,
);
expect(graph).toMatchObject({
integrity_contradictions: [],
coverage: { scope: "container-entries", status: "complete" },
});
expect(graph.occurrences).toHaveLength(fileCount + 1);
// Follow-ups must use retained source facts, not reopen this archive.
await unlink(archive);
@@ -524,12 +456,6 @@ it("keeps complete ZIP output and reads retained inventory views after the sourc
]),
});
await assertHistoricalInventory(client, {
archive,
parent,
inventoryEvidence,
graph,
});
await assertPortableRoundtrip(client, directory, parent, summary.result);
} finally {
await client.close();