refactor(artifact): simplify extraction and inventory results

This commit is contained in:
morluto
2026-09-29 08:07:04 +08:00
parent 81996df7a5
commit 07ff8b30ee
24 changed files with 200 additions and 416 deletions
+4 -9
View File
@@ -26,20 +26,15 @@ import type { BinaryTarget } from "../domain/binaryTarget.js";
import { scanArtifactInventory } from "./ArtifactInventory.js";
import {
digestCanonical,
pageOf,
toOutputLimits,
} from "./ArtifactGraphConstruction.js";
const PAGE_SIZE = 500;
/** Already-approved extraction request from a caller-owned policy boundary. */
export interface ArtifactExtractionInput {
readonly inputPath: string;
readonly inputFormat: BinaryTarget["format"];
readonly outputRoot: string;
readonly occurrenceIds: readonly string[];
readonly offset: number;
readonly limit: number;
readonly limits: ArtifactLimits;
}
@@ -201,8 +196,8 @@ const createExtractionResult = (
...extractionSemantic,
extraction_id: `aex_${digestCanonical(extractionSemantic)}`,
},
output_root: "$OUTPUT_ROOT",
artifacts: pageOf(extracted, input.offset, input.limit),
output_root: input.outputRoot,
artifacts: extracted,
containment_verified: true,
cleanup: { attempted: false, verified: true, residual_paths: [] },
limits: toOutputLimits(input.limits),
@@ -284,10 +279,10 @@ const createReader = async (
};
const validateSelection = (ids: readonly string[]): void => {
if (ids.length === 0 || ids.length > PAGE_SIZE)
if (ids.length === 0)
throw new ArtifactReaderFailure(
"limit",
"Extraction requires 1 to 500 explicitly selected occurrences",
"Extraction requires at least one explicitly selected occurrence",
);
if (new Set(ids).size !== ids.length)
throw new ArtifactReaderFailure(
@@ -0,0 +1,7 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
/** Choose a fresh temporary destination for an artifact extraction. */
export const createArtifactExtractionDestination = (): string =>
join(tmpdir(), `rea-extracted-${randomUUID()}`);
@@ -318,18 +318,6 @@ export const classifyArtifactContent = (
return byPath;
};
export const pageOf = <Value>(
items: readonly Value[],
offset: number,
limit: number,
) => ({
items: items.slice(offset, offset + limit),
offset,
limit,
total: items.length,
next_offset: offset + limit < items.length ? offset + limit : null,
});
export const toOutputLimits = (limits: ArtifactLimits) => ({
max_entries: limits.maxEntries,
max_total_bytes: limits.maxTotalBytes,
+2 -29
View File
@@ -5,7 +5,6 @@ import {
artifactInventoryResultSchema,
type ArtifactInventoryResult,
} from "../domain/artifactGraph.js";
import { pageOf } from "./ArtifactGraphConstruction.js";
import { abortIfNeeded } from "./ArtifactInventory/hash.js";
import { scanCanonicalArtifactInventory } from "./ArtifactInventory/scanCanonical.js";
import type {
@@ -13,7 +12,6 @@ import type {
ArtifactInventoryOptions,
ArtifactInventorySnapshot,
ArtifactNativeMountPolicy,
InventoryPageInput,
} from "./ArtifactInventory/types.js";
export { scanCanonicalArtifactInventory } from "./ArtifactInventory/scanCanonical.js";
@@ -23,14 +21,12 @@ export type {
ArtifactInventoryOptions,
ArtifactInventorySnapshot,
ArtifactNativeMountPolicy,
InventoryPageInput,
} from "./ArtifactInventory/types.js";
/** Inventory one local artifact without extracting or mounting it. */
export const inventoryArtifact = async (
inputPath: string,
limits: ArtifactLimits,
page: InventoryPageInput,
options: {
readonly signal?: AbortSignal;
readonly nativeMount?: ArtifactNativeMountPolicy;
@@ -38,7 +34,7 @@ export const inventoryArtifact = async (
} = {},
): Promise<ArtifactInventoryResult> => {
const snapshot = await scanArtifactInventory(inputPath, limits, options);
return paginateArtifactInventory(snapshot, page);
return artifactInventoryResultSchema.parse(snapshot);
};
/** Inventory one artifact and return every graph collection in one response. */
@@ -52,14 +48,7 @@ export const inventoryArtifactFully = async (
} = {},
): Promise<ArtifactInventoryResult> => {
const snapshot = await scanArtifactInventory(inputPath, limits, options);
return paginateArtifactInventory(snapshot, {
nodeOffset: 0,
nodeLimit: Math.max(1, snapshot.nodes.length),
occurrenceOffset: 0,
occurrenceLimit: Math.max(1, snapshot.occurrences.length),
edgeOffset: 0,
edgeLimit: Math.max(1, snapshot.edges.length),
});
return artifactInventoryResultSchema.parse(snapshot);
};
/** Scan an artifact once and retain the complete immutable graph for projection. */
@@ -72,19 +61,3 @@ export const scanArtifactInventory = async (
const path = await realpath(inputPath);
return scanCanonicalArtifactInventory(path, limits, options);
};
/** Project independently paged graph collections from one inventory snapshot. */
export const paginateArtifactInventory = (
snapshot: ArtifactInventorySnapshot,
page: InventoryPageInput,
): ArtifactInventoryResult =>
artifactInventoryResultSchema.parse({
...snapshot,
nodes: pageOf(snapshot.nodes, page.nodeOffset, page.nodeLimit),
occurrences: pageOf(
snapshot.occurrences,
page.occurrenceOffset,
page.occurrenceLimit,
),
edges: pageOf(snapshot.edges, page.edgeOffset, page.edgeLimit),
});
+1 -4
View File
@@ -7,10 +7,7 @@ import type {
/** Parsed caller intent for integrity mismatch handling. */
export type ArtifactIntegrityIntent =
| { readonly mode: "fail" }
| {
readonly mode: "record-and-continue";
readonly maxMismatches: number;
};
| { readonly mode: "record-and-continue" };
/** Resolve caller approval and operator policy before native reader selection. */
export const resolveNativeMountPolicy = (
@@ -179,11 +179,6 @@ const digestArtifactEntry = async (
unpacked: entry.unpacked,
},
);
if (context.pendingContradictions.length >= context.integrity.maxMismatches)
throw new ArtifactReaderFailure(
"limit",
"Artifact integrity mismatch limit exceeded",
);
context.pendingContradictions.push({
logicalPath,
declaredSha256: entry.declaredSha256,
+3 -15
View File
@@ -4,15 +4,6 @@ import type {
IntegrityContradiction,
} from "../../domain/artifactGraph.js";
export interface InventoryPageInput {
readonly nodeOffset: number;
readonly nodeLimit: number;
readonly occurrenceOffset: number;
readonly occurrenceLimit: number;
readonly edgeOffset: number;
readonly edgeLimit: number;
}
/** Resolved native mount authority admitted to the artifact reader. */
export type ArtifactNativeMountPolicy =
| { readonly status: "disabled" }
@@ -25,10 +16,7 @@ export const NATIVE_MOUNT_DISABLED: ArtifactNativeMountPolicy = {
/** Resolved integrity behavior admitted to the artifact scanner. */
export type ArtifactIntegrityPolicy =
| { readonly mode: "fail" }
| {
readonly mode: "record-and-continue";
readonly maxMismatches: number;
};
| { readonly mode: "record-and-continue" };
export const STRICT_INTEGRITY_POLICY: ArtifactIntegrityPolicy = {
mode: "fail",
@@ -45,8 +33,8 @@ export interface ArtifactInventoryOptions {
export interface ArtifactInventorySnapshot {
readonly manifest: ArtifactInventoryResult["manifest"];
readonly nodes: readonly ArtifactNode[];
readonly occurrences: ArtifactInventoryResult["occurrences"]["items"];
readonly edges: ArtifactInventoryResult["edges"]["items"];
readonly occurrences: ArtifactInventoryResult["occurrences"];
readonly edges: ArtifactInventoryResult["edges"];
readonly limits: ArtifactInventoryResult["limits"];
readonly provenance: ReadonlyArray<
ArtifactInventoryResult["provenance"][number]
@@ -26,16 +26,10 @@ describe("artifact inventory policy resolution", () => {
it("admits parsed continuation intent only under operator policy", () => {
expect(() =>
resolveArtifactIntegrityPolicy(
{ mode: "record-and-continue", maxMismatches: 10 },
false,
),
resolveArtifactIntegrityPolicy({ mode: "record-and-continue" }, false),
).toThrow("requires explicit approval and operator policy");
expect(
resolveArtifactIntegrityPolicy(
{ mode: "record-and-continue", maxMismatches: 10 },
true,
),
).toEqual({ mode: "record-and-continue", maxMismatches: 10 });
resolveArtifactIntegrityPolicy({ mode: "record-and-continue" }, true),
).toEqual({ mode: "record-and-continue" });
});
});
+11 -30
View File
@@ -13,7 +13,7 @@ import {
ARTIFACT_TOOL_CONTRACTS,
artifactInspectionInputSchema,
artifactInventoryInputSchema,
artifactExtractionInputSchema,
artifactExtractionExecutionSchema,
type ArtifactToolName,
} from "../contracts/artifactToolContracts.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
@@ -52,11 +52,6 @@ export class ArtifactProvider implements AnalysisProvider {
operation: contract.name,
available: true as const,
reason: null,
pagination:
contract.name === "inspect_artifact"
? ("none" as const)
: ("offset" as const),
exhaustive: false,
effects: Object.freeze({
mutatesArtifact: false,
launchesProcess: true,
@@ -66,11 +61,6 @@ export class ArtifactProvider implements AnalysisProvider {
changesPermissions: false,
requiresRoot: false,
}),
limits: Object.freeze({
maxResults: 500,
maxPayloadBytes: 4 * 1024 * 1024,
timeoutMs: 120_000,
}),
limitations: Object.freeze([
"DMG child inventory is macOS-only and requires per-call approval plus operator policy; PKG remains root-hash-only.",
"ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only.",
@@ -124,15 +114,13 @@ class ArtifactClient implements AnalysisClient {
return inspected;
}
if (operation === "extract_artifact") {
const parsed = artifactExtractionInputSchema.parse(parameters);
const parsed = artifactExtractionExecutionSchema.parse(parameters);
const result = await extractArtifact(
{
inputPath: this.target.sourcePath ?? this.target.path,
inputFormat: this.target.format,
outputRoot: parsed.output_root,
occurrenceIds: parsed.occurrence_ids,
offset: 0,
limit: parsed.occurrence_ids.length,
limits: DEFAULT_ARTIFACT_LIMITS,
},
options?.signal,
@@ -145,12 +133,10 @@ class ArtifactClient implements AnalysisClient {
this.target.sourcePath ?? this.target.path,
result.manifest,
),
locations: result.artifacts.items.map(
({ relative_path: path }) => ({
kind: "artifact-path" as const,
path,
}),
),
locations: result.artifacts.map(({ relative_path: path }) => ({
kind: "artifact-path" as const,
path,
})),
}),
);
}
@@ -164,7 +150,7 @@ class ArtifactClient implements AnalysisClient {
this.target.sourcePath ?? this.target.path,
result.manifest,
),
locations: result.occurrences.items.map(({ logical_path: path }) => ({
locations: result.occurrences.map(({ logical_path: path }) => ({
kind: "artifact-path" as const,
path,
})),
@@ -188,7 +174,6 @@ class ArtifactClient implements AnalysisClient {
native_mount_approved: parsed.native_mount_approved,
integrity_policy: parsed.integrity_policy,
integrity_continue_approved: parsed.integrity_continue_approved,
max_integrity_mismatches: parsed.max_integrity_mismatches,
});
await options?.progress?.report({
phase: "inspect_artifact.inventory",
@@ -201,12 +186,10 @@ class ArtifactClient implements AnalysisClient {
this.target.sourcePath ?? this.target.path,
inventory.manifest,
);
const locations = inventory.occurrences.items.map(
({ logical_path: path }) => ({
kind: "artifact-path" as const,
path,
}),
);
const locations = inventory.occurrences.map(({ logical_path: path }) => ({
kind: "artifact-path" as const,
path,
}));
const inventoryEvidence = createEvidence(subject, IDENTITY, {
operation: "inventory_artifact",
parameters: inventoryParameters,
@@ -237,7 +220,6 @@ class ArtifactClient implements AnalysisClient {
readonly native_mount_approved: boolean;
readonly integrity_policy: "fail" | "record-and-continue";
readonly integrity_continue_approved: boolean;
readonly max_integrity_mismatches: number;
},
options?: ExecutionOptions,
) {
@@ -255,7 +237,6 @@ class ArtifactClient implements AnalysisClient {
? { mode: "fail" }
: {
mode: parsed.integrity_policy,
maxMismatches: parsed.max_integrity_mismatches,
},
this.integrityContinueEnabled,
),
+2 -12
View File
@@ -1,6 +1,7 @@
import { z } from "incur";
import { runProviderAnalysis } from "../application/DirectAnalysis.js";
import { createArtifactExtractionDestination } from "../application/ArtifactExtractionDestination.js";
import { logCliCommand } from "../cliLogging.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import type { Logger } from "../logger.js";
@@ -30,13 +31,6 @@ const registerInventoryCommand = (cli: CliInstance, logger: Logger): void => {
.boolean()
.default(false)
.describe("Approve continuing after recorded integrity mismatches"),
maxIntegrityMismatches: z
.number()
.int()
.min(1)
.max(100)
.default(10)
.describe("Maximum integrity mismatches to record before stopping"),
nativeMountApproved: z
.boolean()
.default(false)
@@ -45,7 +39,6 @@ const registerInventoryCommand = (cli: CliInstance, logger: Logger): void => {
alias: {
integrityPolicy: "integrity-policy",
integrityContinueApproved: "integrity-continue-approved",
maxIntegrityMismatches: "max-integrity-mismatches",
nativeMountApproved: "native-mount-approved",
},
run: ({ args, options }) =>
@@ -56,7 +49,6 @@ const registerInventoryCommand = (cli: CliInstance, logger: Logger): void => {
{
integrity_policy: options.integrityPolicy,
integrity_continue_approved: options.integrityContinueApproved,
max_integrity_mismatches: options.maxIntegrityMismatches,
native_mount_approved: options.nativeMountApproved,
},
logger,
@@ -70,7 +62,6 @@ const registerExtractionCommand = (cli: CliInstance, logger: Logger): void => {
description: "Extract explicitly selected artifact occurrences safely",
args: z.object({
path: z.string().describe("Application or package path"),
outputRoot: z.string().describe("Absent absolute output root"),
}),
options: z.object({
occurrenceIds: z
@@ -86,8 +77,7 @@ const registerExtractionCommand = (cli: CliInstance, logger: Logger): void => {
args.path,
"extract_artifact",
{
approved: true,
output_root: args.outputRoot,
output_root: createArtifactExtractionDestination(),
occurrence_ids: options.occurrenceIds,
},
logger,
+31 -43
View File
@@ -16,49 +16,37 @@ const inventory = (digit: string) => {
occurrence_count: 1,
edge_count: 0,
},
nodes: {
items: [
{
artifact_id: artifactId,
kind: "resource",
format: "file",
sha256: sha,
size: 1,
media_type: null,
architecture: null,
executable: false,
content_state: "materialized",
limitations: [],
},
],
offset: 0,
limit: 100,
total: 1,
next_offset: null,
},
occurrences: {
items: [
{
occurrence_id: occurrenceId,
artifact_id: artifactId,
parent_occurrence_id: null,
logical_path: ".",
entry_kind: "file",
declared_size: 1,
compressed_size: null,
executable: false,
encrypted: false,
hash_status: "verified",
source_location: null,
limitations: [],
},
],
offset: 0,
limit: 100,
total: 1,
next_offset: null,
},
edges: { items: [], offset: 0, limit: 100, total: 0, next_offset: null },
nodes: [
{
artifact_id: artifactId,
kind: "resource",
format: "file",
sha256: sha,
size: 1,
media_type: null,
architecture: null,
executable: false,
content_state: "materialized",
limitations: [],
},
],
occurrences: [
{
occurrence_id: occurrenceId,
artifact_id: artifactId,
parent_occurrence_id: null,
logical_path: ".",
entry_kind: "file",
declared_size: 1,
compressed_size: null,
executable: false,
encrypted: false,
hash_status: "verified",
source_location: null,
limitations: [],
},
],
edges: [],
limits: {
max_entries: 100,
max_total_bytes: 1024,
+6 -8
View File
@@ -9,12 +9,10 @@ const integrityInput = {
fail: {
integrity_policy: z.literal("fail").default("fail"),
integrity_continue_approved: z.literal(false).default(false),
max_integrity_mismatches: z.number().int().min(1).default(10),
},
continue: {
integrity_policy: z.literal("record-and-continue"),
integrity_continue_approved: z.literal(true),
max_integrity_mismatches: z.number().int().min(1).default(10),
},
} as const;
@@ -30,11 +28,13 @@ export const artifactInventoryInputSchema = z.union([
/** Exact caller boundary for approved artifact extraction. */
export const artifactExtractionInputSchema = z.object({
approved: z.literal(true),
output_root: z.string().min(1),
occurrence_ids: z.array(z.string().regex(/^occ_[a-f0-9]{64}$/u)).min(1),
});
/** Provider input after the local permission boundary chooses its destination. */
export const artifactExtractionExecutionSchema =
artifactExtractionInputSchema.extend({ output_root: z.string().min(1) });
/** Bounded provider-neutral inspection using one atomic inventory substep. */
const artifactInspectionFacts = {
native_mount_approved: z.boolean().default(false),
@@ -49,8 +49,6 @@ const examples: Readonly<Record<string, Readonly<Record<string, unknown>>>> = {
inventory_artifact: {},
inspect_artifact: {},
extract_artifact: {
approved: true,
output_root: "/tmp/rea-extracted",
occurrence_ids: [`occ_${"0".repeat(64)}`],
},
};
@@ -80,7 +78,7 @@ const artifact = <
} satisfies ToolContract<Name, Schema, typeof outputSchema>;
};
/** Artifact-graph inventory and explicitly approved safe extraction contracts. */
/** Artifact-graph inventory and safe extraction contracts. */
export const ARTIFACT_TOOL_CONTRACTS = [
artifact(
"inventory_artifact",
@@ -94,7 +92,7 @@ export const ARTIFACT_TOOL_CONTRACTS = [
),
artifact(
"extract_artifact",
"Extract selected graph artifacts beneath an explicit output root. Requires approval, rejects traversal and symlink escapes, never overwrites, enforces bomb limits, and verifies cleanup.",
"Extract selected graph artifacts into a fresh temporary directory chosen by REA. The result includes its location. Rejects traversal and symlink escapes, never overwrites, enforces archive integrity checks, and verifies cleanup.",
artifactExtractionInputSchema,
),
] as const satisfies readonly ToolContract[];
+8 -26
View File
@@ -42,10 +42,8 @@ const comparisonDimensionSchema = z.enum([
/** Strict Evidence-backed input for deterministic artifact comparison. */
export const artifactComparisonInputSchema = z.strictObject({
left_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
right_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
offset: z.number().int().min(0).default(0),
limit: z.number().int().min(1).max(500).default(100),
left_evidence_id: evidenceIdSchema,
right_evidence_id: evidenceIdSchema,
unknown_registry_approved: z
.literal(true)
.optional()
@@ -76,7 +74,7 @@ const artifactChangeSchema = z.object({
evidence_links: z.array(evidenceIdSchema).min(2).max(200),
});
/** Bounded deterministic artifact comparison with an independently paged delta. */
/** Deterministic artifact comparison with every change returned inline. */
export const artifactComparisonResultSchema = z.object({
status: comparisonStatusSchema,
left_manifest_id: z.string().regex(/^agm_[a-f0-9]{64}$/u),
@@ -89,13 +87,7 @@ export const artifactComparisonResultSchema = z.object({
unknown: z.number().int().min(0),
contradiction: z.number().int().min(0).default(0),
}),
changes: z.object({
items: z.array(artifactChangeSchema).max(500),
offset: z.number().int().min(0),
limit: z.number().int().min(1).max(500),
total: z.number().int().min(0),
next_offset: z.number().int().min(0).nullable(),
}),
changes: z.array(artifactChangeSchema),
limitations: z.array(z.string()),
});
@@ -104,12 +96,10 @@ export type ArtifactComparisonResult = z.infer<
>;
type ArtifactChange = z.infer<typeof artifactChangeSchema>;
/** Compare complete inventory pages without treating missing evidence as equality. */
/** Compare complete inventory evidence without treating missing evidence as equality. */
export const compareArtifacts = (
leftEvidence: unknown,
rightEvidence: unknown,
offset: number,
limit: number,
): ArtifactComparisonResult => {
const left = parseArtifactInventoryEvidence(leftEvidence);
const right = parseArtifactInventoryEvidence(rightEvidence);
@@ -140,7 +130,6 @@ export const compareArtifacts = (
changes,
leftCovered && rightCovered,
);
const page = changes.slice(offset, offset + limit);
return artifactComparisonResultSchema.parse({
status:
!leftComplete || !rightComplete
@@ -159,14 +148,7 @@ export const compareArtifacts = (
left_manifest_id: left.inventory.manifest.manifest_id,
right_manifest_id: right.inventory.manifest.manifest_id,
summary,
changes: {
items: page,
offset,
limit,
total: changes.length,
next_offset:
offset + page.length < changes.length ? offset + page.length : null,
},
changes,
limitations: [...new Set(limitations)].sort((a, b) => a.localeCompare(b)),
});
};
@@ -400,10 +382,10 @@ interface RelationProjection {
/** Logical parent is stable across graph-root ID changes. */
readonly parent_logical_path: string | null;
readonly child_artifact_id: string;
readonly relation: ArtifactInventoryResult["edges"]["items"][number]["relation"];
readonly relation: ArtifactInventoryResult["edges"][number]["relation"];
readonly logical_path: string | null;
/** Producer is semantic provenance; edge ordinal is presentation order. */
readonly producer: ArtifactInventoryResult["edges"]["items"][number]["producer"];
readonly producer: ArtifactInventoryResult["edges"][number]["producer"];
}
const summarize = (
+10 -23
View File
@@ -182,28 +182,15 @@ const artifactGraphManifestSchema = z.object({
edge_count: z.number().int().min(0),
});
/** Independently bounded offset page used for deterministic graph traversal. */
const artifactPageSchema = <Item extends z.ZodType>(item: Item) =>
z.object({
items: z.array(item),
offset: z.number().int().min(0),
limit: z.number().int().min(1),
total: z.number().int().min(0),
next_offset: z.number().int().min(0).nullable(),
});
/** Inventory observation containing stable manifest identity and bounded pages. */
/** Complete inventory observation containing the stable artifact graph. */
export const artifactInventoryResultSchema = z.object({
manifest: artifactGraphManifestSchema,
nodes: artifactPageSchema(artifactNodeSchema),
occurrences: artifactPageSchema(artifactOccurrenceSchema),
edges: artifactPageSchema(artifactEdgeSchema),
nodes: z.array(artifactNodeSchema),
occurrences: z.array(artifactOccurrenceSchema),
edges: z.array(artifactEdgeSchema),
limits: artifactTraversalLimitsSchema,
provenance: z.array(artifactCommandSchema).max(256),
integrity_contradictions: z
.array(integrityContradictionSchema)
.max(100)
.default([]),
provenance: z.array(artifactCommandSchema),
integrity_contradictions: z.array(integrityContradictionSchema).default([]),
limitations: z.array(z.string()),
});
@@ -220,7 +207,7 @@ const extractedArtifactSchema = z.object({
const artifactExtractionManifestSchema = z.object({
extraction_id: extractionIdSchema,
source_manifest_id: manifestIdSchema,
selected_occurrence_ids: z.array(occurrenceIdSchema).min(1).max(500),
selected_occurrence_ids: z.array(occurrenceIdSchema).min(1),
files_sha256: sha256Schema,
output_root_alias: z.literal("$OUTPUT_ROOT"),
});
@@ -230,15 +217,15 @@ export const artifactExtractionResultSchema = z.object({
manifest: artifactGraphManifestSchema,
extraction_manifest: artifactExtractionManifestSchema,
output_root: z.string().min(1),
artifacts: artifactPageSchema(extractedArtifactSchema),
artifacts: z.array(extractedArtifactSchema),
containment_verified: z.boolean(),
cleanup: z.object({
attempted: z.boolean(),
verified: z.boolean(),
residual_paths: z.array(boundedRelativePathSchema).max(500),
residual_paths: z.array(boundedRelativePathSchema),
}),
limits: artifactTraversalLimitsSchema,
provenance: z.array(artifactCommandSchema).max(256),
provenance: z.array(artifactCommandSchema),
limitations: z.array(z.string()),
});
+5 -11
View File
@@ -231,10 +231,10 @@ const allObservations = (
inventory.manifest,
evidenceId,
),
...inventory.nodes.items.map((node) =>
...inventory.nodes.map((node) =>
observation("artifact", node.artifact_id, node, evidenceId),
),
...inventory.occurrences.items.map((occurrence) =>
...inventory.occurrences.map((occurrence) =>
observation("occurrence", occurrence.occurrence_id, occurrence, evidenceId),
),
...inventory.integrity_contradictions.map((value) =>
@@ -261,7 +261,7 @@ const allRelationships = (
inventory: ArtifactInventoryResult,
evidenceId: string,
): ArtifactInspectionResult["derived_relationships"] =>
inventory.edges.items.map((edge) => {
inventory.edges.map((edge) => {
const semantic = {
relation: edge.relation,
source_artifact_id: edge.parent_artifact_id,
@@ -412,17 +412,11 @@ const branch = (
};
const inspectionCoverage = (
inventory: ArtifactInventoryResult,
_inventory: ArtifactInventoryResult,
omissions: Readonly<Record<string, number>>,
): ArtifactInspectionResult["coverage"]["status"] => {
if (Object.values(omissions).some((count) => count > 0)) return "truncated";
return [
inventory.nodes.next_offset,
inventory.occurrences.next_offset,
inventory.edges.next_offset,
].some((offset) => offset !== null)
? "partial"
: "complete-within-substeps";
return "complete-within-substeps";
};
const digestCanonical = (value: unknown): string => {
+12 -15
View File
@@ -16,7 +16,7 @@ export interface InventorySet {
readonly manifest: ArtifactInventoryResult["manifest"];
readonly nodes: readonly ArtifactNode[];
readonly occurrences: readonly ArtifactOccurrence[];
readonly edges: readonly ArtifactInventoryResult["edges"]["items"][number][];
readonly edges: readonly ArtifactInventoryResult["edges"][number][];
readonly integrityContradictions: readonly IntegrityContradiction[];
readonly limitations: readonly string[];
readonly complete: boolean;
@@ -56,22 +56,22 @@ export const parseArtifactInventoryEvidence = (
const validateInventoryPage = (inventory: ArtifactInventoryResult): void => {
assertUnique(
inventory.nodes.items.map(({ artifact_id: id }) => id),
inventory.nodes.map(({ artifact_id: id }) => id),
"node ID",
);
assertUnique(
inventory.occurrences.items.map(({ occurrence_id: id }) => id),
inventory.occurrences.map(({ occurrence_id: id }) => id),
"occurrence ID",
);
assertUnique(
inventory.occurrences.items.map(({ logical_path: path }) => path),
inventory.occurrences.map(({ logical_path: path }) => path),
"logical path",
);
assertUnique(
inventory.edges.items.map(({ edge_id: id }) => id),
inventory.edges.map(({ edge_id: id }) => id),
"edge ID",
);
for (const node of inventory.nodes.items)
for (const node of inventory.nodes)
if (node.artifact_id !== `art_${digestCanonical({ sha256: node.sha256 })}`)
throw new TypeError("Artifact node ID is not content-addressed");
};
@@ -91,10 +91,7 @@ const assembleInventorySet = (
const nodes = new Map<string, ArtifactNode>();
const occurrences = new Map<string, ArtifactOccurrence>();
const paths = new Map<string, ArtifactOccurrence>();
const edges = new Map<
string,
ArtifactInventoryResult["edges"]["items"][number]
>();
const edges = new Map<string, ArtifactInventoryResult["edges"][number]>();
for (const page of pages) {
if (canonicalJson(page.inventory.manifest) !== manifestJson)
throw new TypeError("Artifact inventory pages do not share one manifest");
@@ -105,9 +102,9 @@ const assembleInventorySet = (
throw new TypeError(
"Artifact inventory pages do not share integrity contradictions",
);
for (const node of page.inventory.nodes.items)
for (const node of page.inventory.nodes)
mergeExact(nodes, node.artifact_id, node, "node");
for (const occurrence of page.inventory.occurrences.items) {
for (const occurrence of page.inventory.occurrences) {
mergeExact(
occurrences,
occurrence.occurrence_id,
@@ -116,7 +113,7 @@ const assembleInventorySet = (
);
mergeExact(paths, occurrence.logical_path, occurrence, "logical path");
}
for (const edge of page.inventory.edges.items)
for (const edge of page.inventory.edges)
mergeExact(edges, edge.edge_id, edge, "edge");
}
const inventory = orderedInventory(first.inventory, pages, {
@@ -136,7 +133,7 @@ const orderedInventory = (
readonly occurrences: ReadonlyMap<string, ArtifactOccurrence>;
readonly edges: ReadonlyMap<
string,
ArtifactInventoryResult["edges"]["items"][number]
ArtifactInventoryResult["edges"][number]
>;
},
): InventorySet => {
@@ -242,7 +239,7 @@ const validateOccurrence = (
};
const validateEdge = (
edge: ArtifactInventoryResult["edges"]["items"][number],
edge: ArtifactInventoryResult["edges"][number],
nodeIds: ReadonlySet<string>,
occurrenceIds: ReadonlySet<string>,
): void => {
-61
View File
@@ -1,61 +0,0 @@
/** A caller-visible page whose total population is known exactly. */
export interface KnownCollectionPage<Item> {
readonly items: readonly Item[];
readonly offset: number;
readonly limit: number;
readonly total: number;
readonly returned: number;
readonly complete: boolean;
}
/** Fail-closed completeness derived from a known-total source page. */
export type KnownPageCoverageAssessment = {
readonly includedCount: number;
readonly omittedCount: number;
readonly sourceOmittedCount: number;
} & (
| {
readonly complete: true;
readonly sourceComplete: true;
readonly omittedCount: 0;
readonly sourceOmittedCount: 0;
}
| { readonly complete: false; readonly sourceComplete: boolean }
);
/**
* Assess source-page and downstream-projection completeness without treating an
* unreturned row as absent. `includedCount` is the number retained downstream.
*/
export const assessKnownPageCoverage = <Item>(
page: KnownCollectionPage<Item>,
includedCount: number = page.items.length,
): KnownPageCoverageAssessment => {
const observedCount = Math.min(page.total, page.returned, page.items.length);
const retainedCount = Math.min(
observedCount,
Math.max(0, Math.floor(includedCount)),
);
const sourceOmittedCount = Math.max(0, page.total - observedCount);
const sourceComplete =
page.complete &&
page.offset === 0 &&
page.returned === page.total &&
sourceOmittedCount === 0;
const omittedCount = Math.max(0, page.total - retainedCount);
return sourceComplete && omittedCount === 0
? {
complete: true,
sourceComplete: true,
includedCount: retainedCount,
omittedCount: 0,
sourceOmittedCount: 0,
}
: {
complete: false,
sourceComplete,
includedCount: retainedCount,
omittedCount,
sourceOmittedCount,
};
};
@@ -46,17 +46,14 @@ it("returns every artifact occurrence in one MCP call", async () => {
);
const inventory = z
.object({
occurrences: z.object({
items: z.array(z.object({ logical_path: z.string() })),
total: z.number(),
}),
occurrences: z.array(z.object({ logical_path: z.string() })),
})
.parse(
z.object({ result: z.unknown() }).parse(result.structuredContent)
.result,
);
expect(inventory.occurrences.items).toHaveLength(fileCount + 1);
expect(inventory.occurrences.total).toBe(fileCount + 1);
expect(inventory.occurrences).toHaveLength(fileCount + 1);
expect(inventory.occurrences.length).toBe(fileCount + 1);
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
+21 -28
View File
@@ -94,12 +94,9 @@ it.each([
);
const artifactInventoryResultSchema = z.object({
occurrences: z.object({
items: z.array(
z.object({ logical_path: z.string(), hash_status: z.string() }),
),
total: z.number(),
}),
occurrences: z.array(
z.object({ logical_path: z.string(), hash_status: z.string() }),
),
integrity_contradictions: z.array(
z.object({
logical_path: z.string(),
@@ -170,7 +167,7 @@ it("records an approved mismatch, preserves verified siblings, and never reports
]),
);
expect(inventory.integrity_contradictions).toHaveLength(2);
expect(inventory.occurrences.items).toEqual(
expect(inventory.occurrences).toEqual(
expect.arrayContaining([
expect.objectContaining({
logical_path: "main.js",
@@ -209,27 +206,25 @@ it("records an approved mismatch, preserves verified siblings, and never reports
const compared = await client.callTool({
name: "compare_artifacts",
arguments: {
left_evidence_ids: [evidence.evidence_id],
right_evidence_ids: [evidence.evidence_id],
left_evidence_id: evidence.evidence_id,
right_evidence_id: evidence.evidence_id,
},
});
expect(compared.isError).not.toBe(true);
expect(compactResult(compared.structuredContent).result).toMatchObject({
status: "contradiction",
summary: { contradiction: 2 },
changes: {
items: expect.arrayContaining([
expect.objectContaining({
logical_path: "main.js",
classification: "contradiction",
dimensions: ["integrity"],
}),
expect.objectContaining({
logical_path: "second.js",
classification: "contradiction",
}),
]),
},
changes: expect.arrayContaining([
expect.objectContaining({
logical_path: "main.js",
classification: "contradiction",
dimensions: ["integrity"],
}),
expect.objectContaining({
logical_path: "second.js",
classification: "contradiction",
}),
]),
});
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
@@ -382,11 +377,9 @@ it("returns full artifact graphs inline and compares changed inventories", async
z
.object({
manifest: z.object({ root_format: z.literal("ipa") }),
occurrences: z.object({
items: z.array(z.object({ logical_path: z.string() })),
}),
occurrences: z.array(z.object({ logical_path: z.string() })),
})
.parse(inventoryResult.result).occurrences.items,
.parse(inventoryResult.result).occurrences,
).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -410,8 +403,8 @@ it("returns full artifact graphs inline and compares changed inventories", async
const compared = await client.callTool({
name: "compare_artifacts",
arguments: {
left_evidence_ids: [evidence.evidence_id],
right_evidence_ids: [changedEvidence.evidence_id],
left_evidence_id: evidence.evidence_id,
right_evidence_id: changedEvidence.evidence_id,
unknown_registry_approved: true,
},
});
@@ -28,8 +28,8 @@ describe("artifact archive inventory", () => {
expect(parsed.ok && parsed.value.format).toBe("apk");
if (!parsed.ok) return;
const zipResult = await inventory(parsed.value);
expect(zipResult.occurrences.total).toBe(3);
expect(zipResult.nodes.items.map(({ kind }) => kind)).toEqual(
expect(zipResult.occurrences.length).toBe(3);
expect(zipResult.nodes.map(({ kind }) => kind)).toEqual(
expect.arrayContaining(["javascript", "dynamic-library"]),
);
const cliEvidence = parseEvidence(
@@ -86,7 +86,7 @@ describe("artifact archive inventory", () => {
const asarPath = join(root, "app.asar");
await createPackage(source, asarPath);
const asarResult = await inventory(target(asarPath, "asar"));
expect(asarResult.occurrences.items).toEqual(
expect(asarResult.occurrences).toEqual(
expect.arrayContaining([
expect.objectContaining({ logical_path: "main.js" }),
]),
@@ -95,7 +95,7 @@ describe("artifact archive inventory", () => {
const unpackedPath = join(root, "unpacked.asar");
await createPackageWithOptions(source, unpackedPath, { unpack: "*.js" });
const unpackedResult = await inventory(target(unpackedPath, "asar"));
expect(unpackedResult.occurrences.items).toEqual(
expect(unpackedResult.occurrences).toEqual(
expect.arrayContaining([
expect.objectContaining({
logical_path: "main.js",
@@ -13,7 +13,7 @@ import {
} from "../../../../src/artifacts/ArtifactPaths.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { ArtifactReaderFailure } from "../../../../src/artifacts/ArtifactReader.js";
import { artifactExtractionInputSchema } from "../../../../src/contracts/artifactToolContracts.js";
import { artifactExtractionExecutionSchema } from "../../../../src/contracts/artifactToolContracts.js";
import { artifactInventoryResultSchema } from "../../../../src/domain/artifactGraph.js";
import type { BinaryTarget } from "../../../../src/domain/binaryTarget.js";
import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js";
@@ -47,10 +47,10 @@ describe("artifact archive safety", () => {
if (!parsed.ok) return;
const result = await inventory(parsed.value);
expect(result.manifest.root_format).toBe(format);
expect(result.nodes.items.map(({ kind }) => kind)).toEqual(
expect(result.nodes.map(({ kind }) => kind)).toEqual(
expect.arrayContaining(["javascript", "native-addon"]),
);
const selected = result.occurrences.items.find(
const selected = result.occurrences.find(
({ logical_path: logicalPath }) => logicalPath === "Assets/main.js",
);
expect(selected).toBeDefined();
@@ -60,8 +60,7 @@ describe("artifact archive safety", () => {
.createClient(parsed.value)
.execute(
"extract_artifact",
artifactExtractionInputSchema.parse({
approved: true,
artifactExtractionExecutionSchema.parse({
output_root: output,
occurrence_ids: [selected.occurrence_id],
}),
@@ -41,7 +41,7 @@ describe("artifact directory inventory", () => {
const left = artifactInventoryResultSchema.parse(first.value.result);
const right = artifactInventoryResultSchema.parse(second.value.result);
expect(left.manifest).toEqual(right.manifest);
expect(left.nodes.items.map(({ kind }) => kind)).toEqual(
expect(left.nodes.map(({ kind }) => kind)).toEqual(
expect.arrayContaining([
"executable",
"framework",
@@ -50,17 +50,17 @@ describe("artifact directory inventory", () => {
]),
);
expect(
left.occurrences.items.find(({ logical_path: path }) =>
left.occurrences.find(({ logical_path: path }) =>
path.endsWith("outside"),
),
).toMatchObject({ artifact_id: null, entry_kind: "symlink" });
const script = left.occurrences.items.find(({ logical_path: path }) =>
const script = left.occurrences.find(({ logical_path: path }) =>
path.endsWith("main.js"),
);
const sourceMap = left.occurrences.items.find(({ logical_path: path }) =>
const sourceMap = left.occurrences.find(({ logical_path: path }) =>
path.endsWith("main.js.MAP"),
);
expect(left.edges.items).toEqual(
expect(left.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
relation: "maps-source",
@@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js";
import { artifactExtractionInputSchema } from "../../../../src/contracts/artifactToolContracts.js";
import { artifactExtractionExecutionSchema } from "../../../../src/contracts/artifactToolContracts.js";
import {
artifactExtractionResultSchema,
artifactInventoryResultSchema,
@@ -22,7 +22,7 @@ describe("artifact extraction", () => {
await writeFile(join(source, "assets", "ignored.js"), "ignored();\n");
const targetValue = target(source, "directory");
const graph = await inventory(targetValue);
const selected = graph.occurrences.items.find(
const selected = graph.occurrences.find(
({ logical_path: path }) => path === "assets/selected.js",
);
expect(selected).toBeDefined();
@@ -34,8 +34,7 @@ describe("artifact extraction", () => {
.createClient(targetValue)
.execute(
"extract_artifact",
artifactExtractionInputSchema.parse({
approved: true,
artifactExtractionExecutionSchema.parse({
output_root: cancelledOutput,
occurrence_ids: [selected.occurrence_id],
}),
@@ -51,8 +50,7 @@ describe("artifact extraction", () => {
.createClient(targetValue)
.execute(
"extract_artifact",
artifactExtractionInputSchema.parse({
approved: true,
artifactExtractionExecutionSchema.parse({
output_root: output,
occurrence_ids: [selected.occurrence_id],
}),
@@ -63,9 +61,11 @@ describe("artifact extraction", () => {
result.value.result,
);
expect(firstExtraction).toMatchObject({
output_root: "$OUTPUT_ROOT",
output_root: output,
containment_verified: true,
artifacts: { total: 1 },
artifacts: expect.arrayContaining([
expect.objectContaining({ relative_path: "assets/selected.js" }),
]),
extraction_manifest: { output_root_alias: "$OUTPUT_ROOT" },
});
expect(await readFile(join(output, "assets", "selected.js"), "utf8")).toBe(
@@ -80,8 +80,7 @@ describe("artifact extraction", () => {
.createClient(targetValue)
.execute(
"extract_artifact",
artifactExtractionInputSchema.parse({
approved: true,
artifactExtractionExecutionSchema.parse({
output_root: relocatedOutput,
occurrence_ids: [selected.occurrence_id],
}),
@@ -97,8 +96,7 @@ describe("artifact extraction", () => {
.createClient(targetValue)
.execute(
"extract_artifact",
artifactExtractionInputSchema.parse({
approved: true,
artifactExtractionExecutionSchema.parse({
output_root: output,
occurrence_ids: [selected.occurrence_id],
}),
@@ -6,6 +6,7 @@ import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { inventoryArtifact } from "../../../src/application/ArtifactInventory.js";
import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js";
import {
artifactComparisonResultSchema,
compareArtifacts,
@@ -27,23 +28,8 @@ const PROVIDER = {
version: "1",
} as const;
const observe = async (
path: string,
page: {
readonly nodeOffset?: number;
readonly nodeLimit?: number;
readonly occurrenceOffset?: number;
readonly edgeOffset?: number;
} = {},
) => {
const inventory = await inventoryArtifact(path, LIMITS, {
nodeOffset: page.nodeOffset ?? 0,
nodeLimit: page.nodeLimit ?? 500,
occurrenceOffset: page.occurrenceOffset ?? 0,
occurrenceLimit: 500,
edgeOffset: page.edgeOffset ?? 0,
edgeLimit: 500,
});
const observe = async (path: string) => {
const inventory = await inventoryArtifact(path, LIMITS);
return createEvidence(
{
path,
@@ -76,20 +62,21 @@ describe("artifact comparison", () => {
]);
const left = await observe(leftPath);
const right = await observe(rightPath);
const first = compareArtifacts(left, right, 0, 1);
const second = compareArtifacts(left, right, 0, 1);
const first = compareArtifacts(left, right);
const second = compareArtifacts(left, right);
expect(first).toEqual(second);
expect(artifactComparisonResultSchema.parse(first)).toMatchObject({
status: "changed",
summary: { added: 1, changed: 2, unknown: 0 },
changes: { limit: 1, total: 3, next_offset: 1 },
changes: expect.arrayContaining([
expect.objectContaining({ logical_path: "added.txt" }),
]),
});
expect(first.changes.items[0]?.evidence_links).toEqual([
expect(first.changes[0]?.evidence_links).toEqual([
left.evidence_id,
right.evidence_id,
]);
const all = compareArtifacts(left, right, 0, 500);
expect(all.changes.items).toEqual(
expect(first.changes).toEqual(
expect.arrayContaining([
expect.objectContaining({
logical_path: "added.txt",
@@ -107,8 +94,30 @@ describe("artifact comparison", () => {
it("reports incomplete inventory as truncated, never unchanged", async () => {
const root = await createTestTempDirectory("rea-artifact-truncated-");
await writeFile(join(root, "one.txt"), "one");
const incomplete = await observe(root, { nodeLimit: 1 });
const comparison = compareArtifacts(incomplete, incomplete, 0, 100);
const complete = await observe(root);
const completeInventory = artifactInventoryResultSchema.parse(
complete.normalized_result,
);
const incomplete = createEvidence(
{
path: root,
sha256: complete.subject?.digest.sha256 ?? "0".repeat(64),
format: "directory",
},
PROVIDER,
{
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse({
...completeInventory,
nodes: [],
}),
confidence: "observed",
authority: "shipped-artifact",
limitations: ["Inventory node observations are unavailable."],
},
);
const comparison = compareArtifacts(incomplete, incomplete);
expect(comparison).toMatchObject({
status: "truncated",
summary: { unchanged: 0, unknown: 2 },
@@ -118,7 +127,7 @@ describe("artifact comparison", () => {
);
});
it("assembles bounded page sets for graphs larger than one page", async () => {
it("compares every graph member for inventories larger than 500 entries", async () => {
const root = await createTestTempDirectory("rea-artifact-pages-");
await Promise.all(
Array.from({ length: 501 }, async (_, index) =>
@@ -128,18 +137,18 @@ describe("artifact comparison", () => {
),
),
);
const pages = await Promise.all([
observe(root),
observe(root, {
nodeOffset: 500,
occurrenceOffset: 500,
edgeOffset: 500,
}),
]);
expect(compareArtifacts(pages, pages, 0, 100)).toMatchObject({
const complete = await observe(root);
expect(complete.normalized_result).toMatchObject({
nodes: expect.arrayContaining([
expect.objectContaining({ kind: expect.any(String) }),
]),
occurrences: expect.any(Array),
edges: expect.any(Array),
});
expect(compareArtifacts(complete, complete)).toMatchObject({
status: "unchanged",
summary: { unchanged: 502, unknown: 0 },
changes: { total: 0 },
changes: [],
});
}, 15_000);
@@ -147,19 +156,14 @@ describe("artifact comparison", () => {
const root = await createTestTempDirectory("rea-artifact-invalid-");
const evidence = await observe(root);
expect(() =>
compareArtifacts(
{ ...evidence, operation: "binary_overview" },
evidence,
0,
10,
),
compareArtifacts({ ...evidence, operation: "binary_overview" }, evidence),
).toThrow(/identifier/u);
const wrongOperation = createEvidence(undefined, PROVIDER, {
operation: "binary_overview",
parameters: {},
result: evidence.normalized_result,
});
expect(() => compareArtifacts(wrongOperation, evidence, 0, 10)).toThrow(
expect(() => compareArtifacts(wrongOperation, evidence)).toThrow(
/inventory_artifact/u,
);
const mismatchedSubject = createEvidence(
@@ -171,7 +175,7 @@ describe("artifact comparison", () => {
result: evidence.normalized_result,
},
);
expect(() => compareArtifacts(mismatchedSubject, evidence, 0, 10)).toThrow(
expect(() => compareArtifacts(mismatchedSubject, evidence)).toThrow(
/root digest/u,
);
});