fix(artifacts): compare inventories using declared coverage (#2214)

* fix(artifacts): preserve complete root inventory comparisons

* fix(artifacts): declare inventory coverage before comparison

* fix(artifacts): preserve traversal omissions and materialization limits

* fix(artifacts): preserve coverage declarations and projection scope

Remove the duplicate inventory-schema import. Compare original coverage declarations, including field omission, before adapting legacy pages. Require explicit scope for completeness and container-entry coverage for application projections.

Source inspection and git diff --check only. Tests, builds and typechecks skipped by explicit user request. GitHub review writes paused globally.

* fix(artifacts): require entry coverage for child changes

* fix(artifacts): accept source-valid inventory limitations

* style(artifacts): format mobile graph projection

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: OpenAI Codex <openai-codex@users.noreply.github.com>
This commit is contained in:
Thang Luu
2026-10-14 04:00:27 +09:00
committed by GitHub
parent e4186af31b
commit 45a3509db3
24 changed files with 1023 additions and 162 deletions
+25
View File
@@ -21,6 +21,31 @@ directly to `compare_functions`, and `inspect_artifact` Evidence to
`compare_artifacts`. Use `get_evidence_bundle` when the task needs broader
retained session history or an explicit bundle for transfer.
Artifact inventories declare `coverage.scope` (`root` or `container-entries`)
and `coverage.status` (`complete` or `unknown`) independently of diagnostic
wording. Comparison requires matching scopes, complete graph members and complete
traversal coverage before concluding equality. A fully observed root without
container children can be `unchanged`; its scope notice stays in `limitations`
without creating a gap. Content and executable metadata changes still produce
root deltas. Missing graph members yield `truncated`; unknown traversal coverage
or unavailable occurrence hashes retain uncertainty. Integrity mismatches remain
contradictions. Static artifact equality does not establish unchanged runtime
behavior.
Exhausted traversals with deliberately omitted filesystem entries retain
`unknown` coverage and identify each omitted path/type in `limitations`; FIFO,
socket and device entries are not opened. Case-distinct archive members remain
separate observations. Their case-insensitive materialization diagnostic does
not reduce traversal coverage when both spellings were read.
Current output also retains `coverage_status` (`complete` or `partial`) for existing clients, derived from the scoped traversal status. The owning parser normalizes retained `coverage_status` records to scoped coverage and rejects conflicting declarations.
The coverage field is optional when reading older retained Evidence. The owning
inventory parser recognizes historical REA scope notices for compatibility;
unfamiliar legacy limitations remain conservative. Typed records do not derive
coverage from limitation text. Pages from one inventory must carry the same
coverage declaration.
Function dossiers can declare per-facet coverage in the optional
`normalized_result.facet_availability` map. Each supplied facet is either
`{ "available": true }` or `{ "available": false, "reason": "..." }`.
+31 -2
View File
@@ -1,5 +1,8 @@
import type { Readable } from "node:stream";
import type { ArtifactCommand } from "../domain/artifactGraph.js";
import type {
ArtifactCommand,
ArtifactInventoryCoverage,
} from "../domain/artifactGraph.js";
import type { ZipPackageFormat } from "../domain/zipPackageFormat.js";
import type {
AnalysisCleanupObservation,
@@ -90,10 +93,36 @@ export const sameArtifactEntry = (
entry.sourceIdentity?.mtimeNs === produced.sourceIdentity?.mtimeNs &&
entry.sourceIdentity?.ctimeNs === produced.sourceIdentity?.ctimeNs;
/** One entry or subtree deliberately excluded from a traversal. */
export interface ArtifactTraversalOmission {
readonly path: string;
readonly kind:
| "fifo"
| "socket"
| "block-device"
| "character-device"
| "unsupported"
| "directory-descendants";
}
/** Facts from an exhausted traversal, separate from entry content certainty. */
export interface ArtifactReaderTraversal {
readonly status: ArtifactInventoryCoverage["status"];
readonly limitations: readonly string[];
}
/** Per-iterator observation sink; adapters report intentional omissions without shared state. */
export interface ArtifactTraversalOptions {
readonly onOmission?: (omission: ArtifactTraversalOmission) => void;
}
/** Read-only adapter over one directory, archive, or virtual container. */
export interface ArtifactReader {
readonly format: "directory" | ZipPackageFormat | "asar" | "file";
entries(signal?: AbortSignal): AsyncIterable<ArtifactEntry>;
entries(
signal?: AbortSignal,
options?: ArtifactTraversalOptions,
): AsyncIterable<ArtifactEntry>;
open(entry: ArtifactEntry, signal?: AbortSignal): Promise<Readable>;
provenance(): readonly ArtifactCommand[];
close(): Promise<void>;
+33 -6
View File
@@ -14,12 +14,18 @@ import {
sameArtifactSourceIdentity,
type ArtifactEntry,
type ArtifactReader,
type ArtifactTraversalOmission,
type ArtifactTraversalOptions,
} from "./ArtifactReader.js";
import { hostExecutableMarking } from "./hostExecutableMarking.js";
import { OwnedDirectoryHandle } from "../filesystem/OwnedDirectoryHandle.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { readFileHandleChunks } from "../filesystem/readFileHandleChunks.js";
interface DirectoryTraversalOptions extends ArtifactTraversalOptions {
readonly descendInto?: (path: string) => boolean;
}
/** Non-symlink-following directory reader. */
export class DirectoryArtifactReader implements ArtifactReader {
readonly format = "directory" as const;
@@ -35,9 +41,11 @@ export class DirectoryArtifactReader implements ArtifactReader {
/** Enumerate entries, optionally restricting descent while retaining directories. */
async *entries(
signal?: AbortSignal,
descendInto?: (path: string) => boolean,
selection?: DirectoryTraversalOptions | ((path: string) => boolean),
): AsyncIterable<ArtifactEntry> {
const root = await this.#resolveRoot();
const options =
typeof selection === "function" ? { descendInto: selection } : selection;
const pending = [root];
while (pending.length > 0) {
abortIfNeeded(signal);
@@ -50,7 +58,7 @@ export class DirectoryArtifactReader implements ArtifactReader {
directory,
pending,
signal,
descendInto,
options,
))
yield entry;
await withDirectoryIoContext("inspect directory", directory, () =>
@@ -75,7 +83,7 @@ export class DirectoryArtifactReader implements ArtifactReader {
directory: string,
pending: string[],
signal?: AbortSignal,
descendInto?: (path: string) => boolean,
options?: DirectoryTraversalOptions,
): AsyncIterable<ArtifactEntry> {
const root = await this.#resolveRoot();
const handle = new OwnedDirectoryHandle(
@@ -103,10 +111,16 @@ export class DirectoryArtifactReader implements ArtifactReader {
);
const path = relative(root, absolute).split(sep).join("/");
const entry = artifactEntry(path, absolute, metadata);
if (entry === undefined) continue;
if (entry === undefined) {
options?.onOmission?.({ path, kind: unsupportedEntryKind(metadata) });
continue;
}
yield entry;
if (entry.kind === "directory" && (descendInto?.(path) ?? true))
childDirectories.push(absolute);
if (entry.kind === "directory") {
if (options?.descendInto?.(path) ?? true)
childDirectories.push(absolute);
else options?.onOmission?.({ path, kind: "directory-descendants" });
}
}
} catch (cause: unknown) {
traversal = {
@@ -256,6 +270,19 @@ export class DirectoryArtifactReader implements ArtifactReader {
}
}
const unsupportedEntryKind = (
metadata: BigIntStats,
): ArtifactTraversalOmission["kind"] =>
metadata.isFIFO()
? "fifo"
: metadata.isSocket()
? "socket"
: metadata.isBlockDevice()
? "block-device"
: metadata.isCharacterDevice()
? "character-device"
: "unsupported";
const artifactEntry = (
path: string,
absolute: string,
+14 -2
View File
@@ -8,6 +8,7 @@ import {
ArtifactReaderFailure,
type ArtifactEntry,
type ArtifactReader,
type ArtifactTraversalOptions,
} from "./ArtifactReader.js";
import { DirectoryArtifactReader } from "./DirectoryArtifactReader.js";
import {
@@ -149,13 +150,24 @@ export class NativeDmgArtifactReader implements ArtifactReader {
this.host = host ?? createSystemHost(environment);
}
async *entries(signal?: AbortSignal): AsyncIterable<ArtifactEntry> {
async *entries(
signal?: AbortSignal,
options?: ArtifactTraversalOptions,
): AsyncIterable<ArtifactEntry> {
await this.#ensureAttached(signal);
const directory = this.#directory;
if (directory === undefined)
throw new ArtifactReaderFailure("unavailable", "DMG is not attached");
const prefix = basename(this.path);
const entries = directory.entries(signal)[Symbol.asyncIterator]();
const entries = directory
.entries(signal, {
onOmission: (omission) =>
options?.onOmission?.({
...omission,
path: `${prefix}/${omission.path}`,
}),
})
[Symbol.asyncIterator]();
try {
for (;;) {
if (this.#closed) return;
+42 -35
View File
@@ -1,5 +1,8 @@
import { AsarArtifactReader } from "../AsarArtifactReader.js";
import type { ArtifactReader } from "../ArtifactReader.js";
import type {
ArtifactReader,
ArtifactReaderTraversal,
} from "../ArtifactReader.js";
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
import { MachOSliceArtifactReader } from "../MachOSliceArtifactReader.js";
import { NativeDmgArtifactReader } from "../NativeDmgArtifactReader.js";
@@ -7,7 +10,6 @@ import { ZipArtifactReader } from "../ZipArtifactReader.js";
import type {
ArtifactInventoryCoverage,
ArtifactOccurrence,
IntegrityContradiction,
} from "../../domain/artifactGraph.js";
import {
isZipPackageFormat,
@@ -40,42 +42,47 @@ export const createReader = (
}
};
/** Classify source coverage and preserve its user-facing explanation once. */
export const inventoryAssessment = (
/** Decide traversal scope and availability once, independently of diagnostic wording. */
export const inventoryCoverage = (
format: ArtifactOccurrence["artifact_format"],
reader: ArtifactReader | undefined,
integrityContradictions: readonly IntegrityContradiction[],
caseCollisionLimitation: string | undefined,
traversal: ArtifactReaderTraversal,
): {
readonly coverage_status: ArtifactInventoryCoverage;
readonly coverage: ArtifactInventoryCoverage;
readonly limitations: string[];
} => {
let coverageStatus: ArtifactInventoryCoverage = "complete";
const limitations: string[] = [];
if (reader !== undefined) {
if (isZipPackageFormat(reader.format))
limitations.push(ZIP_NON_ENTRY_TAIL_LIMITATION);
} else if (format === "dmg" || format === "pkg") {
coverageStatus = "partial";
limitations.push(
`${format.toUpperCase()} root hash is observed; child inventory requires a native macOS adapter.`,
);
} else if (format === "mach-o-universal" && process.platform !== "darwin") {
coverageStatus = "partial";
limitations.push(
"Universal Mach-O slices require the native macOS lipo adapter.",
);
} else {
// The graph's modeled scope is the root itself for recognized leaf formats.
limitations.push("Artifact has no child container entries.");
}
if (caseCollisionLimitation !== undefined) {
coverageStatus = "partial";
limitations.push(caseCollisionLimitation);
}
if (integrityContradictions.length > 0)
limitations.push(
`${String(integrityContradictions.length)} integrity contradiction(s) were recorded; mismatched content is observed-untrusted.`,
);
return { coverage_status: coverageStatus, limitations };
if (reader !== undefined)
return {
coverage: { scope: "container-entries", status: traversal.status },
limitations: [
...(isZipPackageFormat(reader.format)
? [ZIP_NON_ENTRY_TAIL_LIMITATION]
: []),
...traversal.limitations,
],
};
// dmg and pkg reach here only after classifyRootFormat matched their bytes.
if (format === "dmg" || format === "pkg")
return {
coverage: { scope: "container-entries", status: "unknown" },
limitations: [
`${format.toUpperCase()} root hash is observed; child inventory requires a native macOS adapter.`,
],
};
if (format === "mach-o-universal")
return {
coverage: { scope: "container-entries", status: "unknown" },
limitations: [
"Universal Mach-O slices require the native macOS lipo adapter.",
],
};
if (isZipPackageFormat(format) || format === "directory" || format === "asar")
return {
coverage: { scope: "container-entries", status: "unknown" },
limitations: [`${format} child inventory reader is unavailable.`],
};
return {
coverage: { scope: "root", status: "complete" },
limitations: ["Artifact has no child container entries."],
};
};
+28 -9
View File
@@ -10,6 +10,7 @@ import type { RootInventorySource } from "./classify.js";
import {
ArtifactReaderFailure,
type ArtifactReader,
type ArtifactReaderTraversal,
} from "../ArtifactReader.js";
import type { ArtifactResourceOwner } from "../ArtifactResourceScope.js";
import {
@@ -40,7 +41,7 @@ import {
hashStableRootArtifact,
hashStableRootArtifactHandle,
} from "./hashStableRootArtifact.js";
import { createReader, inventoryAssessment } from "./reader.js";
import { createReader, inventoryCoverage } from "./reader.js";
import {
scanReader,
type PendingIntegrityContradiction,
@@ -99,6 +100,7 @@ export const scanCanonicalArtifactInventoryInScope = async (
occurrences,
pendingContradictions,
caseCollisionLimitation,
traversal,
} = await scanReader(reader, ownedReaders, options.signal, integrity);
outcome = {
kind: "completed",
@@ -115,6 +117,7 @@ export const scanCanonicalArtifactInventoryInScope = async (
occurrences,
pendingContradictions,
caseCollisionLimitation,
traversal,
}),
};
} catch (cause: unknown) {
@@ -196,6 +199,7 @@ interface SnapshotBuildInput {
readonly occurrences: MutableOccurrence[];
readonly pendingContradictions: PendingIntegrityContradiction[];
readonly caseCollisionLimitation: string | undefined;
readonly traversal: ArtifactReaderTraversal;
}
const buildInventorySnapshot = async (
@@ -268,12 +272,7 @@ const buildInventorySnapshot = async (
orderedOccurrences,
orderedEdges,
});
const assessment = inventoryAssessment(
rootFormat,
reader,
integrityContradictions,
input.caseCollisionLimitation,
);
const traversal = inventoryCoverage(rootFormat, reader, input.traversal);
return {
manifest,
nodes: orderedNodes,
@@ -281,8 +280,14 @@ const buildInventorySnapshot = async (
edges: orderedEdges,
provenance: reader?.provenance() ?? [],
integrity_contradictions: integrityContradictions,
coverage_status: assessment.coverage_status,
limitations: assessment.limitations,
coverage: traversal.coverage,
coverage_status:
traversal.coverage.status === "complete" ? "complete" : "partial",
limitations: buildLimitations(
traversal.limitations,
integrityContradictions,
input.caseCollisionLimitation,
),
};
};
@@ -395,3 +400,17 @@ const buildManifest = ({
occurrence_count: orderedOccurrences.length,
edge_count: orderedEdges.length,
});
const buildLimitations = (
scopeLimitations: readonly string[],
integrityContradictions: readonly IntegrityContradiction[],
caseCollisionLimitation: string | undefined,
): string[] => [
...scopeLimitations,
...(caseCollisionLimitation === undefined ? [] : [caseCollisionLimitation]),
...(integrityContradictions.length === 0
? []
: [
`${String(integrityContradictions.length)} integrity contradiction(s) were recorded; mismatched content is observed-untrusted.`,
]),
];
+37 -2
View File
@@ -9,9 +9,12 @@ import {
ArtifactReaderFailure,
type ArtifactEntry,
type ArtifactReader,
type ArtifactReaderTraversal,
type ArtifactTraversalOmission,
} from "../ArtifactReader.js";
import { AsarArtifactReader } from "../AsarArtifactReader.js";
import type { ArtifactNode } from "../../domain/artifactGraph.js";
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
import {
classifyArtifactContent,
classifyArtifactBytes,
@@ -55,6 +58,7 @@ interface ScanContext {
readonly registry: ArtifactPathRegistry;
readonly expandedContainerIds: Set<string>;
readonly ownedReaders: ArtifactReader[];
readonly omissions: ArtifactTraversalOmission[];
}
export const scanReader = async (
@@ -67,6 +71,7 @@ export const scanReader = async (
readonly occurrences: MutableOccurrence[];
readonly pendingContradictions: PendingIntegrityContradiction[];
readonly caseCollisionLimitation: string | undefined;
readonly traversal: ArtifactReaderTraversal;
}> => {
const { nodes, occurrences, pendingContradictions } = emptyScan();
if (reader === undefined)
@@ -75,6 +80,7 @@ export const scanReader = async (
occurrences,
pendingContradictions,
caseCollisionLimitation: undefined,
traversal: { status: "complete", limitations: [] },
};
const context: ScanContext = {
reader,
@@ -87,6 +93,7 @@ export const scanReader = async (
registry: new ArtifactPathRegistry(),
expandedContainerIds: new Set<string>(),
ownedReaders,
omissions: [],
};
await visitArtifactEntries(context, reader, "");
// Archive directories may appear after their children. Resolve containment
@@ -101,9 +108,37 @@ export const scanReader = async (
occurrences,
pendingContradictions,
caseCollisionLimitation: annotateEnglishUnicodeCaseCollisions(occurrences),
traversal: {
status: context.omissions.length === 0 ? "complete" : "unknown",
limitations: [
...new Set(
context.omissions.map(
({ path, kind }) =>
`Artifact traversal omitted ${kind} at ${JSON.stringify(path)}.`,
),
),
].sort(compareUnicodeCodePoints),
},
};
};
const inventoryEntries = (
context: ScanContext,
reader: ArtifactReader,
prefix: string,
): AsyncIterator<ArtifactEntry> =>
reader
.entries(context.signal, {
onOmission: (omission) =>
context.omissions.push({
...omission,
path: normalizeArtifactPath(
prefix.length === 0 ? omission.path : `${prefix}/${omission.path}`,
),
}),
})
[Symbol.asyncIterator]();
const visitArtifactEntries = async (
context: ScanContext,
currentReader: ArtifactReader,
@@ -117,7 +152,7 @@ const visitArtifactEntries = async (
{
reader: currentReader,
prefix,
iterator: currentReader.entries(context.signal)[Symbol.asyncIterator](),
iterator: inventoryEntries(context, currentReader, prefix),
},
];
let failure: { readonly cause: unknown } | undefined;
@@ -191,7 +226,7 @@ const visitArtifactEntries = async (
stack.push({
reader: nested,
prefix: logicalPath,
iterator: nested.entries(context.signal)[Symbol.asyncIterator](),
iterator: inventoryEntries(context, nested, logicalPath),
});
}
}
+3 -2
View File
@@ -68,7 +68,7 @@ export const androidApplicationProjectionResultSchema = z.strictObject({
// Count completeness is separate from the declared coverage status.
inventory_complete: z.boolean(),
}),
limitations: z.array(z.string().min(1)),
limitations: z.array(z.string()),
});
export type AndroidApplicationProjectionInput = z.infer<
@@ -99,7 +99,8 @@ export const projectAndroidApplication = (
);
const components = classified;
const limitations = [
...(!hasCompleteInventoryCoverage(inventory)
...inventory.limitations,
...(!hasCompleteInventoryCoverage(inventory, "container-entries")
? ["Source inventory coverage is partial; absence is unknown."]
: []),
"Manifest, resource, signing, and bytecode semantics require a dedicated Android provider; this projection reports exact inventory paths and hashes only.",
+18 -12
View File
@@ -94,7 +94,7 @@ export const appleApplicationProjectionResultSchema = z.strictObject({
// Count completeness is separate from the declared coverage status.
inventory_complete: z.boolean(),
}),
limitations: z.array(z.string().min(1)),
limitations: z.array(z.string()),
});
export type AppleApplicationProjectionInput = z.infer<
@@ -169,18 +169,24 @@ export const projectAppleApplication = (
);
const uninventoriedImage =
rootFormat === "dmg" && inventory.occurrences.length === 1;
const inventoryComplete = hasCompleteInventoryCoverage(inventory);
const inventoryComplete = hasCompleteInventoryCoverage(
inventory,
"container-entries",
);
const complete = inventoryComplete && !uninventoriedImage;
const limitations = projectionLimitations({
complete: inventoryComplete,
uninventoriedImage,
roots,
bundles,
sidecars: entries.some(({ path }) => isSidecar(path)),
symlinks: entries.some(({ kind }) => kind === "symlink"),
unattributed,
bridgeCandidateCoverage: bridgeProjection.coverage,
});
const limitations = [
...inventory.limitations,
...projectionLimitations({
complete: inventoryComplete,
uninventoriedImage,
roots,
bundles,
sidecars: entries.some(({ path }) => isSidecar(path)),
symlinks: entries.some(({ kind }) => kind === "symlink"),
unattributed,
bridgeCandidateCoverage: bridgeProjection.coverage,
}),
];
const withoutId = {
root_sha256: inventory.manifest.root_sha256,
root_format: rootFormat,
+53 -12
View File
@@ -104,13 +104,51 @@ export const compareArtifacts = (
const right = parseArtifactInventoryEvidence(rightEvidence);
const leftComplete = left.inventory.complete;
const rightComplete = right.inventory.complete;
const leftCovered = hasCompleteInventoryCoverage(left.inventory);
const rightCovered = hasCompleteInventoryCoverage(right.inventory);
const sameScope =
left.inventory.coverage.scope === right.inventory.coverage.scope;
const rootCoverageComplete =
sameScope &&
left.inventory.complete &&
right.inventory.complete &&
left.inventory.coverage.status === "complete" &&
right.inventory.coverage.status === "complete";
const entryCoverageComplete =
rootCoverageComplete &&
left.inventory.coverage.scope === "container-entries";
const leftCovered =
sameScope &&
hasCompleteInventoryCoverage(left.inventory, left.inventory.coverage.scope);
const rightCovered =
sameScope &&
hasCompleteInventoryCoverage(
right.inventory,
right.inventory.coverage.scope,
);
const fullyCovered =
leftCovered &&
rightCovered &&
(entryCoverageComplete ||
(rootCoverageComplete &&
left.inventory.manifest.occurrence_count === 1 &&
right.inventory.manifest.occurrence_count === 1));
const limitations = [
...left.inventory.limitations.map((item) => `Left: ${item}`),
...right.inventory.limitations.map((item) => `Right: ${item}`),
...(leftComplete ? [] : ["Left artifact inventory is incomplete."]),
...(rightComplete ? [] : ["Right artifact inventory is incomplete."]),
...(sameScope ? [] : ["Artifact inventory scopes differ."]),
...(sameScope &&
left.inventory.coverage.scope === "root" &&
(left.inventory.manifest.occurrence_count > 1 ||
right.inventory.manifest.occurrence_count > 1)
? ["Container entry coverage is unavailable for child paths."]
: []),
...(left.inventory.coverage.status === "complete"
? []
: ["Left artifact inventory traversal coverage is unknown."]),
...(right.inventory.coverage.status === "complete"
? []
: ["Right artifact inventory traversal coverage is unknown."]),
];
const changes = compareOccurrences(
left.inventory,
@@ -119,7 +157,7 @@ export const compareArtifacts = (
...left.evidence.map(({ evidence_id: id }) => id),
...right.evidence.map(({ evidence_id: id }) => id),
],
leftCovered && rightCovered,
{ root: rootCoverageComplete, entries: entryCoverageComplete },
);
applyRootHashChange(left.inventory, right.inventory, changes, [
...left.evidence.map(({ evidence_id: id }) => id),
@@ -129,7 +167,7 @@ export const compareArtifacts = (
left.inventory,
right.inventory,
changes,
leftCovered && rightCovered,
fullyCovered,
);
return artifactComparisonResultSchema.parse({
status:
@@ -139,7 +177,7 @@ export const compareArtifacts = (
({ classification }) => classification === "contradiction",
)
? "contradiction"
: !leftCovered || !rightCovered
: !fullyCovered
? "unknown"
: changes.some(({ classification }) => classification === "unknown")
? "unknown"
@@ -158,7 +196,7 @@ const compareOccurrences = (
left: InventorySet,
right: InventorySet,
evidenceLinks: readonly string[],
complete: boolean,
coverage: { readonly root: boolean; readonly entries: boolean },
): ArtifactChange[] => {
const leftByPath = new Map(
left.occurrences.map((item) => [item.logical_path, item]),
@@ -187,7 +225,8 @@ const compareOccurrences = (
rightNode: nodeFor(rightOccurrence, rightNodes),
leftRelations: leftRelations.get(path) ?? [],
rightRelations: rightRelations.get(path) ?? [],
complete,
rootCovered: coverage.root,
entriesCovered: coverage.entries,
evidenceLinks,
});
if (change !== null) output.push(change);
@@ -203,7 +242,8 @@ interface ClassifyPathInput {
readonly rightNode: ArtifactNode | undefined;
readonly leftRelations: readonly RelationProjection[];
readonly rightRelations: readonly RelationProjection[];
readonly complete: boolean;
readonly rootCovered: boolean;
readonly entriesCovered: boolean;
readonly evidenceLinks: readonly string[];
}
@@ -217,17 +257,18 @@ const buildArtifactChangeBase = (input: ClassifyPathInput) => ({
});
const classifyPresence = (input: ClassifyPathInput): ArtifactChange | null => {
const covered = input.path === "." ? input.rootCovered : input.entriesCovered;
if (input.leftOccurrence === undefined)
return {
...buildArtifactChangeBase(input),
classification: input.complete ? "added" : "unknown",
dimensions: [input.complete ? "content" : "availability"],
classification: covered ? "added" : "unknown",
dimensions: [covered ? "content" : "availability"],
};
if (input.rightOccurrence === undefined)
return {
...buildArtifactChangeBase(input),
classification: input.complete ? "removed" : "unknown",
dimensions: [input.complete ? "content" : "availability"],
classification: covered ? "removed" : "unknown",
dimensions: [covered ? "content" : "availability"],
};
return null;
};
+13 -6
View File
@@ -188,8 +188,11 @@ export const artifactGraphManifestSchema = z.object({
edge_count: z.number().int().min(0),
});
/** Whether the inventory producer covered its modeled child-entry scope. */
export const artifactInventoryCoverageSchema = z.enum(["complete", "partial"]);
/** Producer-owned traversal coverage; content certainty remains on each occurrence. */
export const artifactInventoryCoverageSchema = z.strictObject({
scope: z.enum(["root", "container-entries"]),
status: z.enum(["complete", "unknown"]),
});
export type ArtifactInventoryCoverage = z.infer<
typeof artifactInventoryCoverageSchema
>;
@@ -202,15 +205,19 @@ export const artifactInventoryResultSchema = z.object({
edges: z.array(artifactEdgeSchema),
provenance: z.array(artifactCommandSchema),
integrity_contradictions: z.array(integrityContradictionSchema).default([]),
// Omitted by older retained Evidence; the inventory parser owns its compatibility policy.
coverage: artifactInventoryCoverageSchema.optional(),
limitations: z.array(z.string()),
/** Optional only for persisted inventory Evidence written before this field. */
coverage_status: artifactInventoryCoverageSchema.optional(),
coverage_status: z.enum(["complete", "partial"]).optional(),
});
/** Current inventory producers always classify the coverage they examined. */
/** Current producers always declare coverage; historical Evidence inputs may omit it. */
export const artifactInventoryOutputSchema =
artifactInventoryResultSchema.extend({
coverage_status: artifactInventoryCoverageSchema,
artifactInventoryResultSchema.safeExtend({
coverage: artifactInventoryCoverageSchema,
coverage_status:
artifactInventoryResultSchema.shape.coverage_status.unwrap(),
});
export type ArtifactInventoryOutput = z.infer<
typeof artifactInventoryOutputSchema
+89 -31
View File
@@ -18,7 +18,10 @@ import {
} from "./artifactIdentity.js";
import { parseEvidence, type Evidence } from "./evidence.js";
import { artifactInspectionResultSchema } from "./artifactInspection.js";
import { ZIP_NON_ENTRY_TAIL_LIMITATION } from "./zipPackageFormat.js";
import {
isZipPackageFormat,
ZIP_NON_ENTRY_TAIL_LIMITATION,
} from "./zipPackageFormat.js";
/** Complete or partially assembled inventory used by comparison workflows. */
export interface InventorySet {
@@ -30,13 +33,17 @@ export interface InventorySet {
readonly limitations: readonly string[];
/** True when merged node, occurrence, and edge counts match the manifest. */
readonly complete: boolean;
readonly coverage_status: ArtifactInventoryCoverage;
readonly coverage: ArtifactInventoryCoverage;
}
/** Whether all graph members are present and the producer reports full coverage. */
export const hasCompleteInventoryCoverage = (
inventory: InventorySet,
): boolean => inventory.complete && inventory.coverage_status === "complete";
requiredScope: ArtifactInventoryCoverage["scope"],
): boolean =>
inventory.complete &&
inventory.coverage.scope === requiredScope &&
inventory.coverage.status === "complete";
export interface ParsedInventorySet {
readonly evidence: readonly Evidence[];
@@ -132,11 +139,14 @@ const assembleInventoryResults = (
const first = pages[0];
if (first === undefined)
throw new TypeError("Artifact inventory requires Evidence pages");
const coverageStatuses = new Set(pages.map(resolveInventoryCoverageStatus));
if (coverageStatuses.size !== 1)
throw new TypeError("Artifact inventory pages disagree on coverage status");
const coverageStatus = [...coverageStatuses][0]!;
const manifestJson = canonicalJson(first.manifest, "Artifact inventory");
const coverageJson = canonicalJson(
{
coverage: first.coverage ?? null,
coverage_status: first.coverage_status ?? null,
},
"Artifact inventory coverage",
);
const nodes = new Map<string, ArtifactNode>();
const occurrences = new Map<string, ArtifactOccurrence>();
const paths = new Map<string, ArtifactOccurrence>();
@@ -144,6 +154,18 @@ const assembleInventoryResults = (
for (const page of pages) {
if (canonicalJson(page.manifest, "Artifact inventory") !== manifestJson)
throw new TypeError("Artifact inventory pages do not share one manifest");
if (
canonicalJson(
{
coverage: page.coverage ?? null,
coverage_status: page.coverage_status ?? null,
},
"Artifact inventory coverage",
) !== coverageJson
)
throw new TypeError(
"Artifact inventory pages do not share one coverage declaration",
);
if (
canonicalJson(page.integrity_contradictions, "Artifact inventory") !==
canonicalJson(first.integrity_contradictions, "Artifact inventory")
@@ -165,12 +187,11 @@ const assembleInventoryResults = (
for (const edge of page.edges)
mergeExact(edges, edge.edge_id, edge, "edge");
}
const inventory = orderedInventory(
first,
pages,
{ nodes, occurrences, edges },
coverageStatus,
);
const inventory = orderedInventory(first, pages, {
nodes,
occurrences,
edges,
});
if (inventory.complete) validateCompleteInventory(inventory);
return inventory;
};
@@ -186,7 +207,6 @@ const orderedInventory = (
ArtifactInventoryResult["edges"][number]
>;
},
coverageStatus: ArtifactInventoryCoverage,
): InventorySet => {
const nodes = [...values.nodes.values()].sort((left, right) =>
compareUnicodeCodePoints(left.artifact_id, right.artifact_id),
@@ -197,40 +217,78 @@ const orderedInventory = (
const edges = [...values.edges.values()].sort((left, right) =>
compareUnicodeCodePoints(left.edge_id, right.edge_id),
);
const limitations = [
...new Set(pages.flatMap((inventory) => inventory.limitations)),
].sort(compareUnicodeCodePoints);
return {
manifest: first.manifest,
nodes,
occurrences,
edges,
integrityContradictions: first.integrity_contradictions,
limitations: [
...new Set(pages.flatMap((inventory) => inventory.limitations)),
].sort(compareUnicodeCodePoints),
limitations,
coverage: resolveInventoryCoverage({ ...first, limitations }),
complete:
nodes.length === first.manifest.node_count &&
occurrences.length === first.manifest.occurrence_count &&
edges.length === first.manifest.edge_count,
coverage_status: coverageStatus,
};
};
const resolveInventoryCoverageStatus = (
// Normalize both retained coverage formats at this owning boundary.
const resolveInventoryCoverage = (
inventory: ArtifactInventoryResult,
): ArtifactInventoryCoverage => {
if (inventory.coverage_status !== undefined) return inventory.coverage_status;
// Older retained Evidence has no typed status. Keep only its known
// informational notices complete; unknown limitation text fails closed.
return inventory.limitations.every(isKnownNonCoverageNotice)
? "complete"
: "partial";
const legacy = legacyInventoryCoverage(
inventory.manifest.root_format,
inventory.limitations,
inventory.integrity_contradictions.length,
);
if (inventory.coverage !== undefined) {
if (
inventory.coverage_status !== undefined &&
(inventory.coverage.status === "complete") !==
(inventory.coverage_status === "complete")
)
throw new TypeError(
"Artifact inventory has conflicting coverage declarations",
);
return inventory.coverage;
}
return inventory.coverage_status === undefined
? legacy
: {
...legacy,
status:
inventory.coverage_status === "complete" ? "complete" : "unknown",
};
};
const isKnownNonCoverageNotice = (limitation: string): boolean =>
limitation === "Artifact has no child container entries." ||
limitation === ZIP_NON_ENTRY_TAIL_LIMITATION ||
/^\d+ integrity contradiction\(s\) were recorded; mismatched content is observed-untrusted\.$/u.test(
limitation,
);
// Retained pre-coverage records cannot classify arbitrary prose. Recognize only
// the historical producer vocabulary here; unfamiliar diagnostics remain unknown.
const legacyInventoryCoverage = (
format: ArtifactInventoryResult["manifest"]["root_format"],
limitations: readonly string[],
contradictionCount: number,
): ArtifactInventoryCoverage => ({
scope:
isZipPackageFormat(format) ||
["directory", "asar", "dmg", "pkg", "mach-o-universal"].includes(format)
? "container-entries"
: "root",
status: limitations.some(
(limitation) =>
limitation !== "Artifact has no child container entries." &&
limitation !== ZIP_NON_ENTRY_TAIL_LIMITATION &&
!(
contradictionCount > 0 &&
limitation ===
`${String(contradictionCount)} integrity contradiction(s) were recorded; mismatched content is observed-untrusted.`
),
)
? "unknown"
: "complete",
});
const validateCompleteInventory = (inventory: InventorySet): void => {
const nodeIds = new Set(inventory.nodes.map(({ artifact_id: id }) => id));
+4 -1
View File
@@ -7,7 +7,9 @@ import type {
/** Immutable inventory produced by one complete artifact scan. */
export interface ArtifactInventorySnapshot {
readonly coverage_status: ArtifactInventoryCoverage;
readonly coverage_status: NonNullable<
ArtifactInventoryResult["coverage_status"]
>;
readonly manifest: ArtifactInventoryResult["manifest"];
readonly nodes: readonly ArtifactNode[];
readonly occurrences: ArtifactInventoryResult["occurrences"];
@@ -16,5 +18,6 @@ export interface ArtifactInventorySnapshot {
ArtifactInventoryResult["provenance"][number]
>;
readonly integrity_contradictions: readonly IntegrityContradiction[];
readonly coverage?: ArtifactInventoryCoverage;
readonly limitations: readonly string[];
}
+3 -2
View File
@@ -72,7 +72,7 @@ export const harmonyApplicationProjectionResultSchema = z.strictObject({
// Count completeness is separate from the declared coverage status.
inventory_complete: z.boolean(),
}),
limitations: z.array(z.string().min(1)),
limitations: z.array(z.string()),
});
export type HarmonyApplicationProjectionInput = z.infer<
@@ -112,7 +112,8 @@ export const projectHarmonyApplication = (
classified.native_libraries,
);
const limitations = [
...(!hasCompleteInventoryCoverage(inventory)
...inventory.limitations,
...(!hasCompleteInventoryCoverage(inventory, "container-entries")
? ["Source inventory coverage is partial; absence is unknown."]
: []),
...(unavailable.length > 0
@@ -56,7 +56,8 @@ export const projectionCoverage = (
inventory_complete: boolean;
} => ({
status:
hasCompleteInventoryCoverage(inventory) && budgetComplete
hasCompleteInventoryCoverage(inventory, "container-entries") &&
budgetComplete
? ("complete-within-inventory" as const)
: ("partial" as const),
inventory_complete: inventory.complete,
@@ -0,0 +1,432 @@
import { chmod, mkdir, writeFile } from "node:fs/promises";
import { createServer } from "node:net";
import { join } from "node:path";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
import { expect } from "vitest";
import {
artifactInventoryResultSchema,
type ArtifactInventoryResult,
} from "../../../src/domain/artifactGraph.js";
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
import {
createEvidence,
parseEvidence,
type Evidence,
} from "../../../src/domain/evidence.js";
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
const plist = (message: string) =>
`<?xml version="1.0" encoding="UTF-8"?>\n<plist version="1.0"><dict><key>message</key><string>${message}</string></dict></plist>\n`;
const inventoryEvidence = (input: unknown): Evidence => {
const inspection = parseEvidence(input);
const inventory = artifactInspectionResultSchema
.parse(inspection.normalized_result)
.substeps.find(
({ evidence }) => evidence.operation === "inventory_artifact",
)?.evidence;
if (inventory === undefined) throw new Error("Expected inventory Evidence");
return parseEvidence(inventory);
};
const inspectPair = async (
cli: TestCli,
leftPath: string,
rightPath: string,
) => {
const result = await cli.run({
arguments: ["inspect-artifact", leftPath, "--json"],
});
expect(result.exitCode, `${result.stderr}\n${result.stdout}`).toBe(0);
const left = inventoryEvidence(result.json);
const { call } = await connectLocalToolsMcp();
const opened = await call("open_binary", { path: rightPath });
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
const inspected = await call("inspect_artifact", {});
expect(inspected.isError, JSON.stringify(inspected)).not.toBe(true);
return { left, right: inventoryEvidence(inspected.structuredContent), call };
};
const assertCoverageControls = async ({
left,
right,
inventory,
compare,
}: {
left: Evidence;
right: Evidence;
inventory: ArtifactInventoryResult;
compare: (left: Evidence, right: Evidence) => Promise<Evidence>;
}) => {
// Authenticated derived controls isolate missing members and genuine coverage
// gaps; they do not claim that the complete plist producer omitted children.
const derive = (result: unknown) =>
createEvidence(
{
path: right.subject?.local_path ?? "",
sha256: inventory.manifest.root_sha256,
format: "plist",
},
right.provider,
{
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse(result),
confidence: "derived",
authority: "shipped-artifact",
},
);
const partial = await compare(left, derive({ ...inventory, nodes: [] }));
expect(partial.normalized_result).toMatchObject({
status: "truncated",
summary: { unchanged: 0, unknown: 1 },
});
const reworded = await compare(
left,
derive({
...inventory,
coverage: { scope: "root", status: "complete" },
limitations: ["Root bytes observed; no container children are modeled."],
}),
);
expect(reworded.normalized_result).toMatchObject({
status: "unchanged",
summary: { unchanged: 1, unknown: 0 },
limitations: expect.arrayContaining([
"Right: Root bytes observed; no container children are modeled.",
]),
});
const gap = await compare(
left,
derive({
...inventory,
coverage: { scope: "root", status: "unknown" },
coverage_status: "partial",
limitations: [],
}),
);
expect(gap.normalized_result).toMatchObject({
status: "unknown",
summary: { unchanged: 0, unknown: 1 },
});
const differentScope = await compare(
left,
derive({
...inventory,
coverage: { scope: "container-entries", status: "complete" },
}),
);
expect(differentScope.normalized_result).toMatchObject({
status: "unknown",
limitations: expect.arrayContaining(["Artifact inventory scopes differ."]),
});
const { coverage, coverage_status: _coverageStatus, ...legacy } = inventory;
expect(coverage).toBeDefined();
const retained = await compare(left, derive(legacy));
expect(retained.normalized_result).toMatchObject({
status: "unchanged",
summary: { unchanged: 1, unknown: 0 },
});
for (const limitation of [
"Child inventory is unavailable.",
"Missing child observations; 1 integrity contradiction(s) were recorded.",
]) {
const unfamiliarLegacy = await compare(
left,
derive({ ...legacy, limitations: [limitation] }),
);
expect(unfamiliarLegacy.normalized_result).toMatchObject({
status: "unknown",
summary: { unchanged: 0, unknown: 1 },
});
}
};
cliTest(
"compares fully observed roots without inventing coverage gaps through CLI and MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-artifact-coverage-");
const leftPath = join(root, "left.plist");
const rightPath = join(root, "right.plist");
await Promise.all([
writeFile(leftPath, plist("hello")),
writeFile(rightPath, plist("hello")),
]);
const leftResult = await cli.run({
arguments: ["inspect-artifact", leftPath, "--json"],
});
expect(leftResult.exitCode, leftResult.stderr).toBe(0);
const left = inventoryEvidence(leftResult.json);
const { call } = await connectLocalToolsMcp();
const inspect = async (path: string) => {
const opened = await call("open_binary", { path });
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
const result = await call("inspect_artifact", {});
expect(result.isError, JSON.stringify(result)).not.toBe(true);
return inventoryEvidence(result.structuredContent);
};
const compare = async (left: Evidence, right: Evidence) => {
const result = await call("compare_artifacts", { left, right });
expect(result.isError, JSON.stringify(result)).not.toBe(true);
return parseEvidence(result.structuredContent);
};
const right = await inspect(rightPath);
const inventory = artifactInventoryResultSchema.parse(
right.normalized_result,
);
expect(inventory.coverage).toEqual({ scope: "root", status: "complete" });
expect(inventory.manifest).toMatchObject({
root_format: "plist",
node_count: 1,
occurrence_count: 1,
edge_count: 0,
});
expect(inventory.limitations).toEqual([
"Artifact has no child container entries.",
]);
const equal = await compare(left, right);
expect(equal.normalized_result).toMatchObject({
status: "unchanged",
summary: { unchanged: 1, unknown: 0 },
changes: [],
limitations: [
"Left: Artifact has no child container entries.",
"Right: Artifact has no child container entries.",
],
});
const behavior = await call("find_changed_behavior", {
comparisons: [equal],
});
expect(behavior.isError, JSON.stringify(behavior)).not.toBe(true);
expect(behavior.structuredContent).toMatchObject({
normalized_result: {
behavior_status: "unknown",
summary: { unresolved: 0 },
findings: { items: [] },
limitations: expect.arrayContaining([
"No process comparison Evidence was supplied.",
]),
},
});
const unknowns = await call("list_unknowns", {
domain: "artifact-comparison",
});
expect(unknowns.isError, JSON.stringify(unknowns)).not.toBe(true);
expect(unknowns.structuredContent).toMatchObject({
result: { items: [], total: 0 },
});
await writeFile(rightPath, plist("world"));
const changed = await compare(left, await inspect(rightPath));
expect(changed.normalized_result).toMatchObject({
status: "changed",
summary: { unchanged: 0, changed: 1, unknown: 0 },
changes: [
expect.objectContaining({ logical_path: ".", dimensions: ["content"] }),
],
});
const zipPaths = [join(root, "left.zip"), join(root, "right.zip")] as const;
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("same.txt", new TextReader("same"));
const bytes = await writer.close();
await Promise.all(zipPaths.map((path) => writeFile(path, bytes)));
const zip = await compare(
await inspect(zipPaths[0]),
await inspect(zipPaths[1]),
);
expect(zip.normalized_result).toMatchObject({
status: "unchanged",
summary: { unchanged: 2, unknown: 0 },
changes: [],
});
await assertCoverageControls({ left, right, inventory, compare });
},
30_000,
);
cliTest.skipIf(process.platform === "win32")(
"preserves executable metadata changes for fully observed plist roots",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-artifact-mode-");
const leftPath = join(root, "left.plist");
const rightPath = join(root, "right.plist");
await Promise.all([
writeFile(leftPath, plist("hello")),
writeFile(rightPath, plist("hello")),
]);
await Promise.all([chmod(leftPath, 0o644), chmod(rightPath, 0o755)]);
const result = await cli.run({
arguments: ["inspect-artifact", leftPath, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const left = inventoryEvidence(result.json);
const { call } = await connectLocalToolsMcp();
const opened = await call("open_binary", { path: rightPath });
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
const inspected = await call("inspect_artifact", {});
expect(inspected.isError, JSON.stringify(inspected)).not.toBe(true);
const right = inventoryEvidence(inspected.structuredContent);
expect(right.subject?.digest.sha256).toBe(left.subject?.digest.sha256);
const comparison = await call("compare_artifacts", { left, right });
expect(comparison.isError, JSON.stringify(comparison)).not.toBe(true);
expect(
parseEvidence(comparison.structuredContent).normalized_result,
).toMatchObject({
status: "changed",
summary: { unchanged: 0, changed: 1, unknown: 0 },
changes: [
expect.objectContaining({
logical_path: ".",
dimensions: ["executable"],
}),
],
});
},
30_000,
);
cliTest.skipIf(process.platform === "win32")(
"retains traversal uncertainty when a directory omits special filesystem entries",
async ({ cli, processes }) => {
const root = await createTestTempDirectory("rea-gap-");
const leftPath = join(root, "l.app"),
rightPath = join(root, "r.app");
await Promise.all([mkdir(leftPath), mkdir(rightPath)]);
await Promise.all([
writeFile(join(leftPath, "same.txt"), "same"),
writeFile(join(rightPath, "same.txt"), "same"),
]);
await Promise.all(
[leftPath, rightPath].flatMap((path) => [
writeFile(
join(path, "Info.plist"),
"<plist><dict><key>CFBundleExecutable</key><string>main.js</string></dict></plist>",
),
writeFile(join(path, "main.js"), "export const value = 1;"),
]),
);
const fifo = await processes.run("mkfifo", [join(leftPath, "p")]);
expect(fifo.exitCode, fifo.stderr).toBe(0);
const socket = createServer();
await new Promise<void>((resolve, reject) => {
socket.once("error", reject);
socket.listen(join(leftPath, "s"), resolve);
});
try {
const { left, right, call } = await inspectPair(cli, leftPath, rightPath);
const inventory = artifactInventoryResultSchema.parse(
left.normalized_result,
);
expect(inventory.coverage).toEqual({
scope: "container-entries",
status: "unknown",
});
expect(inventory.limitations).toEqual(
expect.arrayContaining([
'Artifact traversal omitted fifo at "p".',
'Artifact traversal omitted socket at "s".',
]),
);
const compared = await call("compare_artifacts", { left, right });
expect(compared.isError, JSON.stringify(compared)).not.toBe(true);
expect(
parseEvidence(compared.structuredContent).normalized_result,
).toMatchObject({ status: "unknown", summary: { unchanged: 0 } });
} finally {
await new Promise<void>((resolve, reject) =>
socket.close((cause) =>
cause === undefined ? resolve() : reject(cause),
),
);
}
},
30_000,
);
cliTest(
"compares fully traversed case-distinct ZIP entries while retaining materialization limits",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-case-coverage-");
const leftPath = join(root, "left.zip"),
rightPath = join(root, "right.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("A", new TextReader("upper"));
await writer.add("a", new TextReader("lower"));
const bytes = await writer.close();
await Promise.all([
writeFile(leftPath, bytes),
writeFile(rightPath, bytes),
]);
const { left, right, call } = await inspectPair(cli, leftPath, rightPath);
const inventory = artifactInventoryResultSchema.parse(
left.normalized_result,
);
expect(inventory.coverage).toEqual({
scope: "container-entries",
status: "complete",
});
expect(
inventory.occurrences.map(({ logical_path }) => logical_path).sort(),
).toEqual([".", "A", "a"]);
const compared = await call("compare_artifacts", { left, right });
expect(compared.isError, JSON.stringify(compared)).not.toBe(true);
expect(
parseEvidence(compared.structuredContent).normalized_result,
).toMatchObject({
status: "unchanged",
summary: { unchanged: 3, unknown: 0 },
changes: [],
limitations: expect.arrayContaining([
expect.stringContaining("case-insensitive destination"),
]),
});
},
30_000,
);
cliTest(
"keeps unreadable encrypted ZIP content unknown with complete traversal",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-encrypted-comparison-");
const leftPath = join(root, "left.zip");
const rightPath = join(root, "right.zip");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add("secret.txt", new TextReader("secret bytes"), {
password: "fixture-password",
});
const bytes = await writer.close();
await Promise.all([
writeFile(leftPath, bytes),
writeFile(rightPath, bytes),
]);
const { left, right, call } = await inspectPair(cli, leftPath, rightPath);
for (const evidence of [left, right])
expect(
artifactInventoryResultSchema.parse(evidence.normalized_result),
).toMatchObject({
coverage: { scope: "container-entries", status: "complete" },
occurrences: expect.arrayContaining([
expect.objectContaining({
logical_path: "secret.txt",
hash_status: "unavailable",
encrypted: true,
}),
]),
});
const compared = await call("compare_artifacts", { left, right });
expect(compared.isError, JSON.stringify(compared)).not.toBe(true);
const result = parseEvidence(compared.structuredContent).normalized_result;
expect(result).toMatchObject({
status: "unknown",
summary: { unknown: expect.any(Number) },
});
expect(result).toHaveProperty("summary.unknown", 1);
},
);
@@ -228,7 +228,7 @@ describe("case-distinct APK resource names", () => {
await runProviderAnalysis(path, "inventory_artifact", {}),
);
expect(inventory.normalized_result).toMatchObject({
coverage_status: "partial",
coverage_status: "complete",
});
const result = projectAndroidApplicationEvidence({
inventory_evidence: [inventory],
@@ -238,11 +238,11 @@ describe("case-distinct APK resource names", () => {
);
expect(projection.root_format).toBe("apk");
expect(projection.coverage).toEqual({
status: "partial",
status: "complete-within-inventory",
inventory_complete: true,
});
expect(projection.limitations).toContain(
"Source inventory coverage is partial; absence is unknown.",
expect(projection.limitations.join(" ")).toContain(
"case-insensitive destination",
);
expect(projection.components.manifests).toHaveLength(1);
expect(projection.components.dex).toHaveLength(1);
@@ -75,7 +75,7 @@ async function createCompleteAppleProjection() {
}
describe("Apple application projection", () => {
it("keeps a case-colliding real IPA inventory partial", async () => {
it("retains complete traversal and case-collision limits for a real IPA", async () => {
const root = await createTestTempDirectory("rea-apple-case-collision-");
const path = join(root, "CaseCollision.ipa");
const writer = new ZipWriter(new Uint8ArrayWriter());
@@ -93,7 +93,7 @@ describe("Apple application projection", () => {
await runProviderAnalysis(path, "inventory_artifact", {}),
);
expect(inventory.normalized_result).toMatchObject({
coverage_status: "partial",
coverage_status: "complete",
});
const result = projectAppleApplicationEvidence({
inventory_evidence: [inventory],
@@ -103,11 +103,11 @@ describe("Apple application projection", () => {
);
expect(projection.coverage).toEqual({
status: "partial",
status: "complete-within-inventory",
inventory_complete: true,
});
expect(projection.limitations).toContain(
"Source inventory coverage is partial; absence is unknown.",
expect(projection.limitations.join(" ")).toContain(
"case-insensitive destination",
);
});
@@ -77,7 +77,11 @@ describe("leaf artifact comparison", () => {
// Persisted inventory Evidence predates coverage_status. The known leaf
// notice remains complete when normalized at the inventory boundary.
const parsed = artifactInventoryResultSchema.parse(left.normalized_result);
const { coverage_status: _coverageStatus, ...legacyInventory } = parsed;
const {
coverage: _coverage,
coverage_status: _coverageStatus,
...legacyInventory
} = parsed;
const legacy = createEvidence(
{
path: left.subject!.local_path,
@@ -157,7 +161,7 @@ describe("artifact inventory coverage", () => {
const graph = artifactInventoryResultSchema.parse(
complete.normalized_result,
);
const partial = createEvidence(
const contradictory = createEvidence(
{
path: complete.subject!.local_path,
sha256: complete.subject!.digest.sha256,
@@ -168,6 +172,29 @@ describe("artifact inventory coverage", () => {
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse({ ...graph, coverage_status: "partial" }),
confidence: "derived",
authority: "shipped-artifact",
},
);
expect(() => parseArtifactInventoryEvidence(contradictory)).toThrow(
"Artifact inventory has conflicting coverage declarations",
);
const { coverage: _coverage, ...legacyStatusGraph } = graph;
const partial = createEvidence(
{
path: complete.subject!.local_path,
sha256: complete.subject!.digest.sha256,
format: complete.subject!.format,
},
PROVIDER,
{
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse({
...legacyStatusGraph,
coverage_status: "partial",
}),
confidence: "observed",
authority: "shipped-artifact",
},
@@ -180,7 +207,8 @@ describe("artifact inventory coverage", () => {
summary: { unchanged: 0, unknown: 1 },
});
const { coverage_status: _coverageStatus, ...legacyGraph } = graph;
const { coverage_status: _coverageStatus, ...legacyGraph } =
legacyStatusGraph;
const unknownLegacy = createEvidence(
{
path: complete.subject!.local_path,
@@ -219,6 +247,7 @@ describe("artifact inventory coverage", () => {
const fragmentGraph = artifactInventoryResultSchema.parse(
fragment.normalized_result,
);
const { coverage: _coverage, ...legacyFragmentGraph } = fragmentGraph;
const mixed = createEvidence(
{
path: fragment.subject!.local_path,
@@ -230,7 +259,7 @@ describe("artifact inventory coverage", () => {
operation: "inventory_artifact",
parameters: fragment.parameters,
result: jsonValueSchema.parse({
...fragmentGraph,
...legacyFragmentGraph,
coverage_status: "partial",
}),
confidence: "derived",
@@ -239,7 +268,7 @@ describe("artifact inventory coverage", () => {
);
expect(() =>
parseArtifactInventoryEvidence([firstFragment, mixed]),
).toThrow("Artifact inventory pages disagree on coverage status");
).toThrow("Artifact inventory pages do not share one coverage declaration");
});
});
@@ -314,6 +343,61 @@ describe("artifact comparison", () => {
});
describe("artifact comparison completeness", () => {
it("does not infer child changes from root-only coverage", async () => {
const root = await createTestTempDirectory("rea-artifact-root-coverage-");
const leftPath = join(root, "left.app");
const rightPath = join(root, "right.app");
await Promise.all([mkdir(leftPath), mkdir(rightPath)]);
await Promise.all([
writeFile(join(leftPath, "left-only.txt"), "left"),
writeFile(join(rightPath, "right-only.txt"), "right"),
]);
const [left, right] = await Promise.all([
observe(leftPath),
observe(rightPath),
]);
const rootOnly = (evidence: Awaited<ReturnType<typeof observe>>) => {
const inventory = artifactInventoryResultSchema.parse(
evidence.normalized_result,
);
return createEvidence(
{
path: evidence.subject?.local_path ?? "",
sha256: inventory.manifest.root_sha256,
format: inventory.manifest.root_format,
},
PROVIDER,
{
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse({
...inventory,
coverage: { scope: "root", status: "complete" },
}),
confidence: "derived",
authority: "shipped-artifact",
},
);
};
const compared = compareArtifacts(rootOnly(left), rootOnly(right));
expect(compared.status).toBe("unknown");
expect(compared.summary).toMatchObject({ added: 0, removed: 0 });
expect(
compared.changes
.filter(({ logical_path }) => logical_path !== ".")
.map(({ logical_path, classification }) => ({
logical_path,
classification,
})),
).toEqual([
{ logical_path: "left-only.txt", classification: "unknown" },
{ logical_path: "right-only.txt", classification: "unknown" },
]);
expect(compared.limitations).toContain(
"Container entry coverage is unavailable for child paths.",
);
});
it("compares every graph member for inventories larger than 500 entries", async () => {
const root = await createTestTempDirectory("rea-artifact-pages-");
const entryCount = 501;
@@ -415,6 +499,30 @@ describe("artifact comparison completeness", () => {
evidence_links: citations,
})),
});
const fragment = leftFragments[0];
if (fragment === undefined) throw new Error("Expected inventory fragments");
const conflictingScope = createEvidence(
{
path: leftPath,
sha256: left.subject?.digest.sha256 ?? "",
format: "directory",
},
PROVIDER,
{
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse({
...artifactInventoryResultSchema.parse(fragment.normalized_result),
coverage: { scope: "root", status: "complete" },
}),
},
);
expect(() =>
compareArtifacts(
[conflictingScope, ...leftFragments.slice(1)],
rightFragments,
),
).toThrow(/coverage declaration/u);
}, 15_000);
it("rejects non-inventory and tampered Evidence", async () => {
@@ -52,3 +52,22 @@ it("rejects a root path replaced between its initial stat and open", async () =>
message: `Root artifact changed before inventory: ${path}`,
});
});
it("keeps unavailable container traversal distinct from observed root bytes", async () => {
const root = await createTestTempDirectory("rea-artifact-root-scope-");
const path = join(root, "fixture.pkg");
// The recognized XAR prefix selects PKG inventory; no child decoder is installed.
await writeFile(path, Buffer.from("xar!fixture"));
const inventory = await inventoryArtifact(path);
expect(inventory.manifest).toMatchObject({
root_format: "pkg",
node_count: 1,
occurrence_count: 1,
edge_count: 0,
});
expect(inventory.occurrences[0]?.hash_status).toBe("verified");
expect(inventory.coverage).toEqual({
scope: "container-entries",
status: "unknown",
});
});
@@ -214,7 +214,7 @@ describe("HarmonyOS packaging boundaries", () => {
});
describe("HarmonyOS inventory coverage", () => {
it("keeps a case-colliding inventory partial in the application projection", async () => {
it("retains complete traversal and case-collision limits in the application projection", async () => {
const path = await writeApplicationZip(
"rea-harmony-case-collision-",
"Fixture.hap",
@@ -225,7 +225,7 @@ describe("HarmonyOS inventory coverage", () => {
);
const inventory = await inventoryApplicationPackage(path);
expect(inventory.normalized_result).toMatchObject({
coverage_status: "partial",
coverage_status: "complete",
});
const projection = requireDeterministicProjection(
@@ -234,11 +234,11 @@ describe("HarmonyOS inventory coverage", () => {
harmonyApplicationProjectionResultSchema,
);
expect(projection.coverage).toEqual({
status: "partial",
status: "complete-within-inventory",
inventory_complete: true,
});
expect(projection.limitations).toContain(
"Source inventory coverage is partial; absence is unknown.",
expect(projection.limitations.join(" ")).toContain(
"case-insensitive destination",
);
});
@@ -254,8 +254,11 @@ const assertHistoricalInventory = async (
const { archive, parent, inventoryEvidence, graph } = input;
// Historical records may omit a later inventory field. Input parsing must
// not insert defaults inside that authenticated child Evidence.
const { integrity_contradictions: _contradictions, ...legacyInventory } =
graph;
const {
integrity_contradictions: _contradictions,
coverage: _coverage,
...legacyInventory
} = graph;
const legacyTarget = {
path: archive,
sha256: graph.manifest.root_sha256,
@@ -275,6 +278,9 @@ const assertHistoricalInventory = async (
expect(
legacyInspection.substeps[0]?.evidence.normalized_result,
).not.toHaveProperty("integrity_contradictions");
expect(
legacyInspection.substeps[0]?.evidence.normalized_result,
).not.toHaveProperty("coverage");
const legacyParent = createEvidence(legacyTarget, parent.provider, {
operation: "inspect_artifact",
parameters: {},
+27 -15
View File
@@ -1,5 +1,7 @@
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js";
import { ok as resultOk } from "../../../src/domain/result.js";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -98,20 +100,6 @@ it.each([
20_000,
);
const artifactInventoryResultSchema = z.object({
occurrences: z.array(
z.object({ logical_path: z.string(), hash_status: z.string() }),
),
integrity_contradictions: z.array(
z.object({
logical_path: z.string(),
declared_sha256: z.string(),
observed_sha256: z.string(),
trust: z.literal("observed-untrusted"),
}),
),
});
it("extracts an active archive through MCP when requested", async () => {
const root = await createTestTempDirectory("rea-artifact-extract-mcp-");
const archive = join(root, "fixture.zip");
@@ -318,6 +306,30 @@ it("records an explicitly continued mismatch, preserves verified siblings, and n
}),
]),
});
const { coverage: _coverage, ...legacyInventory } = inventory;
const historical = createEvidence(
{ path: archive, sha256: inventory.manifest.root_sha256, format: "asar" },
compact.provider,
{
operation: "inventory_artifact",
parameters: {},
result: jsonValueSchema.parse(legacyInventory),
},
);
const retainedComparison = await client.callTool({
name: "compare_artifacts",
arguments: { left: evidence, right: historical },
});
expect(
retainedComparison.isError,
JSON.stringify(retainedComparison),
).not.toBe(true);
expect(
toolEvidence(retainedComparison.structuredContent).normalized_result,
).toMatchObject({
status: "contradiction",
summary: { unchanged: 2, contradiction: 2, unknown: 0 },
});
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
@@ -17,6 +17,7 @@ import { execFileOutput } from "../../../../src/process/ExecFileOutput.js";
import {
ArtifactReaderFailure,
type ArtifactEntry,
type ArtifactTraversalOmission,
} from "../../../../src/artifacts/ArtifactReader.js";
import { ArtifactResourceScope } from "../../../../src/artifacts/ArtifactResourceScope.js";
import { DirectoryArtifactReader } from "../../../../src/artifacts/DirectoryArtifactReader.js";
@@ -473,10 +474,10 @@ describe("native DMG traversal shutdown", () => {
.mockImplementation(async function* (
this: DirectoryArtifactReader,
signal?: AbortSignal,
descendInto?: (path: string) => boolean,
selection?: Parameters<DirectoryArtifactReader["entries"]>[1],
) {
const iterator = originalEntries
.call(this, signal, descendInto)
.call(this, signal, selection)
[Symbol.asyncIterator]();
try {
for (;;) {
@@ -606,9 +607,20 @@ describe("native DMG artifact reader", () => {
undefined,
host,
);
if (mountPoint === undefined) throw new Error("expected mounted directory");
if (process.platform !== "win32")
await execFileOutput("mkfifo", [join(mountPoint, "special.pipe")]);
const entries = [];
for await (const entry of reader.entries()) entries.push(entry.path);
const omissions: ArtifactTraversalOmission[] = [];
for await (const entry of reader.entries(undefined, {
onOmission: (omission) => omissions.push(omission),
}))
entries.push(entry.path);
expect(entries).toContain("image.dmg/Fixture/hello.txt");
if (process.platform !== "win32")
expect(omissions).toEqual([
{ path: "image.dmg/Fixture/special.pipe", kind: "fifo" },
]);
const provenance = reader.provenance();
const first = provenance[0];
if (first === undefined) throw new Error("expected attachment provenance");