refactor(artifacts): share inspection validation and prune legacy fixtures
This commit is contained in:
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: [],
|
||||
}),
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user