From 07ff8b30ee497d9f4477a0a9ae97f3f5addbb45a Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:07:04 +0800 Subject: [PATCH] refactor(artifact): simplify extraction and inventory results --- src/application/ArtifactExtraction.ts | 13 +-- .../ArtifactExtractionDestination.ts | 7 ++ src/application/ArtifactGraphConstruction.ts | 12 --- src/application/ArtifactInventory.ts | 31 +------ src/application/ArtifactInventory/policy.ts | 5 +- .../ArtifactInventory/scanReader.ts | 5 - src/application/ArtifactInventory/types.ts | 18 +--- .../ArtifactInventoryPolicy.test.ts | 12 +-- src/artifacts/ArtifactProvider.ts | 41 +++------ src/cli/artifactCommands.ts | 14 +-- src/contracts/artifactComparisonExample.ts | 74 +++++++-------- src/contracts/artifactToolContracts.ts | 14 ++- src/domain/artifactComparison.ts | 34 ++----- src/domain/artifactGraph.ts | 33 ++----- src/domain/artifactInspection.ts | 16 +--- src/domain/artifactInventoryEvidence.ts | 27 +++--- src/domain/knownPageCoverage.ts | 61 ------------ .../mcp/artifactMcp.inlineInventory.test.ts | 9 +- tests/boundary/mcp/artifactMcp.test.ts | 49 +++++----- .../artifactProvider.archiveInventory.test.ts | 8 +- .../artifactProvider.archiveSafety.test.ts | 9 +- ...rtifactProvider.directoryInventory.test.ts | 10 +- .../artifactProvider.extraction.test.ts | 22 ++--- .../artifactComparison.test.ts | 92 ++++++++++--------- 24 files changed, 200 insertions(+), 416 deletions(-) create mode 100644 src/application/ArtifactExtractionDestination.ts delete mode 100644 src/domain/knownPageCoverage.ts diff --git a/src/application/ArtifactExtraction.ts b/src/application/ArtifactExtraction.ts index dae3d67afd..bc81f07e0b 100644 --- a/src/application/ArtifactExtraction.ts +++ b/src/application/ArtifactExtraction.ts @@ -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( diff --git a/src/application/ArtifactExtractionDestination.ts b/src/application/ArtifactExtractionDestination.ts new file mode 100644 index 0000000000..5b8934ff07 --- /dev/null +++ b/src/application/ArtifactExtractionDestination.ts @@ -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()}`); diff --git a/src/application/ArtifactGraphConstruction.ts b/src/application/ArtifactGraphConstruction.ts index a3aa22fb7b..b9959328e4 100644 --- a/src/application/ArtifactGraphConstruction.ts +++ b/src/application/ArtifactGraphConstruction.ts @@ -318,18 +318,6 @@ export const classifyArtifactContent = ( return byPath; }; -export const pageOf = ( - 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, diff --git a/src/application/ArtifactInventory.ts b/src/application/ArtifactInventory.ts index 37d23a748f..7b0ac2ebfd 100644 --- a/src/application/ArtifactInventory.ts +++ b/src/application/ArtifactInventory.ts @@ -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 => { 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 => { 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), - }); diff --git a/src/application/ArtifactInventory/policy.ts b/src/application/ArtifactInventory/policy.ts index 5494a3907d..2489a24d8b 100644 --- a/src/application/ArtifactInventory/policy.ts +++ b/src/application/ArtifactInventory/policy.ts @@ -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 = ( diff --git a/src/application/ArtifactInventory/scanReader.ts b/src/application/ArtifactInventory/scanReader.ts index 581c130142..138de46d89 100644 --- a/src/application/ArtifactInventory/scanReader.ts +++ b/src/application/ArtifactInventory/scanReader.ts @@ -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, diff --git a/src/application/ArtifactInventory/types.ts b/src/application/ArtifactInventory/types.ts index d41531f52e..7f6237ff7f 100644 --- a/src/application/ArtifactInventory/types.ts +++ b/src/application/ArtifactInventory/types.ts @@ -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] diff --git a/src/application/ArtifactInventoryPolicy.test.ts b/src/application/ArtifactInventoryPolicy.test.ts index 0e5f44b97d..2f70e2121d 100644 --- a/src/application/ArtifactInventoryPolicy.test.ts +++ b/src/application/ArtifactInventoryPolicy.test.ts @@ -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" }); }); }); diff --git a/src/artifacts/ArtifactProvider.ts b/src/artifacts/ArtifactProvider.ts index 88f61f8b6f..1fc782b3f5 100644 --- a/src/artifacts/ArtifactProvider.ts +++ b/src/artifacts/ArtifactProvider.ts @@ -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, ), diff --git a/src/cli/artifactCommands.ts b/src/cli/artifactCommands.ts index a0c4af082f..b36a19652b 100644 --- a/src/cli/artifactCommands.ts +++ b/src/cli/artifactCommands.ts @@ -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, diff --git a/src/contracts/artifactComparisonExample.ts b/src/contracts/artifactComparisonExample.ts index c18143f616..229731999a 100644 --- a/src/contracts/artifactComparisonExample.ts +++ b/src/contracts/artifactComparisonExample.ts @@ -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, diff --git a/src/contracts/artifactToolContracts.ts b/src/contracts/artifactToolContracts.ts index 674cc14d9e..7612bb975e 100644 --- a/src/contracts/artifactToolContracts.ts +++ b/src/contracts/artifactToolContracts.ts @@ -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>>> = { 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; }; -/** 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[]; diff --git a/src/domain/artifactComparison.ts b/src/domain/artifactComparison.ts index fdbc3a6c82..35214354ba 100644 --- a/src/domain/artifactComparison.ts +++ b/src/domain/artifactComparison.ts @@ -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; -/** 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 = ( diff --git a/src/domain/artifactGraph.ts b/src/domain/artifactGraph.ts index 2e8408ae03..7cbbfda901 100644 --- a/src/domain/artifactGraph.ts +++ b/src/domain/artifactGraph.ts @@ -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: 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()), }); diff --git a/src/domain/artifactInspection.ts b/src/domain/artifactInspection.ts index 2f51dde3c5..136656e73f 100644 --- a/src/domain/artifactInspection.ts +++ b/src/domain/artifactInspection.ts @@ -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>, ): 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 => { diff --git a/src/domain/artifactInventoryEvidence.ts b/src/domain/artifactInventoryEvidence.ts index 1f536a5c9b..4952673903 100644 --- a/src/domain/artifactInventoryEvidence.ts +++ b/src/domain/artifactInventoryEvidence.ts @@ -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(); const occurrences = new Map(); const paths = new Map(); - const edges = new Map< - string, - ArtifactInventoryResult["edges"]["items"][number] - >(); + const edges = new Map(); 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; 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, occurrenceIds: ReadonlySet, ): void => { diff --git a/src/domain/knownPageCoverage.ts b/src/domain/knownPageCoverage.ts deleted file mode 100644 index 5659ed146b..0000000000 --- a/src/domain/knownPageCoverage.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** A caller-visible page whose total population is known exactly. */ -export interface KnownCollectionPage { - 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 = ( - page: KnownCollectionPage, - 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, - }; -}; diff --git a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts index dc82fa2d49..3309ae2bc8 100644 --- a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts +++ b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts @@ -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()]); } diff --git a/tests/boundary/mcp/artifactMcp.test.ts b/tests/boundary/mcp/artifactMcp.test.ts index 53d11e18d7..5ab7f217f4 100644 --- a/tests/boundary/mcp/artifactMcp.test.ts +++ b/tests/boundary/mcp/artifactMcp.test.ts @@ -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, }, }); diff --git a/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts b/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts index 2a9eaad267..1a2dac7c2b 100644 --- a/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts +++ b/tests/boundary/providers/native/artifactProvider.archiveInventory.test.ts @@ -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", diff --git a/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts b/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts index 0c7e8a0112..66f9033c5f 100644 --- a/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts +++ b/tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts @@ -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], }), diff --git a/tests/boundary/providers/native/artifactProvider.directoryInventory.test.ts b/tests/boundary/providers/native/artifactProvider.directoryInventory.test.ts index 4b061ce25c..45547fd297 100644 --- a/tests/boundary/providers/native/artifactProvider.directoryInventory.test.ts +++ b/tests/boundary/providers/native/artifactProvider.directoryInventory.test.ts @@ -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", diff --git a/tests/boundary/providers/native/artifactProvider.extraction.test.ts b/tests/boundary/providers/native/artifactProvider.extraction.test.ts index a163d0742f..b3728c848b 100644 --- a/tests/boundary/providers/native/artifactProvider.extraction.test.ts +++ b/tests/boundary/providers/native/artifactProvider.extraction.test.ts @@ -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], }), diff --git a/tests/composition/analysis-sessions/artifactComparison.test.ts b/tests/composition/analysis-sessions/artifactComparison.test.ts index 02ecbef75c..0f9183f66d 100644 --- a/tests/composition/analysis-sessions/artifactComparison.test.ts +++ b/tests/composition/analysis-sessions/artifactComparison.test.ts @@ -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, ); });