fix(artifacts)!: authenticate complete inspection and summary inputs

BREAKING CHANGE: Artifact inventories require scoped coverage. Obsolete coverage_status and prose-based coverage inference are removed. Inspection carriers authenticate every retained inventory substep.
This commit is contained in:
morluto committed 2026-10-15 03:43:28 +08:00
1 parent db8128dc0d
commit 0405c9d361
22 files changed
+732 -353

No files matched your search

@@ -0,0 +1,166 @@
import { expect, it } from "vitest";
import { inspectAnalysisView } from "../analysisView/AnalysisViewService.js";
import { ARTIFACT_COMPARISON_EXAMPLE } from "../../contracts/artifactComparisonExample.js";
import { artifactInventorySummaryViewResultSchema } from "../../domain/analysisView/analysisView.js";
import {
createEvidence,
parseEvidence,
type Evidence,
} from "../../domain/evidence.js";
import {
EvidenceIntegrityError,
EvidenceReferenceError,
} from "../../domain/evidenceErrors.js";
import { resolveArtifactComparisonEvidence } from "./ArtifactComparisonEvidence.js";
const artifactSummary = () => {
const parent = ARTIFACT_COMPARISON_EXAMPLE.left;
const summary = inspectAnalysisView(
{
source: { kind: "inline", evidence: parent },
view: { kind: "summary" },
},
(evidenceId) => (evidenceId === parent.evidence_id ? parent : undefined),
);
if (!summary.ok) throw summary.error;
return { parent, summary: summary.value };
};
const recreateSummary = (
summary: Evidence,
normalizedResult: Evidence["normalized_result"],
parameters: Evidence["parameters"] = summary.parameters,
): Evidence => {
if (summary.subject === null)
throw new Error("artifact summary fixture must identify its subject");
return createEvidence(
{
path: summary.subject.local_path,
sha256: summary.subject.digest.sha256,
format: summary.subject.format,
...(summary.subject.architecture === null
? {}
: { architecture: summary.subject.architecture }),
},
summary.provider,
{
predicateType: summary.predicate_type,
operation: summary.operation,
parameters,
result: normalizedResult,
...(summary.analysis_profile === null
? {}
: { analysisProfile: summary.analysis_profile }),
rawResult: summary.raw_result,
confidence: summary.confidence,
authority: summary.authority,
environment: summary.environment,
limitations: summary.limitations,
...(summary.relationship_coverage === null
? {}
: { relationshipCoverage: summary.relationship_coverage }),
locations: summary.locations,
evidenceLinks: summary.evidence_links,
},
);
};
it("reports a missing retained parent instead of treating summary facts as inventory", () => {
const { summary } = artifactSummary();
const resolved = resolveArtifactComparisonEvidence(summary, undefined);
expect(resolved.ok).toBe(false);
if (!resolved.ok) {
expect(resolved.error).toBeInstanceOf(EvidenceReferenceError);
expect(resolved.error).toMatchObject({ reason: "missing" });
}
});
it("uses retained Evidence resolution to reject a mismatched parent identity", () => {
const { summary } = artifactSummary();
const otherParent = ARTIFACT_COMPARISON_EXAMPLE.right;
const resolved = resolveArtifactComparisonEvidence(
summary,
() => otherParent,
);
expect(resolved.ok).toBe(false);
if (!resolved.ok) {
expect(resolved.error).toBeInstanceOf(EvidenceReferenceError);
expect(resolved.error).toMatchObject({ reason: "identity_mismatch" });
}
});
it("rejects a summary that declares the wrong parent operation", () => {
const { parent, summary } = artifactSummary();
const summaryResult = artifactInventorySummaryViewResultSchema.parse(
summary.normalized_result,
);
const normalizedResult = {
...summaryResult,
parent_operation: "inspect_artifact",
};
const forged = recreateSummary(summary, normalizedResult);
const resolved = resolveArtifactComparisonEvidence(forged, (evidenceId) =>
evidenceId === parent.evidence_id ? parent : undefined,
);
expect(resolved.ok).toBe(false);
if (!resolved.ok)
expect(resolved.error).toBeInstanceOf(EvidenceIntegrityError);
});
it("rejects summary parameters that do not identify the projected parent", () => {
const { parent, summary } = artifactSummary();
const forged = recreateSummary(summary, summary.normalized_result, {
...summary.parameters,
parent_evidence_id: `ev_${"f".repeat(64)}`,
});
const resolved = resolveArtifactComparisonEvidence(forged, (evidenceId) =>
evidenceId === parent.evidence_id ? parent : undefined,
);
expect(resolved.ok).toBe(false);
if (!resolved.ok)
expect(resolved.error).toBeInstanceOf(EvidenceIntegrityError);
});
it("rejects summary facts that do not reproject from the complete parent", () => {
const { parent, summary } = artifactSummary();
const summaryResult = artifactInventorySummaryViewResultSchema.parse(
summary.normalized_result,
);
const normalizedResult = {
...summaryResult,
summary: {
...summaryResult.summary,
available_counts: {
...summaryResult.summary.available_counts,
nodes: summaryResult.summary.available_counts.nodes + 1,
},
},
};
const forged = recreateSummary(summary, normalizedResult);
const resolved = resolveArtifactComparisonEvidence(forged, (evidenceId) =>
evidenceId === parent.evidence_id ? parent : undefined,
);
expect(resolved.ok).toBe(false);
if (!resolved.ok)
expect(resolved.error).toBeInstanceOf(EvidenceIntegrityError);
});
it("accepts display-only subject changes excluded from Evidence identity", () => {
const { parent, summary } = artifactSummary();
if (summary.subject === null)
throw new Error("artifact summary fixture must identify its subject");
const displayOnly = parseEvidence({
...summary,
subject: {
...summary.subject,
name: "renamed.app",
local_path: "/display-only/renamed.app",
},
});
const resolved = resolveArtifactComparisonEvidence(
displayOnly,
(evidenceId) => (evidenceId === parent.evidence_id ? parent : undefined),
);
expect(resolved).toEqual({ ok: true, value: parent });
});
@@ -0,0 +1,61 @@
import type { EvidenceLookup } from "../EvidenceInputResolver.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
import { inspectAnalysisViewValidated } from "../analysisView/AnalysisViewService.js";
import { artifactInventorySummaryViewResultSchema } from "../../domain/analysisView/analysisView.js";
import {
EvidenceIntegrityError,
EvidenceReferenceError,
} from "../../domain/evidenceErrors.js";
import type { Evidence } from "../../domain/evidence.js";
import { err, ok, type Result } from "../../domain/result.js";
/** Resolve only an artifact summary's exact retained owner before comparison. */
export const resolveArtifactComparisonEvidence = (
evidence: Evidence,
evidenceById: EvidenceLookup | undefined,
): Result<Evidence, AnalysisError> => {
if (evidence.operation !== "inspect_analysis_view") return ok(evidence);
const summary = artifactInventorySummaryViewResultSchema.safeParse(
evidence.normalized_result,
);
if (!summary.success)
return err(
new AnalysisInputError("compare_artifacts", undefined, [
{
path: ["evidence"],
reason: "invalid_value",
message:
"Artifact comparison accepts only an artifact inventory summary from inspect_analysis_view.",
},
]),
);
const parentId = summary.data.parent_evidence_id;
const parent = evidenceById?.(parentId);
if (parent === undefined)
return err(
new EvidenceReferenceError(
parentId,
"missing",
"retained complete artifact inventory Evidence",
null,
),
);
const reprojected = inspectAnalysisViewValidated(
{
source: { kind: "retained-evidence", evidence_id: parentId },
view: { kind: "summary" },
},
(evidenceId) => (evidenceId === parentId ? parent : undefined),
);
if (!reprojected.ok) return reprojected;
if (reprojected.value.evidence_id !== evidence.evidence_id)
return err(
new EvidenceIntegrityError(
"Artifact summary does not match its retained parent inventory.",
),
);
return ok(parent);
};
+4 -4
View File
@@ -1,8 +1,8 @@
import { realpath } from "node:fs/promises";
import {
artifactInventoryOutputSchema,
type ArtifactInventoryOutput,
artifactInventoryResultSchema,
type ArtifactInventoryResult,
} from "../../domain/artifactGraph.js";
import { abortIfNeeded } from "../ArtifactHash.js";
import { scanCanonicalArtifactInventoryInScope } from "./scanCanonical.js";
@@ -13,9 +13,9 @@ import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySn
export const inventoryArtifact = async (
inputPath: string,
options: ArtifactInventoryOptions,
): Promise<ArtifactInventoryOutput> => {
): Promise<ArtifactInventoryResult> => {
const snapshot = await scanArtifactInventory(inputPath, options);
return artifactInventoryOutputSchema.parse(snapshot);
return artifactInventoryResultSchema.parse(snapshot);
};
/** Scan an artifact once and retain the complete immutable graph for projection. */
-1
View File
@@ -289,7 +289,6 @@ const buildInventorySnapshot = async (
provenance: reader?.provenance() ?? [],
integrity_contradictions: integrityContradictions,
coverage,
coverage_status: coverage.status === "complete" ? "complete" : "partial",
limitations: buildLimitations(
[...traversal.limitations, ...edgeResult.limitations],
integrityContradictions,
+32 -24
View File
@@ -1,12 +1,22 @@
import { digestCanonicalValue } from "../domain/canonicalDigest.js";
import {
artifactGraphDigest,
artifactIdForContent,
artifactManifestId,
occurrenceIdForLocation,
} from "../domain/artifactIdentity.js";
import type {
ArtifactEdge,
ArtifactNode,
ArtifactOccurrence,
IntegrityContradiction,
} from "../domain/artifactGraph.js";
import { createEvidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
const inventory = (digit: string) => {
const sha = digit.repeat(64);
const artifactId = `art_${digestCanonicalValue({ sha256: sha }, "Artifact example")}`;
const occurrenceId = `occ_${digestCanonicalValue({ root: artifactId }, "Artifact example")}`;
const nodes = [
const artifactId = artifactIdForContent(sha);
const nodes: ArtifactNode[] = [
{
artifact_id: artifactId,
format: "file",
@@ -18,9 +28,13 @@ const inventory = (digit: string) => {
limitations: [],
},
];
const occurrences = [
const occurrences: ArtifactOccurrence[] = [
{
occurrence_id: occurrenceId,
occurrence_id: occurrenceIdForLocation({
rootArtifactId: artifactId,
logicalPath: ".",
entryKind: "file",
}),
artifact_id: artifactId,
parent_occurrence_id: null,
logical_path: ".",
@@ -36,36 +50,30 @@ const inventory = (digit: string) => {
limitations: [],
},
];
const graphSha256 = digestCanonicalValue(
{
nodes,
occurrences,
edges: [],
integrity_contradictions: [],
},
"Artifact example",
);
const edges: ArtifactEdge[] = [];
const integrityContradictions: IntegrityContradiction[] = [];
const graphSha256 = artifactGraphDigest({
nodes,
occurrences,
edges,
contradictions: integrityContradictions,
});
return jsonValueSchema.parse({
manifest: {
manifest_id: `agm_${digestCanonicalValue(
{
root_artifact_id: artifactId,
graph_sha256: graphSha256,
},
"Artifact example",
)}`,
manifest_id: artifactManifestId(artifactId, graphSha256),
root_artifact_id: artifactId,
root_sha256: sha,
root_format: "file",
graph_sha256: graphSha256,
node_count: nodes.length,
occurrence_count: occurrences.length,
edge_count: 0,
edge_count: edges.length,
},
nodes,
occurrences,
edges: [],
edges,
provenance: [],
coverage: { scope: "root", status: "complete" },
limitations: [],
});
};
+1 -1
View File
@@ -104,7 +104,7 @@ export const SESSION_TOOL_CONTRACTS = [
),
session(
"compare_artifacts",
"Compare complete artifact inventories by logical occurrence path, content identity, metadata, and graph relations. Pass the inventory Evidence nested in each inspect_artifact result as left and right. Every delta cites both inputs, and gaps yield truncated or unknown, never equivalence. Returns every change inline.",
"Compare complete artifact inventories by logical occurrence path, content identity, metadata, and graph relations. Pass the inventory Evidence nested in each inspect_artifact result as left and right, or pass its inspect_analysis_view summary while the complete parent Evidence remains retained in this session. Summary facts are not treated as a complete inventory; comparison resolves the exact retained parent. Every delta cites both complete inputs, and gaps yield truncated or unknown, never equivalence. Returns every change inline.",
artifactComparisonInputSchema,
),
session(
+2 -2
View File
@@ -45,7 +45,7 @@ import {
listArchitecturesSchema,
} from "../domain/native/nativeInspection.js";
import { artifactExtractionResultSchema } from "../domain/artifactGraph.js";
import { artifactInspectionOutputSchema } from "../domain/artifactInspection.js";
import { artifactInspectionResultSchema } from "../domain/artifactInspection.js";
import { interfaceBuilderAnalysisSchema } from "../domain/apple/interfaceBuilderGraph.js";
import { dylibResolutionResultSchema } from "../domain/apple/dylibResolution.js";
import { keyedArchiveResultSchema } from "../domain/apple/keyedArchive.js";
@@ -404,7 +404,7 @@ export const nativeOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
export const artifactOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
inspect_artifact: evidenceResultOf(
z.union([
artifactInspectionOutputSchema,
artifactInspectionResultSchema,
artifactInventorySummaryViewResultSchema,
]),
),
+32
View File
@@ -61,6 +61,7 @@ describe("artifact graph path and provenance fields", () => {
effects: ["read"],
},
],
coverage: { scope: "container-entries", status: "complete" },
integrity_contradictions: [],
limitations: [],
});
@@ -69,4 +70,35 @@ describe("artifact graph path and provenance fields", () => {
expect(parsed.provenance[0]?.arguments).toHaveLength(1_025);
expect(parsed.provenance[0]?.arguments[0]).toBe(arguments_[0]);
});
it("requires current scoped coverage and rejects the obsolete status alias", () => {
const inventory = {
manifest: {
manifest_id: `agm_${"b".repeat(64)}`,
root_artifact_id: `art_${"a".repeat(64)}`,
root_sha256: "c".repeat(64),
root_format: "plist",
graph_sha256: "d".repeat(64),
node_count: 1,
occurrence_count: 1,
edge_count: 0,
},
nodes: [],
occurrences: [],
edges: [],
provenance: [],
integrity_contradictions: [],
limitations: [],
};
expect(artifactInventoryResultSchema.safeParse(inventory).success).toBe(
false,
);
expect(
artifactInventoryResultSchema.safeParse({
...inventory,
coverage_status: "complete",
}).success,
).toBe(false);
});
});
+3 -17
View File
@@ -196,32 +196,18 @@ export type ArtifactInventoryCoverage = z.infer<
typeof artifactInventoryCoverageSchema
>;
/** Complete inventory observation containing the stable artifact graph. */
export const artifactInventoryResultSchema = z.object({
/** Current inventory observation containing the stable artifact graph. */
export const artifactInventoryResultSchema = z.strictObject({
manifest: artifactGraphManifestSchema,
nodes: z.array(artifactNodeSchema),
occurrences: z.array(artifactOccurrenceSchema),
edges: z.array(artifactEdgeSchema),
provenance: z.array(artifactCommandSchema),
integrity_contradictions: z.array(integrityContradictionSchema).default([]),
// Omitted by older retained Evidence; the inventory parser owns its compatibility policy.
coverage: artifactInventoryCoverageSchema.optional(),
coverage: artifactInventoryCoverageSchema,
limitations: z.array(z.string()),
/** Optional only for persisted inventory Evidence written before this field. */
coverage_status: z.enum(["complete", "partial"]).optional(),
});
/** Current producers always declare coverage; historical Evidence inputs may omit it. */
export const artifactInventoryOutputSchema =
artifactInventoryResultSchema.safeExtend({
coverage: artifactInventoryCoverageSchema,
coverage_status:
artifactInventoryResultSchema.shape.coverage_status.unwrap(),
});
export type ArtifactInventoryOutput = z.infer<
typeof artifactInventoryOutputSchema
>;
/** One safely materialized artifact beneath caller-approved output root. */
const extractedArtifactSchema = z.object({
artifact_id: artifactIdSchema,
+5 -21
View File
@@ -1,7 +1,6 @@
import { z } from "zod";
import {
artifactInventoryOutputSchema,
artifactInventoryResultSchema,
type ArtifactInventoryResult,
} from "./artifactGraph.js";
@@ -75,7 +74,10 @@ export const artifactInspectionResultSchema = z.strictObject({
operation: z.literal("inventory_artifact"),
status: z.literal("completed"),
evidence_id: evidenceIdSchema,
evidence: evidenceSchema,
evidence: evidenceSchema.safeExtend({
operation: z.literal("inventory_artifact"),
normalized_result: artifactInventoryResultSchema,
}),
}),
)
.min(1),
@@ -86,7 +88,7 @@ export const artifactInspectionResultSchema = z.strictObject({
next_probes: z.array(nextProbeSchema),
coverage: z.strictObject({
status: z.enum(["complete-within-substeps", "partial", "truncated"]),
substeps_completed: z.literal(1),
substeps_completed: z.number().int().min(1),
observations_retained: z.number().int().min(0),
observations_omitted: z.number().int().min(0),
hypotheses_retained: z.number().int().min(0),
@@ -104,24 +106,6 @@ export type ArtifactInspectionResult = z.infer<
typeof artifactInspectionResultSchema
>;
/**
* The current producer guarantees a concrete inventory payload. Specialize
* its output without narrowing the historical inspection/Evidence input
* parser or applying inventory defaults inside an authenticated imported record.
*/
export const artifactInspectionOutputSchema =
artifactInspectionResultSchema.safeExtend({
substeps: z
.array(
artifactInspectionResultSchema.shape.substeps.element.safeExtend({
evidence: evidenceSchema.safeExtend({
normalized_result: artifactInventoryOutputSchema,
}),
}),
)
.min(1),
});
/** Project one successful inventory Evidence record into an inspection report. */
export const createArtifactInspection = (
inventoryInput: unknown,
+34 -93
View File
@@ -18,10 +18,6 @@ import {
} from "./artifactIdentity.js";
import { parseEvidence, type Evidence } from "./evidence.js";
import { artifactInspectionResultSchema } from "./artifactInspection.js";
import {
isZipPackageFormat,
isZipInventoryScopeLimitation,
} from "./zipPackageFormat.js";
/** Complete or partially assembled inventory used by comparison workflows. */
export interface InventorySet {
@@ -54,34 +50,42 @@ export interface ParsedInventorySet {
export const parseArtifactInventoryEvidence = (
input: unknown,
): ParsedInventorySet => {
const pages = (Array.isArray(input) ? input : [input]).map((value) => {
const evidence = parseEvidence(value);
const inventoryEvidence =
evidence.operation === "inventory_artifact"
? evidence
: evidence.operation === "inspect_artifact"
? artifactInspectionResultSchema.parse(evidence.normalized_result)
.substeps[0]?.evidence
: undefined;
const evidence = (Array.isArray(input) ? input : [input]).map(parseEvidence);
const pages = evidence.flatMap((carrier) => {
let inventoryEvidence: readonly Evidence[] | undefined;
if (carrier.operation === "inventory_artifact") {
inventoryEvidence = [carrier];
} else if (carrier.operation === "inspect_artifact") {
const inspection = artifactInspectionResultSchema.parse(
carrier.normalized_result,
);
if (inspection.coverage.substeps_completed !== inspection.substeps.length)
throw new TypeError(
"Artifact inspection substep count does not match its coverage declaration",
);
inventoryEvidence = inspection.substeps.map((substep) =>
parseEvidence(substep.evidence),
);
}
if (inventoryEvidence === undefined)
throw new TypeError(
"Artifact comparison requires inspect_artifact Evidence",
);
const inventory = artifactInventoryResultSchema.parse(
inventoryEvidence.normalized_result,
);
validateArtifactInventorySubject(
inventoryEvidence.subject,
inventory.manifest,
);
validateInventoryPage(inventory);
return { evidence, inventory };
return inventoryEvidence.map((pageEvidence) => {
const inventory = artifactInventoryResultSchema.parse(
pageEvidence.normalized_result,
);
validateArtifactInventorySubject(
pageEvidence.subject,
inventory.manifest,
);
validateInventoryPage(inventory);
return inventory;
});
});
return {
evidence: pages.map(({ evidence }) => evidence),
inventory: assembleInventoryResults(
pages.map(({ inventory }) => inventory),
),
evidence,
inventory: assembleInventoryResults(pages),
};
};
@@ -137,10 +141,7 @@ const assembleInventoryResults = (
throw new TypeError("Artifact inventory requires Evidence pages");
const manifestJson = canonicalJson(first.manifest, "Artifact inventory");
const coverageJson = canonicalJson(
{
coverage: first.coverage ?? null,
coverage_status: first.coverage_status ?? null,
},
first.coverage,
"Artifact inventory coverage",
);
const nodes = new Map<string, ArtifactNode>();
@@ -150,13 +151,8 @@ const assembleInventoryResults = (
if (canonicalJson(page.manifest, "Artifact inventory") !== manifestJson)
throw new TypeError("Artifact inventory pages do not share one manifest");
if (
canonicalJson(
{
coverage: page.coverage ?? null,
coverage_status: page.coverage_status ?? null,
},
"Artifact inventory coverage",
) !== coverageJson
canonicalJson(page.coverage, "Artifact inventory coverage") !==
coverageJson
)
throw new TypeError(
"Artifact inventory pages do not share one coverage declaration",
@@ -221,7 +217,7 @@ const orderedInventory = (
edges,
integrityContradictions: first.integrity_contradictions,
limitations,
coverage: resolveInventoryCoverage({ ...first, limitations }),
coverage: first.coverage,
complete:
nodes.length === first.manifest.node_count &&
occurrences.length === first.manifest.occurrence_count &&
@@ -267,61 +263,6 @@ const orderOccurrences = (
});
};
// Normalize both retained coverage formats at this owning boundary.
const resolveInventoryCoverage = (
inventory: ArtifactInventoryResult,
): ArtifactInventoryCoverage => {
const legacy = legacyInventoryCoverage(
inventory.manifest.root_format,
inventory.limitations,
inventory.integrity_contradictions.length,
);
if (inventory.coverage !== undefined) {
if (
inventory.coverage_status !== undefined &&
(inventory.coverage.status === "complete") !==
(inventory.coverage_status === "complete")
)
throw new TypeError(
"Artifact inventory has conflicting coverage declarations",
);
return inventory.coverage;
}
return inventory.coverage_status === undefined
? legacy
: {
...legacy,
status:
inventory.coverage_status === "complete" ? "complete" : "unknown",
};
};
// Retained pre-coverage records cannot classify arbitrary prose. Recognize only
// the historical producer vocabulary here; unfamiliar diagnostics remain unknown.
const legacyInventoryCoverage = (
format: ArtifactInventoryResult["manifest"]["root_format"],
limitations: readonly string[],
contradictionCount: number,
): ArtifactInventoryCoverage => ({
scope:
isZipPackageFormat(format) ||
["directory", "asar", "dmg", "pkg", "mach-o-universal"].includes(format)
? "container-entries"
: "root",
status: limitations.some(
(limitation) =>
limitation !== "Artifact has no child container entries." &&
!isZipInventoryScopeLimitation(limitation) &&
!(
contradictionCount > 0 &&
limitation ===
`${String(contradictionCount)} integrity contradiction(s) were recorded; mismatched content is observed-untrusted.`
),
)
? "unknown"
: "complete",
});
const validateCompleteInventory = (inventory: InventorySet): void => {
const nodeIds = new Set(inventory.nodes.map(({ artifact_id: id }) => id));
const occurrenceIds = new Set(
+1 -4
View File
@@ -7,9 +7,6 @@ import type {
/** Immutable inventory produced by one complete artifact scan. */
export interface ArtifactInventorySnapshot {
readonly coverage_status: NonNullable<
ArtifactInventoryResult["coverage_status"]
>;
readonly manifest: ArtifactInventoryResult["manifest"];
readonly nodes: readonly ArtifactNode[];
readonly occurrences: ArtifactInventoryResult["occurrences"];
@@ -18,6 +15,6 @@ export interface ArtifactInventorySnapshot {
ArtifactInventoryResult["provenance"][number]
>;
readonly integrity_contradictions: readonly IntegrityContradiction[];
readonly coverage?: ArtifactInventoryCoverage;
readonly coverage: ArtifactInventoryCoverage;
readonly limitations: readonly string[];
}
-6
View File
@@ -55,9 +55,3 @@ export const zipPackageFormatForPath = (
/** Inventory lists entries, not all bytes surrounding the central directory. */
export const ZIP_NON_ENTRY_LIMITATION =
"ZIP inventory lists central-directory entries only. Non-entry bytes, including an APK Signing Block before the central directory or a ZIP comment after it, are not occurrences.";
/** Recognize the same inventory scope in current and previously retained Evidence. */
export const isZipInventoryScopeLimitation = (limitation: string): boolean =>
limitation === ZIP_NON_ENTRY_LIMITATION ||
limitation ===
"ZIP inventory lists central-directory entries only. Bytes after the central directory, including an APK Signing Block or a ZIP comment, are not occurrences.";
+23 -18
View File
@@ -180,24 +180,29 @@ const evidenceValues = (
if (kind === "evidence") return [evidence.evidence_id];
if (kind === "capture")
return isProcessCaptureEvidence(evidence) ? [evidence.evidence_id] : [];
const inventoryResult =
evidence.operation === "inventory_artifact"
? evidence.normalized_result
: evidence.operation === "inspect_artifact"
? (() => {
const inspection = artifactInspectionResultSchema.safeParse(
evidence.normalized_result,
);
return inspection.success
? inspection.data.substeps[0]?.evidence.normalized_result
: undefined;
})()
: undefined;
if (inventoryResult === undefined) return [];
const inventory = artifactInventoryResultSchema.safeParse(inventoryResult);
if (!inventory.success) return [];
if (kind === "manifest") return [inventory.data.manifest.manifest_id];
return inventory.data.occurrences.map(({ occurrence_id: id }) => id);
let inventoryResults: readonly unknown[];
if (evidence.operation === "inventory_artifact")
inventoryResults = [evidence.normalized_result];
else if (evidence.operation === "inspect_artifact") {
const inspection = artifactInspectionResultSchema.safeParse(
evidence.normalized_result,
);
if (!inspection.success) return [];
inventoryResults = inspection.data.substeps.map(
({ evidence: inventoryEvidence }) => inventoryEvidence.normalized_result,
);
} else return [];
const values: string[] = [];
for (const inventoryResult of inventoryResults) {
const inventory = artifactInventoryResultSchema.safeParse(inventoryResult);
if (!inventory.success) continue;
if (kind === "manifest") values.push(inventory.data.manifest.manifest_id);
else
values.push(
...inventory.data.occurrences.map(({ occurrence_id: id }) => id),
);
}
return values;
};
const isProcessCaptureEvidence = (evidence: LedgerEvidence): boolean => {
+18 -2
View File
@@ -5,6 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js";
import { compareArtifacts } from "../domain/artifactComparison.js";
import { createEvidence, parseEvidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { resolveArtifactComparisonEvidence } from "../application/artifacts/ArtifactComparisonEvidence.js";
import type { RecordUnknownInput } from "../domain/residualUnknown.js";
import { recordDerivedEvidence } from "./recordDerivedEvidence.js";
import { runDerivedOperation } from "./runDerivedOperation.js";
@@ -20,6 +21,9 @@ export const registerArtifactComparisonTool = (
contract: ReturnType<typeof toolContract<"compare_artifacts">>,
): void => {
const admission = withAdmittedAnalysis({ kind: "session", session });
const evidenceById = (evidenceId: string) =>
session.evidenceForAnalysis?.(evidenceId) ??
session.evidenceById(evidenceId);
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
@@ -34,8 +38,20 @@ export const registerArtifactComparisonTool = (
context,
contract.name,
() => {
const left = parseEvidence(input.left);
const right = parseEvidence(input.right);
const suppliedLeft = parseEvidence(input.left);
const suppliedRight = parseEvidence(input.right);
const resolvedLeft = resolveArtifactComparisonEvidence(
suppliedLeft,
evidenceById,
);
if (!resolvedLeft.ok) throw resolvedLeft.error;
const resolvedRight = resolveArtifactComparisonEvidence(
suppliedRight,
evidenceById,
);
if (!resolvedRight.ok) throw resolvedRight.error;
const left = resolvedLeft.value;
const right = resolvedRight.value;
return { left, right, comparison: compareArtifacts(left, right) };
},
);
@@ -105,7 +105,6 @@ const assertCoverageControls = async ({
derive({
...inventory,
coverage: { scope: "root", status: "unknown" },
coverage_status: "partial",
limitations: [],
}),
);
@@ -125,26 +124,14 @@ const assertCoverageControls = async ({
limitations: expect.arrayContaining(["Artifact inventory scopes differ."]),
});
const { coverage, coverage_status: _coverageStatus, ...legacy } = inventory;
expect(coverage).toBeDefined();
const retained = await compare(left, derive(legacy));
expect(retained.normalized_result).toMatchObject({
status: "unchanged",
summary: { unchanged: 1, unknown: 0 },
});
for (const limitation of [
"Child inventory is unavailable.",
"Missing child observations; 1 integrity contradiction(s) were recorded.",
]) {
const unfamiliarLegacy = await compare(
left,
derive({ ...legacy, limitations: [limitation] }),
);
expect(unfamiliarLegacy.normalized_result).toMatchObject({
status: "unknown",
summary: { unchanged: 0, unknown: 1 },
});
}
const { coverage: _coverage, ...withoutCoverage } = inventory;
expect(() => artifactInventoryResultSchema.parse(withoutCoverage)).toThrow();
expect(() =>
artifactInventoryResultSchema.parse({
...inventory,
coverage_status: "complete",
}),
).toThrow();
};
cliTest(
@@ -34,7 +34,7 @@ it("compares identical complete plist inventories across CLI and MCP and creates
if (cliInventory === undefined)
throw new Error("CLI inspection did not retain inventory Evidence");
expect(cliInventory.normalized_result).toMatchObject({
coverage_status: "complete",
coverage: { scope: "root", status: "complete" },
limitations: ["Artifact has no child container entries."],
});
@@ -50,7 +50,7 @@ it("compares identical complete plist inventories across CLI and MCP and creates
if (mcpInventory === undefined)
throw new Error("MCP inspection did not retain inventory Evidence");
expect(mcpInventory.normalized_result).toMatchObject({
coverage_status: "complete",
coverage: { scope: "root", status: "complete" },
limitations: ["Artifact has no child container entries."],
});
@@ -264,7 +264,7 @@ describe("case-distinct APK resource names", () => {
await runProviderAnalysis(path, "inventory_artifact", {}),
);
expect(inventory.normalized_result).toMatchObject({
coverage_status: "complete",
coverage: { scope: "container-entries", status: "complete" },
});
const result = projectAndroidApplicationEvidence({
inventory_evidence: [inventory],
@@ -9,12 +9,13 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { inventoryArtifact } from "../../fixtures/artifactInventory.js";
import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js";
import { ARTIFACT_COMPARISON_EXAMPLE } from "../../../src/contracts/artifactComparisonExample.js";
import {
artifactComparisonResultSchema,
compareArtifacts,
} from "../../../src/domain/artifactComparison.js";
import { parseArtifactInventoryEvidence } from "../../../src/domain/artifactInventoryEvidence.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { createEvidence, type Evidence } from "../../../src/domain/evidence.js";
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
const PROVIDER = {
@@ -42,6 +43,66 @@ const observe = async (path: string) => {
);
};
const inspectPages = (
pages: readonly Evidence[],
substepsCompleted = pages.length,
) => {
const first = pages[0];
if (first === undefined) throw new Error("Missing inspection page");
const inventory = artifactInventoryResultSchema.parse(
first.normalized_result,
);
return createEvidence(
{
path: "artifact-inspection-example",
sha256: inventory.manifest.root_sha256,
format: inventory.manifest.root_format,
},
PROVIDER,
{
operation: "inspect_artifact",
parameters: {},
result: jsonValueSchema.parse({
inspection_id: `ai_${"f".repeat(64)}`,
subject: {
manifest_id: inventory.manifest.manifest_id,
root_artifact_id: inventory.manifest.root_artifact_id,
root_sha256: inventory.manifest.root_sha256,
root_format: inventory.manifest.root_format,
},
substeps: pages.map((evidence, index) => ({
substep_id: `ais_${(index + 1).toString(16).padStart(64, "0")}`,
operation: "inventory_artifact",
status: "completed",
evidence_id: evidence.evidence_id,
evidence,
})),
observations: [],
hypotheses: [],
contradictions: [],
unexplored_branches: [],
next_probes: [],
coverage: {
status: "complete-within-substeps",
substeps_completed: substepsCompleted,
observations_retained: 0,
observations_omitted: 0,
hypotheses_retained: 0,
hypotheses_omitted: 0,
unexplored_branches_retained: 0,
unexplored_branches_omitted: 0,
next_probes_retained: 0,
next_probes_omitted: 0,
},
evidence_links: pages.map(({ evidence_id }) => evidence_id),
limitations: [],
}),
confidence: "observed",
authority: "shipped-artifact",
},
);
};
const changedArtifactComparison = async () => {
const parent = await createTestTempDirectory("rea-artifact-compare-");
const leftPath = join(parent, "left.app");
@@ -64,26 +125,16 @@ const changedArtifactComparison = async () => {
};
describe("leaf artifact comparison", () => {
it("accepts persisted complete leaf inventory Evidence without coverage_status", async () => {
it("rejects inventory Evidence without required producer coverage", async () => {
const root = await createTestTempDirectory("rea-artifact-plist-compare-");
const leftPath = join(root, "left.plist");
const rightPath = join(root, "right.plist");
const xml =
'<?xml version="1.0"?><plist version="1.0"><dict><key>label</key><string>hello</string></dict></plist>';
await Promise.all([writeFile(leftPath, xml), writeFile(rightPath, xml)]);
const [left, right] = await Promise.all([
observe(leftPath),
observe(rightPath),
]);
// Persisted inventory Evidence predates coverage_status. The known leaf
// notice remains complete when normalized at the inventory boundary.
await writeFile(leftPath, xml);
const left = await observe(leftPath);
const parsed = artifactInventoryResultSchema.parse(left.normalized_result);
const {
coverage: _coverage,
coverage_status: _coverageStatus,
...legacyInventory
} = parsed;
const legacy = createEvidence(
const { coverage: _coverage, ...withoutCoverage } = parsed;
const malformed = createEvidence(
{
path: left.subject!.local_path,
sha256: left.subject!.digest.sha256,
@@ -93,16 +144,12 @@ describe("leaf artifact comparison", () => {
{
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse(legacyInventory),
result: jsonValueSchema.parse(withoutCoverage),
confidence: "observed",
authority: "shipped-artifact",
},
);
expect(compareArtifacts(legacy, right)).toMatchObject({
status: "unchanged",
summary: { unchanged: 1, unknown: 0 },
changes: [],
});
expect(() => parseArtifactInventoryEvidence(malformed)).toThrow();
});
it("keeps content and executable changes visible for complete plist roots", async () => {
@@ -150,8 +197,95 @@ describe("leaf artifact comparison", () => {
});
});
describe("artifact inspection inventory substeps", () => {
it("assembles every paged inventory substep and retains its outer Evidence once", async () => {
const root = await createTestTempDirectory("rea-inspect-pages-");
const path = join(root, "fixture.app");
await mkdir(path);
await Promise.all([
writeFile(join(path, "first.js"), "first();"),
writeFile(join(path, "second.js"), "second();"),
]);
const source = await observe(path);
const pages = fragmentInventoryEvidence(source, 2);
const inspection = inspectPages(pages);
const parsed = parseArtifactInventoryEvidence(inspection);
expect(parsed.evidence).toEqual([inspection]);
expect(parsed.inventory.complete).toBe(true);
expect(parsed.inventory.nodes).toHaveLength(
artifactInventoryResultSchema.parse(source.normalized_result).nodes
.length,
);
});
it("rejects inspection substeps with different inventory manifests", async () => {
const root = await createTestTempDirectory("rea-inspect-mixed-pages-");
const firstPath = join(root, "first.plist");
const secondPath = join(root, "second.plist");
await Promise.all([
writeFile(
firstPath,
'<?xml version="1.0"?><plist version="1.0"><dict><key>a</key><string>1</string></dict></plist>',
),
writeFile(
secondPath,
'<?xml version="1.0"?><plist version="1.0"><dict><key>b</key><string>2</string></dict></plist>',
),
]);
const inspection = inspectPages([
await observe(firstPath),
await observe(secondPath),
]);
expect(() => parseArtifactInventoryEvidence(inspection)).toThrow(
"Artifact inventory pages do not share one manifest",
);
});
it("rejects inspection coverage counts that disagree with retained substeps", async () => {
const root = await createTestTempDirectory("rea-inspect-count-mismatch-");
const path = join(root, "fixture.app");
await mkdir(path);
await Promise.all([
writeFile(join(path, "first.js"), "first();"),
writeFile(join(path, "second.js"), "second();"),
]);
const pages = fragmentInventoryEvidence(await observe(path), 2);
const inspection = inspectPages(pages, 1);
expect(() => parseArtifactInventoryEvidence(inspection)).toThrow(
"Artifact inspection substep count does not match its coverage declaration",
);
});
it("authenticates later inventory substeps instead of silently ignoring them", async () => {
const root = await createTestTempDirectory("rea-inspect-invalid-page-");
const path = join(root, "fixture.app");
await mkdir(path);
await writeFile(join(path, "main.js"), "main();");
const source = await observe(path);
const [first, later] = fragmentInventoryEvidence(source, 2);
if (first === undefined || later === undefined)
throw new Error("Missing inventory page");
const graph = artifactInventoryResultSchema.parse(later.normalized_result);
const tamperedLater: Evidence = {
...later,
normalized_result: jsonValueSchema.parse({
...graph,
limitations: ["Changed after this Evidence identifier was computed."],
}),
};
const inspection = inspectPages([first, tamperedLater]);
expect(() => parseArtifactInventoryEvidence(inspection)).toThrow(
"Evidence semantic identifier does not match its record",
);
});
});
describe("artifact inventory coverage", () => {
it("keeps explicit and legacy unknown coverage fail-closed", async () => {
it("uses current unknown coverage without deriving it from limitation prose", async () => {
const root = await createTestTempDirectory("rea-artifact-coverage-");
const path = join(root, "complete.plist");
await writeFile(
@@ -162,7 +296,7 @@ describe("artifact inventory coverage", () => {
const graph = artifactInventoryResultSchema.parse(
complete.normalized_result,
);
const contradictory = createEvidence(
const unknownCoverage = createEvidence(
{
path: complete.subject!.local_path,
sha256: complete.subject!.digest.sha256,
@@ -172,63 +306,19 @@ describe("artifact inventory coverage", () => {
{
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse({ ...graph, coverage_status: "partial" }),
result: jsonValueSchema.parse({
...graph,
coverage: { scope: "root", status: "unknown" },
limitations: ["No extra child entries were observed."],
}),
confidence: "derived",
authority: "shipped-artifact",
},
);
expect(() => parseArtifactInventoryEvidence(contradictory)).toThrow(
"Artifact inventory has conflicting coverage declarations",
);
const { coverage: _coverage, ...legacyStatusGraph } = graph;
const partial = createEvidence(
{
path: complete.subject!.local_path,
sha256: complete.subject!.digest.sha256,
format: complete.subject!.format,
},
PROVIDER,
{
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse({
...legacyStatusGraph,
coverage_status: "partial",
}),
confidence: "observed",
authority: "shipped-artifact",
},
);
expect(parseArtifactInventoryEvidence(partial).inventory.complete).toBe(
true,
);
expect(compareArtifacts(partial, partial)).toMatchObject({
status: "unknown",
summary: { unchanged: 0, unknown: 1 },
});
const { coverage_status: _coverageStatus, ...legacyGraph } =
legacyStatusGraph;
const unknownLegacy = createEvidence(
{
path: complete.subject!.local_path,
sha256: complete.subject!.digest.sha256,
format: complete.subject!.format,
},
PROVIDER,
{
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse({
...legacyGraph,
limitations: ["Producer omitted unexamined child entries."],
}),
confidence: "observed",
authority: "shipped-artifact",
},
);
expect(compareArtifacts(unknownLegacy, unknownLegacy)).toMatchObject({
expect(
parseArtifactInventoryEvidence(unknownCoverage).inventory.complete,
).toBe(true);
expect(compareArtifacts(unknownCoverage, unknownCoverage)).toMatchObject({
status: "unknown",
summary: { unchanged: 0, unknown: 1 },
});
@@ -248,7 +338,6 @@ describe("artifact inventory coverage", () => {
const fragmentGraph = artifactInventoryResultSchema.parse(
fragment.normalized_result,
);
const { coverage: _coverage, ...legacyFragmentGraph } = fragmentGraph;
const mixed = createEvidence(
{
path: fragment.subject!.local_path,
@@ -260,8 +349,8 @@ describe("artifact inventory coverage", () => {
operation: "inventory_artifact",
parameters: fragment.parameters,
result: jsonValueSchema.parse({
...legacyFragmentGraph,
coverage_status: "partial",
...fragmentGraph,
coverage: { ...fragmentGraph.coverage, status: "unknown" },
}),
confidence: "derived",
authority: "shipped-artifact",
@@ -274,6 +363,42 @@ describe("artifact inventory coverage", () => {
});
describe("artifact comparison", () => {
it("keeps the canonical public example valid and fully comparable", () => {
const left = parseArtifactInventoryEvidence(
ARTIFACT_COMPARISON_EXAMPLE.left,
);
const right = parseArtifactInventoryEvidence(
ARTIFACT_COMPARISON_EXAMPLE.right,
);
expect(left.inventory).toMatchObject({
complete: true,
coverage: { scope: "root", status: "complete" },
});
expect(right.inventory).toMatchObject({
complete: true,
coverage: { scope: "root", status: "complete" },
});
expect(
artifactComparisonResultSchema.parse(
compareArtifacts(
ARTIFACT_COMPARISON_EXAMPLE.left,
ARTIFACT_COMPARISON_EXAMPLE.right,
),
),
).toMatchObject({
status: "changed",
summary: { changed: 1, unknown: 0 },
changes: [
expect.objectContaining({
logical_path: ".",
classification: "changed",
dimensions: expect.arrayContaining(["content"]),
}),
],
});
});
it("classifies deterministic path changes and cites both inventories", async () => {
const firstObservation = await changedArtifactComparison();
const secondObservation = await changedArtifactComparison();
@@ -480,7 +605,7 @@ describe("artifact source-map relationship coverage", () => {
({ occurrence_id }) => occurrence_id === map.occurrence_id,
);
expect(inventory.coverage?.status).toBe("unknown");
expect(inventory.coverage.status).toBe("unknown");
expect(map.limitations).toContain(
'Source-map target is unknown because multiple occurrences share "main.js".',
);
@@ -731,7 +856,7 @@ describe("artifact comparison fragment integrity", () => {
});
describe("ZIP non-entry coverage", () => {
it("retains pre-directory APK block changes and compares historical scope descriptions", async () => {
it("retains pre-directory APK block changes under current ZIP coverage", async () => {
const root = await createTestTempDirectory("rea-apk-block-compare-");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("classes.dex", new TextReader("unchanged entry"));
@@ -776,43 +901,5 @@ describe("ZIP non-entry coverage", () => {
expect.objectContaining({ logical_path: ".", dimensions: ["content"] }),
],
});
const {
coverage: _coverage,
coverage_status: _coverageStatus,
...legacyInventory
} = inventory;
const withLimitation = (limitation: string) =>
createEvidence(
{
path: paths[0]!,
sha256: inventory.manifest.root_sha256,
format: "apk",
},
PROVIDER,
{
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse({
...legacyInventory,
limitations: [limitation],
}),
confidence: "observed",
authority: "shipped-artifact",
},
);
const historical = withLimitation(
"ZIP inventory lists central-directory entries only. Bytes after the central directory, including an APK Signing Block or a ZIP comment, are not occurrences.",
);
expect(compareArtifacts(historical, left)).toMatchObject({
status: "unchanged",
summary: { unchanged: 2, unknown: 0 },
changes: [],
});
expect(
compareArtifacts(
withLimitation("ZIP entry bytes could not be read."),
left,
).status,
).toBe("unknown");
});
});
@@ -225,7 +225,7 @@ describe("HarmonyOS inventory coverage", () => {
);
const inventory = await inventoryApplicationPackage(path);
expect(inventory.normalized_result).toMatchObject({
coverage_status: "complete",
coverage: { scope: "container-entries", status: "complete" },
});
const projection = requireDeterministicProjection(
+68 -14
View File
@@ -306,30 +306,23 @@ it("records an explicitly continued mismatch, preserves verified siblings, and n
}),
]),
});
const { coverage: _coverage, ...legacyInventory } = inventory;
const historical = createEvidence(
const { coverage: _coverage, ...inventoryWithoutCoverage } = inventory;
const missingCoverage = createEvidence(
{ path: archive, sha256: inventory.manifest.root_sha256, format: "asar" },
compact.provider,
{
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse(legacyInventory),
result: jsonValueSchema.parse(inventoryWithoutCoverage),
},
);
const retainedComparison = await client.callTool({
name: "compare_artifacts",
arguments: { left: evidence, right: historical },
});
expect(
retainedComparison.isError,
JSON.stringify(retainedComparison),
).not.toBe(true);
expect(
toolEvidence(retainedComparison.structuredContent).normalized_result,
).toMatchObject({
status: "contradiction",
summary: { unchanged: 2, contradiction: 2, unknown: 0 },
arguments: { left: evidence, right: missingCoverage },
});
expect(retainedComparison.isError, JSON.stringify(retainedComparison)).toBe(
true,
);
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
@@ -547,4 +540,65 @@ it("returns full artifact graphs inline and compares changed inventories", async
}
});
it("compares summary Evidence through retained complete inspection parents", async () => {
const directory = await createTestTempDirectory("rea-artifact-summary-mcp-");
const archive = join(directory, "one.zip");
const changedArchive = join(directory, "two.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("a.txt", new TextReader("same"));
await writeFile(archive, await writer.close());
const changedWriter = new ZipWriter(new Uint8ArrayWriter());
await changedWriter.add("a.txt", new TextReader("same"));
await changedWriter.add("b.txt", new TextReader("new"));
await writeFile(changedArchive, await changedWriter.close());
const session = createTestBinarySession(new ArtifactProvider(process.env));
const server = createServer({ kind: "session", session });
const client = new Client({
name: "artifact-summary-mcp-test",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const summaries = [];
for (const path of [archive, changedArchive]) {
const opened = await client.callTool({
name: "open_binary",
arguments: { path },
});
expect(opened.isError).not.toBe(true);
const inspected = await client.callTool({
name: "inspect_artifact",
arguments: { detail: "summary" },
});
expect(inspected.isError).not.toBe(true);
summaries.push(toolEvidence(inspected.structuredContent));
}
const [left, right] = summaries;
if (left === undefined || right === undefined)
throw new Error("missing artifact summary Evidence");
expect(left.operation).toBe("inspect_analysis_view");
expect(right.operation).toBe("inspect_analysis_view");
const compared = await client.callTool({
name: "compare_artifacts",
arguments: { left, right },
});
expect(compared.isError).not.toBe(true);
const result = toolEvidence(compared.structuredContent);
expect(result.normalized_result).toMatchObject({ status: "changed" });
const leftParentId = z
.object({ parent_evidence_id: z.string() })
.parse(left.normalized_result).parent_evidence_id;
const rightParentId = z
.object({ parent_evidence_id: z.string() })
.parse(right.normalized_result).parent_evidence_id;
expect(result.evidence_links).toEqual([leftParentId, rightParentId]);
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
});
const toolEvidence = (value: unknown) => parseEvidence(value);
+63 -1
View File
@@ -15,7 +15,10 @@ import { ARTIFACT_COMPARISON_EXAMPLE } from "../../../src/contracts/artifactComp
import { PROCESS_CAPTURE_REFERENCE } from "../../../src/contracts/investigationExamples.js";
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/domain/process/processCaptureExample.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { createArtifactInspection } from "../../../src/domain/artifactInspection.js";
import {
artifactInspectionResultSchema,
createArtifactInspection,
} from "../../../src/domain/artifactInspection.js";
import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js";
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
import { createPromptCompletionSource } from "../../../src/server/promptCompletion.js";
@@ -204,6 +207,65 @@ describe("guided prompt completion from investigation records", () => {
await session.close();
expect(await completion.complete("evidence", "ev_")).toEqual([]);
});
});
describe("guided prompt completion from retained records", () => {
it("offers identifiers from every retained inspection inventory substep", async () => {
const session = createTestBinarySession(() => client([]));
const leftInspection = createArtifactInspection(
ARTIFACT_COMPARISON_EXAMPLE.left,
);
const rightInspection = createArtifactInspection(
ARTIFACT_COMPARISON_EXAMPLE.right,
);
const inspection = artifactInspectionResultSchema.parse({
...leftInspection,
substeps: [...leftInspection.substeps, ...rightInspection.substeps],
evidence_links: [
...leftInspection.evidence_links,
...rightInspection.evidence_links,
],
});
const inspectionEvidence = createEvidence(undefined, fixtureProvider, {
operation: "inspect_artifact",
parameters: {},
result: jsonValueSchema.parse(inspection),
confidence: "observed",
authority: "shipped-artifact",
evidenceLinks: inspection.evidence_links,
});
expect(session.recordEvidence(inspectionEvidence).ok).toBe(true);
const completion = createPromptCompletionSource(session, session);
const expectedManifests = [
ARTIFACT_COMPARISON_EXAMPLE.left,
ARTIFACT_COMPARISON_EXAMPLE.right,
]
.map(
({ normalized_result }) =>
artifactInventoryResultSchema.parse(normalized_result).manifest
.manifest_id,
)
.sort();
const expectedOccurrences = [
ARTIFACT_COMPARISON_EXAMPLE.left,
ARTIFACT_COMPARISON_EXAMPLE.right,
]
.flatMap(({ normalized_result }) =>
artifactInventoryResultSchema
.parse(normalized_result)
.occurrences.map(({ occurrence_id }) => occurrence_id),
)
.sort();
expect(await completion.complete("manifest", "agm_")).toEqual(
expectedManifests,
);
expect(await completion.complete("occurrence", "occ_")).toEqual(
expectedOccurrences,
);
await session.close();
});
it("deduplicates, case-folds, and returns deterministic live evidence IDs", async () => {
const session = createTestBinarySession(() => client([]));