Files
reverse-engineering-anything/src/domain/artifactComparison.ts
T
morlutoandCursor Agent a6e02657e6 fix(artifacts): preserve unknown relationship coverage (#9904)
* fix(artifacts): preserve unknown relationship coverage

* fix(artifacts): retain producer relationship availability

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-10 22:52:59 -05:00

888 lines
30 KiB
TypeScript

import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js";
import { z } from "zod";
import {
artifactOccurrenceRelationshipCoverage,
type ArtifactInventoryResult,
type ArtifactNode,
type ArtifactOccurrence,
} from "./artifactGraph.js";
import { AnalysisInputError } from "./analysisErrorCore.js";
import {
ArtifactInventoryCarrierError,
hasCompleteInventoryCoverage,
parseArtifactInventoryEvidence,
type InventorySet,
} from "./artifactInventoryEvidence.js";
import {
formatMetadataDeclaration,
type FormatMetadataDeclaration,
} from "./artifactFormatMetadata.js";
import { prefixedDigestSchema } from "./../domain/digests.js";
import { canonicalJson } from "./comparisonSemantics.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const comparisonStatusSchema = z.enum([
"unchanged",
"added",
"removed",
"changed",
"truncated",
"unknown",
"contradiction",
]);
const changeKindSchema = z.enum([
"added",
"removed",
"changed",
"unknown",
"contradiction",
]);
const comparisonDimensionSchema = z.enum([
"content",
"kind",
"format",
"size",
"executable",
"relations",
"metadata",
"availability",
"integrity",
]);
/** One path-classified artifact change with citations to both observations. */
const artifactChangeSchema = z.object({
classification: changeKindSchema,
logical_path: z.string().min(1),
dimensions: z.array(comparisonDimensionSchema).min(1),
left_occurrence_id: z
.string()
.regex(/^occ_[a-f0-9]{64}$/u)
.nullable(),
right_occurrence_id: z
.string()
.regex(/^occ_[a-f0-9]{64}$/u)
.nullable(),
left_artifact_id: z
.string()
.regex(/^art_[a-f0-9]{64}$/u)
.nullable(),
right_artifact_id: z
.string()
.regex(/^art_[a-f0-9]{64}$/u)
.nullable(),
evidence_links: z.array(evidenceIdSchema).min(2),
});
/** Deterministic artifact comparison with every change returned inline. */
export const artifactComparisonResultSchema = z.object({
status: comparisonStatusSchema,
left_manifest_id: prefixedDigestSchema("agm"),
right_manifest_id: prefixedDigestSchema("agm"),
summary: z.object({
unchanged: z.int().min(0),
added: z.int().min(0),
removed: z.int().min(0),
changed: z.int().min(0),
unknown: z.int().min(0),
contradiction: z.int().min(0).default(0),
}),
changes: z.array(artifactChangeSchema),
// Absent on legacy records; absence does not identify a contradicting side.
integrity_mismatch_evidence_ids: z.array(evidenceIdSchema).optional(),
limitations: z.array(z.string()),
});
export type ArtifactComparisonResult = z.infer<
typeof artifactComparisonResultSchema
>;
type ArtifactChange = z.infer<typeof artifactChangeSchema>;
/** Comparison limitation when a path's format declarations cannot be compared. */
export const FORMAT_DECLARATION_UNKNOWN_LIMITATION =
"Format metadata declarations are missing on one side or differ in kind for matching paths; omitted metadata may be a legacy or non-container observation and leaves the complete declaration unavailable.";
const comparisonInventory = (input: unknown, side: "left" | "right") => {
try {
return parseArtifactInventoryEvidence(input);
} catch (cause: unknown) {
if (!(cause instanceof ArtifactInventoryCarrierError)) throw cause;
throw new AnalysisInputError(
"compare_artifacts",
{ cause, userMessage: cause.message },
[{ path: [side], reason: "invalid_value", message: cause.message }],
);
}
};
/** Compare complete inventory evidence without treating missing evidence as equality. */
export const compareArtifacts = (
leftEvidence: unknown,
rightEvidence: unknown,
): ArtifactComparisonResult => {
const left = comparisonInventory(leftEvidence, "left");
const right = comparisonInventory(rightEvidence, "right");
const leftComplete = left.inventory.complete;
const rightComplete = right.inventory.complete;
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 leftOccurrencesByPath = occurrencesByPath(left.inventory.occurrences);
const rightOccurrencesByPath = occurrencesByPath(right.inventory.occurrences);
const comparison = compareOccurrences(
left.inventory,
right.inventory,
leftOccurrencesByPath,
rightOccurrencesByPath,
[
...left.evidence.map(({ evidence_id: id }) => id),
...right.evidence.map(({ evidence_id: id }) => id),
],
{ root: rootCoverageComplete, entries: entryCoverageComplete },
);
const changes = comparison.changes;
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."]),
...(comparison.hasUnknownAsarPacking
? [
"ASAR packing metadata is recorded on only one side for matching paths; omitted metadata may be a legacy observation and does not establish packed or unpacked storage.",
]
: []),
...(comparison.hasUnknownFormatDeclaration
? [FORMAT_DECLARATION_UNKNOWN_LIMITATION]
: []),
...(comparison.hasAmbiguousDuplicatePaths
? [
"Duplicate logical paths cannot be paired across inventories; comparisons for those paths are unknown.",
]
: []),
];
applyRootHashChange(left.inventory, right.inventory, changes, [
...left.evidence.map(({ evidence_id: id }) => id),
...right.evidence.map(({ evidence_id: id }) => id),
]);
const summary = summarize(
left.inventory,
right.inventory,
changes,
fullyCovered,
);
return artifactComparisonResultSchema.parse({
status:
!leftComplete || !rightComplete
? "truncated"
: changes.some(
({ classification }) => classification === "contradiction",
)
? "contradiction"
: !fullyCovered
? "unknown"
: changes.some(({ classification }) => classification === "unknown")
? "unknown"
: changes.length === 0
? "unchanged"
: "changed",
left_manifest_id: left.inventory.manifest.manifest_id,
right_manifest_id: right.inventory.manifest.manifest_id,
summary,
changes,
integrity_mismatch_evidence_ids: [
...new Set(
[left, right].flatMap((source) =>
source.inventory.occurrences.some(
(occurrence) => occurrence.hash_status === "mismatched",
)
? source.evidence.map((record) => record.evidence_id)
: [],
),
),
].sort(compareUnicodeCodePoints),
limitations: [...new Set(limitations)].sort(compareUnicodeCodePoints),
});
};
const compareOccurrences = (
left: InventorySet,
right: InventorySet,
leftByPath: ReadonlyMap<string, ArtifactOccurrence[]>,
rightByPath: ReadonlyMap<string, ArtifactOccurrence[]>,
evidenceLinks: readonly string[],
coverage: { readonly root: boolean; readonly entries: boolean },
): {
readonly changes: ArtifactChange[];
readonly hasAmbiguousDuplicatePaths: boolean;
readonly hasUnknownAsarPacking: boolean;
readonly hasUnknownFormatDeclaration: boolean;
} => {
const leftNodes = new Map(left.nodes.map((item) => [item.artifact_id, item]));
const rightNodes = new Map(
right.nodes.map((item) => [item.artifact_id, item]),
);
const leftRelationsFor = relationsForOccurrence(left);
const rightRelationsFor = relationsForOccurrence(right);
const paths = [
...new Set([...leftByPath.keys(), ...rightByPath.keys()]),
].sort(compareUnicodeCodePoints);
const output: ArtifactChange[] = [];
let hasAmbiguousDuplicatePaths = false;
let hasUnknownAsarPacking = false;
let hasUnknownFormatDeclaration = false;
for (const path of paths) {
const leftMatches = leftByPath.get(path) ?? [];
const rightMatches = rightByPath.get(path) ?? [];
if (leftMatches.length > 1 || rightMatches.length > 1) {
hasAmbiguousDuplicatePaths = true;
const integrityMismatch = [...leftMatches, ...rightMatches].some(
({ hash_status: status }) => status === "mismatched",
);
output.push({
logical_path: path,
classification: integrityMismatch ? "contradiction" : "unknown",
dimensions: integrityMismatch
? ["availability", "integrity"]
: ["availability"],
left_occurrence_id: null,
right_occurrence_id: null,
left_artifact_id: null,
right_artifact_id: null,
evidence_links: [...evidenceLinks],
});
continue;
}
const leftOccurrence = leftMatches[0];
const rightOccurrence = rightMatches[0];
const metadata = occurrenceMetadata({ leftOccurrence, rightOccurrence });
if (leftOccurrence !== undefined && rightOccurrence !== undefined) {
if (metadata.asarPacking === "unknown") hasUnknownAsarPacking = true;
if (metadata.formatDeclaration === "unknown")
hasUnknownFormatDeclaration = true;
}
const change = classifyPath({
metadata,
path,
leftOccurrence,
rightOccurrence,
leftNode: nodeFor(leftOccurrence, leftNodes),
rightNode: nodeFor(rightOccurrence, rightNodes),
relations: compareRelations(
leftRelationsFor(leftOccurrence),
rightRelationsFor(rightOccurrence),
),
rootCovered: coverage.root,
entriesCovered: coverage.entries,
evidenceLinks,
});
if (change !== null) output.push(change);
}
return {
changes: output,
hasAmbiguousDuplicatePaths,
hasUnknownAsarPacking,
hasUnknownFormatDeclaration,
};
};
const occurrencesByPath = (
occurrences: readonly ArtifactOccurrence[],
): ReadonlyMap<string, ArtifactOccurrence[]> => {
const output = new Map<string, ArtifactOccurrence[]>();
for (const occurrence of occurrences) {
const matches = output.get(occurrence.logical_path);
if (matches === undefined)
output.set(occurrence.logical_path, [occurrence]);
else matches.push(occurrence);
}
return output;
};
interface ClassifyPathInput {
readonly metadata: OccurrenceMetadata;
readonly path: string;
readonly leftOccurrence: ArtifactOccurrence | undefined;
readonly rightOccurrence: ArtifactOccurrence | undefined;
readonly leftNode: ArtifactNode | undefined;
readonly rightNode: ArtifactNode | undefined;
readonly relations: FactComparison;
readonly rootCovered: boolean;
readonly entriesCovered: boolean;
readonly evidenceLinks: readonly string[];
}
const buildArtifactChangeBase = (input: ClassifyPathInput) => ({
logical_path: input.path,
left_occurrence_id: input.leftOccurrence?.occurrence_id ?? null,
right_occurrence_id: input.rightOccurrence?.occurrence_id ?? null,
left_artifact_id: input.leftOccurrence?.artifact_id ?? null,
right_artifact_id: input.rightOccurrence?.artifact_id ?? null,
evidence_links: [...input.evidenceLinks],
});
const classifyPresence = (
input: ClassifyPathInput,
base: ReturnType<typeof buildArtifactChangeBase>,
): ArtifactChange | null => {
const covered = input.path === "." ? input.rootCovered : input.entriesCovered;
if (input.leftOccurrence === undefined)
return {
...base,
classification: covered ? "added" : "unknown",
dimensions: [covered ? "content" : "availability"],
};
if (input.rightOccurrence === undefined)
return {
...base,
classification: covered ? "removed" : "unknown",
dimensions: [covered ? "content" : "availability"],
};
return null;
};
const classifyIntegrity = (
input: ClassifyPathInput,
base: ReturnType<typeof buildArtifactChangeBase>,
): ArtifactChange | null => {
if (
input.leftOccurrence?.hash_status === "mismatched" ||
input.rightOccurrence?.hash_status === "mismatched"
)
return {
...base,
classification: "contradiction",
dimensions: ["integrity"],
};
return null;
};
const classifyAvailability = (
input: ClassifyPathInput,
base: ReturnType<typeof buildArtifactChangeBase>,
): ArtifactChange | null => {
if (
input.leftOccurrence?.hash_status !== "verified" ||
input.rightOccurrence?.hash_status !== "verified" ||
input.leftNode === undefined ||
input.rightNode === undefined
)
return { ...base, classification: "unknown", dimensions: ["availability"] };
return null;
};
const classifyPath = (input: ClassifyPathInput): ArtifactChange | null => {
const base = buildArtifactChangeBase(input);
// A declared/observed mismatch is known even without an opposite occurrence.
const integrity = classifyIntegrity(input, base);
if (integrity !== null) return integrity;
const presence = classifyPresence(input, base);
if (presence !== null) return presence;
const declaration = classifyDeclarationRecord(input, base);
if (declaration.handled) return declaration.change;
const availability = classifyAvailability(input, base);
if (availability !== null) {
const known = occurrenceDifferences(input);
const dimensions: ArtifactChange["dimensions"] = [];
if (known.kind) dimensions.push("kind");
if (known.format) dimensions.push("format");
if (known.executable) dimensions.push("executable");
if (input.relations === "changed") dimensions.push("relations");
if (known.metadata || declaredDigestsDiffer(input))
dimensions.push("metadata");
dimensions.push("availability");
return {
...base,
classification: declarationChanged(input.metadata)
? "changed"
: "unknown",
dimensions,
};
}
const dimensions = changedDimensions(input);
const occurrence = occurrenceDifferences(input);
if (
declarationUnknown(input.metadata) ||
occurrence.kindUnknown ||
occurrence.formatUnknown ||
input.relations === "unknown" ||
differsWithUnknown(input.leftNode?.format, input.rightNode?.format) ||
differsWithMissing(
input.leftNode?.media_type,
input.rightNode?.media_type,
) ||
differsWithMissing(
input.leftNode?.architecture,
input.rightNode?.architecture,
)
) {
if (dimensions.length === 0)
return {
...base,
classification: "unknown",
dimensions: ["availability"],
};
dimensions.push("availability");
}
return dimensions.length === 0
? null
: { ...base, classification: "changed", dimensions };
};
type OccurrencePair = Pick<
ClassifyPathInput,
"leftOccurrence" | "rightOccurrence"
>;
type FactComparison = "unchanged" | "changed" | "unknown";
interface OccurrenceMetadata {
/** ASAR storage is an occurrence field, not format metadata. */
readonly asarPacking: FactComparison;
/** Every occurrence records `encrypted`, so a pair's comparison is known. */
readonly encryption: Exclude<FactComparison, "unknown">;
/** Absent when neither occurrence records a compared declaration. */
readonly formatDeclaration: FactComparison | "absent";
/** Unknown when either side could not read a compared field. */
readonly formatDeclarationAvailability: Exclude<FactComparison, "changed">;
}
/** A declaration that differs even when content is unavailable. */
const declarationChanged = (metadata: OccurrenceMetadata): boolean =>
metadata.asarPacking === "changed" ||
metadata.formatDeclaration === "changed";
/** A declared fact that differs: a declaration or the encryption. */
const declaredFactChanged = (metadata: OccurrenceMetadata): boolean =>
Object.values(metadata).includes("changed");
/** A declared fact that one side omits, records as another kind or cannot read. */
const declarationUnknown = (metadata: OccurrenceMetadata): boolean =>
Object.values(metadata).includes("unknown");
/** Unknown classifications and absent node metadata cannot establish a difference. */
const differsWithUnknown = (
left: string | null | undefined,
right: string | null | undefined,
): boolean =>
left !== right &&
(left == null || right == null || left === "unknown" || right === "unknown");
const differsWithMissing = (
left: string | null | undefined,
right: string | null | undefined,
): boolean => left !== right && (left == null || right == null);
/** Occurrence facts remain independently comparable when content is unavailable. */
const occurrenceDifferences = (input: {
readonly metadata: OccurrenceMetadata;
readonly leftOccurrence: ArtifactOccurrence | undefined;
readonly rightOccurrence: ArtifactOccurrence | undefined;
}) => ({
kind:
input.leftOccurrence?.artifact_kind !==
input.rightOccurrence?.artifact_kind &&
!differsWithUnknown(
input.leftOccurrence?.artifact_kind,
input.rightOccurrence?.artifact_kind,
),
kindUnknown: differsWithUnknown(
input.leftOccurrence?.artifact_kind,
input.rightOccurrence?.artifact_kind,
),
format:
input.leftOccurrence?.artifact_format !==
input.rightOccurrence?.artifact_format &&
!differsWithUnknown(
input.leftOccurrence?.artifact_format,
input.rightOccurrence?.artifact_format,
),
formatUnknown: differsWithUnknown(
input.leftOccurrence?.artifact_format,
input.rightOccurrence?.artifact_format,
),
executable:
input.leftOccurrence?.executable !== input.rightOccurrence?.executable,
metadata:
declaredFactChanged(input.metadata) ||
input.leftOccurrence?.entry_kind !== input.rightOccurrence?.entry_kind,
});
const compareAsarPacking = (
left: boolean | undefined,
right: boolean | undefined,
): FactComparison =>
left === right
? "unchanged"
: left === undefined || right === undefined
? "unknown"
: "changed";
/**
* Declarations compare within one kind. A declaration that only one side
* records, including one whose other side records another kind, is unknown.
* A field that either side could not read is unknown, and the other fields
* still compare.
*/
const compareFormatDeclarations = (
left: FormatMetadataDeclaration | undefined,
right: FormatMetadataDeclaration | undefined,
): Pick<
OccurrenceMetadata,
"formatDeclaration" | "formatDeclarationAvailability"
> => {
if (left === undefined && right === undefined)
return {
formatDeclaration: "absent",
formatDeclarationAvailability: "unchanged",
};
if (left === undefined || right === undefined || left.kind !== right.kind)
return {
formatDeclaration: "unknown",
formatDeclarationAvailability: "unchanged",
};
const unreadable = new Set([...left.unreadable, ...right.unreadable]);
const readable = (facts: FormatMetadataDeclaration["facts"]) =>
Object.fromEntries(
Object.entries(facts).filter(([field]) => !unreadable.has(field)),
);
return {
formatDeclaration:
canonicalJson(readable(left.facts)) ===
canonicalJson(readable(right.facts))
? "unchanged"
: "changed",
formatDeclarationAvailability: Object.keys(left.facts).some((field) =>
unreadable.has(field),
)
? "unknown"
: "unchanged",
};
};
/** Interpret each pair's declarations once, preserving historical omission. */
const occurrenceMetadata = (input: OccurrencePair): OccurrenceMetadata => {
const left = input.leftOccurrence;
const right = input.rightOccurrence;
return {
asarPacking: compareAsarPacking(left?.unpacked, right?.unpacked),
encryption: left?.encrypted === right?.encrypted ? "unchanged" : "changed",
...compareFormatDeclarations(
formatMetadataDeclaration(left?.format_metadata),
formatMetadataDeclaration(right?.format_metadata),
),
};
};
/**
* A node-less metadata record has no content node, so its declared facts are
* all that comparison checks; an unread payload is not compared. Neither are
* its limitations, which readers derive from those facts.
*/
const classifyDeclarationRecord = (
input: ClassifyPathInput,
base: ReturnType<typeof buildArtifactChangeBase>,
): { readonly handled: boolean; readonly change: ArtifactChange | null } => {
// Metadata is not inherently a declaration. Only apply this comparison
// when a producer explicitly recorded a compared one. A record that names
// content keeps the content comparison.
if (
input.leftOccurrence?.entry_kind !== "metadata" ||
input.rightOccurrence?.entry_kind !== "metadata" ||
input.leftOccurrence.artifact_id !== null ||
input.rightOccurrence.artifact_id !== null ||
input.metadata.formatDeclaration === "absent"
)
return { handled: false, change: null };
const dimensions: ArtifactChange["dimensions"] = [];
if (input.leftOccurrence.executable !== input.rightOccurrence.executable)
dimensions.push("executable");
if (declaredFactChanged(input.metadata)) dimensions.push("metadata");
const unknown = declarationUnknown(input.metadata);
if (dimensions.length === 0)
return {
handled: true,
change: unknown
? { ...base, classification: "unknown", dimensions: ["availability"] }
: null,
};
if (unknown) dimensions.push("availability");
return {
handled: true,
change: { ...base, classification: "changed", dimensions },
};
};
/**
* Without observed content on both sides, the containers' declared digests
* are the remaining content evidence. Claims of one algorithm that differ
* are a metadata difference, but they are unverified, so they do not make
* the path changed; equal claims add nothing. Observed bytes replace both.
*/
const declaredDigestsDiffer = (input: OccurrencePair): boolean => {
const left = input.leftOccurrence?.declared_integrity;
const right = input.rightOccurrence?.declared_integrity;
return (
left !== undefined &&
right !== undefined &&
left.algorithm === right.algorithm &&
left.digest !== right.digest
);
};
const changedDimensions = (input: {
readonly metadata: OccurrenceMetadata;
readonly leftNode: ArtifactNode | undefined;
readonly rightNode: ArtifactNode | undefined;
readonly leftOccurrence: ArtifactOccurrence | undefined;
readonly rightOccurrence: ArtifactOccurrence | undefined;
readonly relations: FactComparison;
}): z.infer<typeof comparisonDimensionSchema>[] => {
const left = input.leftNode;
const right = input.rightNode;
if (left === undefined || right === undefined) return ["availability"];
const occurrence = occurrenceDifferences(input);
const checks: readonly [
z.infer<typeof comparisonDimensionSchema>,
boolean,
][] = [
["content", left.sha256 !== right.sha256],
["kind", occurrence.kind],
[
"format",
(left.format !== right.format &&
!differsWithUnknown(left.format, right.format)) ||
occurrence.format,
],
["size", left.size !== right.size],
["executable", occurrence.executable],
["relations", input.relations === "changed"],
["metadata", occurrence.metadata || metadataChanged(left, right)],
];
return checks
.filter(([, changed]) => changed)
.map(([dimension]) => dimension);
};
const metadataChanged = (left: ArtifactNode, right: ArtifactNode): boolean =>
(left.media_type !== right.media_type &&
!differsWithMissing(left.media_type, right.media_type)) ||
(left.architecture !== right.architecture &&
!differsWithMissing(left.architecture, right.architecture)) ||
left.content_state !== right.content_state;
const nodeFor = (
occurrence: ArtifactOccurrence | undefined,
nodes: ReadonlyMap<string, ArtifactNode>,
): ArtifactNode | undefined =>
occurrence?.artifact_id === null || occurrence?.artifact_id === undefined
? undefined
: nodes.get(occurrence.artifact_id);
/** An empty projection means observed absence. Missing edges or parent facts leave it unavailable. */
const relationsForOccurrence = (
inventory: InventorySet,
): ((
occurrence: ArtifactOccurrence | undefined,
) => readonly RelationProjection[] | undefined) => {
if (inventory.edges.length !== inventory.manifest.edge_count)
return () => undefined;
const output = new Map<string, RelationProjection[]>();
const unresolvedPaths = new Set<string>();
const occurrences = new Map(
inventory.occurrences.map((item) => [item.occurrence_id, item]),
);
for (const edge of inventory.edges) {
const occurrence = occurrences.get(edge.occurrence_id);
// An incomplete inventory can omit the occurrence that names the child.
if (occurrence?.artifact_id === null || occurrence === undefined) continue;
const path = occurrence.logical_path;
const parent =
occurrence.parent_occurrence_id === null
? null
: occurrences.get(occurrence.parent_occurrence_id);
if (parent === undefined) {
unresolvedPaths.add(path);
continue;
}
const values = output.get(path);
const projection: RelationProjection = {
parent_logical_path: parent?.logical_path ?? null,
// Containment roots are path-normalized to avoid graph-wide deltas, but
// a source map's target is a semantic content identity, not its parent
// directory. Preserve that identity in the relation comparison.
mapped_artifact_id:
edge.relation === "maps-source" ? edge.parent_artifact_id : null,
child_artifact_id: occurrence.artifact_id,
relation: edge.relation,
logical_path: occurrence.logical_path,
producer: edge.producer,
};
if (values === undefined) output.set(path, [projection]);
else values.push(projection);
}
for (const values of output.values())
values.sort((left, right) =>
compareUnicodeCodePoints(canonicalJson(left), canonicalJson(right)),
);
return (occurrence) => {
if (occurrence?.artifact_id == null) return undefined;
if (artifactOccurrenceRelationshipCoverage(occurrence) === "unknown")
return undefined;
const path = occurrence.logical_path;
return unresolvedPaths.has(path) ? undefined : (output.get(path) ?? []);
};
};
const compareRelations = (
left: readonly RelationProjection[] | undefined,
right: readonly RelationProjection[] | undefined,
): FactComparison => {
if (left === undefined || right === undefined) return "unknown";
return JSON.stringify(left) === JSON.stringify(right)
? "unchanged"
: "changed";
};
interface RelationProjection {
/** Logical parent is stable across graph-root ID changes. */
readonly parent_logical_path: string | null;
/** Content identity is material for a maps-source edge. */
readonly mapped_artifact_id: string | null;
readonly child_artifact_id: string;
readonly relation: ArtifactInventoryResult["edges"][number]["relation"];
readonly logical_path: string | null;
/** Producer is semantic provenance; edge ordinal is presentation order. */
readonly producer: ArtifactInventoryResult["edges"][number]["producer"];
}
const summarize = (
left: InventorySet,
right: InventorySet,
changes: readonly ArtifactChange[],
covered: boolean,
) => {
const counts = {
added: 0,
removed: 0,
changed: 0,
unknown: 0,
contradiction: 0,
};
for (const change of changes) counts[change.classification] += 1;
// Change rows are keyed by logical path, and one row for a duplicate path
// stands for every left occurrence on it. Only occurrences on paths
// without a left-side change are affirmatively unchanged.
const changedPaths = new Set(
changes
.filter(({ classification }) => classification !== "added")
.map(({ logical_path: path }) => path),
);
return {
unchanged: covered
? left.occurrences.filter(
({ logical_path: path }) => !changedPaths.has(path),
).length
: 0,
...counts,
unknown: covered
? counts.unknown
: Math.max(
counts.unknown,
Math.max(
left.manifest.occurrence_count,
right.manifest.occurrence_count,
) -
counts.added -
counts.removed -
counts.changed -
counts.contradiction,
),
};
};
// Differing manifest root digests prove a root content change even when node
// details are unavailable. Fold that fact into an existing root row so one
// logical path keeps one classification.
const applyRootHashChange = (
left: InventorySet,
right: InventorySet,
changes: ArtifactChange[],
evidenceLinks: readonly string[],
): void => {
if (left.manifest.root_sha256 === right.manifest.root_sha256) return;
const index = changes.findIndex((change) => change.logical_path === ".");
const existing = changes[index];
if (existing !== undefined) {
if (
existing.dimensions.includes("content") ||
(existing.classification !== "unknown" &&
existing.classification !== "changed")
)
return;
changes[index] = {
...existing,
classification: "changed",
dimensions: comparisonDimensionSchema.options.filter(
(dimension) =>
dimension === "content" || existing.dimensions.includes(dimension),
),
evidence_links: [
...new Set([...existing.evidence_links, ...evidenceLinks]),
],
};
return;
}
const leftRoot = left.occurrences.find(
(occurrence) => occurrence.logical_path === ".",
);
const rightRoot = right.occurrences.find(
(occurrence) => occurrence.logical_path === ".",
);
changes.push({
classification: "changed",
logical_path: ".",
dimensions: ["content"],
left_occurrence_id: leftRoot?.occurrence_id ?? null,
right_occurrence_id: rightRoot?.occurrence_id ?? null,
left_artifact_id: leftRoot?.artifact_id ?? null,
right_artifact_id: rightRoot?.artifact_id ?? null,
evidence_links: [...evidenceLinks],
});
};