mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
refactor(artifact): simplify extraction and inventory results
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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
@@ -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()),
|
||||
});
|
||||
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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()]);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user