mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
037709a52c
compareJavaScriptApplicationVersions built the change graph with createJavaScriptApplicationGraph, which validates it and re-derives every node, observation, edge and graph ID, then parsed the result with a schema that embeds the full graph schema, repeating all of that work. Seal the change graph with the owned immutable factory and accept that exact sealed graph in the result parse, as owned analysis results already do. Items, coverage and the public result schema are unchanged; output is byte-identical.
164 lines
6.6 KiB
TypeScript
164 lines
6.6 KiB
TypeScript
import { digestCanonicalValue } from "../canonicalDigest.js";
|
|
import { uniqueSorted } from "../canonicalOrdering.js";
|
|
import type { Evidence } from "../evidence.js";
|
|
import { buildJavaScriptApplicationChangeGraph } from "./javascriptApplicationChangeGraph.js";
|
|
import {
|
|
ownedApplicationVersionComparisonResultSchema,
|
|
type ApplicationVersionComparisonResult,
|
|
} from "./javascriptApplicationVersionComparisonSchemas.js";
|
|
import type { JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js";
|
|
import { classifyJavaScriptApplicationVersions } from "./javascriptApplicationVersionItems.js";
|
|
import { matchJavaScriptApplicationVersions } from "./javascriptApplicationVersionKeys.js";
|
|
|
|
/** Pure inputs after application-layer Evidence authentication. */
|
|
export interface ApplicationVersionComparisonProjectionInput {
|
|
readonly left: {
|
|
readonly evidenceId: string;
|
|
readonly rootArtifactSha256: string;
|
|
readonly graph: JavaScriptApplicationGraph;
|
|
};
|
|
readonly right: {
|
|
readonly evidenceId: string;
|
|
readonly rootArtifactSha256: string;
|
|
readonly graph: JavaScriptApplicationGraph;
|
|
};
|
|
readonly leftNativeEvidence: readonly Evidence[];
|
|
readonly rightNativeEvidence: readonly Evidence[];
|
|
}
|
|
|
|
/** Compare application entities with ordered unique-only identity tiers. */
|
|
export const compareJavaScriptApplicationVersions = (
|
|
input: ApplicationVersionComparisonProjectionInput,
|
|
): ApplicationVersionComparisonResult => {
|
|
const matching = matchJavaScriptApplicationVersions(
|
|
input.left.graph.nodes,
|
|
input.right.graph.nodes,
|
|
);
|
|
const projection = classifyJavaScriptApplicationVersions(matching, {
|
|
leftGraph: input.left.graph,
|
|
rightGraph: input.right.graph,
|
|
leftEvidenceId: input.left.evidenceId,
|
|
rightEvidenceId: input.right.evidenceId,
|
|
leftNativeEvidence: input.leftNativeEvidence,
|
|
rightNativeEvidence: input.rightNativeEvidence,
|
|
});
|
|
const changeGraph = buildJavaScriptApplicationChangeGraph({
|
|
left: input.left.graph,
|
|
right: input.right.graph,
|
|
leftEvidenceId: input.left.evidenceId,
|
|
rightEvidenceId: input.right.evidenceId,
|
|
items: projection.items,
|
|
});
|
|
const evidenceLinks = uniqueSorted(
|
|
projection.items.flatMap(({ evidence_links: links }) => links),
|
|
);
|
|
const omissions = {
|
|
left_graph_omitted_count: input.left.graph.coverage.omitted_count,
|
|
right_graph_omitted_count: input.right.graph.coverage.omitted_count,
|
|
};
|
|
const coverage = comparisonCoverage(input, omissions);
|
|
const semantic = {
|
|
left: {
|
|
evidence_id: input.left.evidenceId,
|
|
graph_id: input.left.graph.graph_id,
|
|
root_artifact_sha256: input.left.rootArtifactSha256,
|
|
},
|
|
right: {
|
|
evidence_id: input.right.evidenceId,
|
|
graph_id: input.right.graph.graph_id,
|
|
root_artifact_sha256: input.right.rootArtifactSha256,
|
|
},
|
|
summary: summary(projection.items),
|
|
matching: matchingSummary(projection.items),
|
|
items: projection.items,
|
|
graph: changeGraph.graph,
|
|
coverage,
|
|
evidence_links: evidenceLinks,
|
|
limitations: comparisonLimitations(input),
|
|
};
|
|
return ownedApplicationVersionComparisonResultSchema.parse({
|
|
...semantic,
|
|
comparison_id: `javc_${digestCanonicalValue(semantic, "Application version comparison")}`,
|
|
});
|
|
};
|
|
|
|
const summary = (
|
|
items: readonly ApplicationVersionComparisonResult["items"][number][],
|
|
): ApplicationVersionComparisonResult["summary"] => ({
|
|
unchanged: countStatus(items, "unchanged"),
|
|
added: countStatus(items, "added"),
|
|
removed: countStatus(items, "removed"),
|
|
changed: countStatus(items, "changed"),
|
|
unknown: countStatus(items, "unknown"),
|
|
});
|
|
|
|
const countStatus = (
|
|
items: readonly ApplicationVersionComparisonResult["items"][number][],
|
|
status: ApplicationVersionComparisonResult["items"][number]["status"],
|
|
): number => items.filter((item) => item.status === status).length;
|
|
|
|
const matchingSummary = (
|
|
items: readonly ApplicationVersionComparisonResult["items"][number][],
|
|
): ApplicationVersionComparisonResult["matching"] => ({
|
|
exact_node_identity: countBasis(items, "exact-node-identity"),
|
|
exact_content_digest: countBasis(items, "exact-content-digest"),
|
|
exact_module_source_digest: countBasis(items, "exact-module-source-digest"),
|
|
source_map_identity: countBasis(items, "source-map-identity"),
|
|
structural_fingerprint: countBasis(items, "structural-fingerprint"),
|
|
semantic_key: countBasis(items, "semantic-key"),
|
|
ambiguous: items.filter(({ match }) => match.status === "ambiguous").length,
|
|
unmatched: items.filter(({ match }) => match.status === "unmatched").length,
|
|
});
|
|
|
|
const countBasis = (
|
|
items: readonly ApplicationVersionComparisonResult["items"][number][],
|
|
basis: ApplicationVersionComparisonResult["items"][number]["match"]["basis"],
|
|
): number => items.filter(({ match }) => match.basis === basis).length;
|
|
|
|
type ComparisonOmissions = ApplicationVersionComparisonResult["coverage"];
|
|
type ComparisonOmissionCounts = Pick<
|
|
ComparisonOmissions,
|
|
"left_graph_omitted_count" | "right_graph_omitted_count"
|
|
>;
|
|
|
|
const comparisonCoverage = (
|
|
input: ApplicationVersionComparisonProjectionInput,
|
|
omissions: ComparisonOmissionCounts,
|
|
): ApplicationVersionComparisonResult["coverage"] => {
|
|
const context = {
|
|
left_graph_status: input.left.graph.coverage.status,
|
|
right_graph_status: input.right.graph.coverage.status,
|
|
};
|
|
const sourceGraphsComplete =
|
|
input.left.graph.coverage.status === "complete" &&
|
|
input.right.graph.coverage.status === "complete";
|
|
const sourceGraphsTruncated =
|
|
input.left.graph.coverage.truncated || input.right.graph.coverage.truncated;
|
|
return {
|
|
...context,
|
|
...omissions,
|
|
status: sourceGraphsComplete
|
|
? "complete-within-inputs"
|
|
: sourceGraphsTruncated
|
|
? "truncated"
|
|
: "partial",
|
|
};
|
|
};
|
|
|
|
const comparisonLimitations = (
|
|
input: ApplicationVersionComparisonProjectionInput,
|
|
): string[] =>
|
|
uniqueSorted([
|
|
"Application comparison never uses bundler module ordinals as persistent identity and performs no fuzzy pairing.",
|
|
"Exact digest equality proves byte identity only; source-map, structural-fingerprint, and semantic-key matches retain inferred confidence.",
|
|
"Minified names and chunk locations are not assumed stable across versions.",
|
|
"The comparison reads retained graphs and Evidence only; it does not execute either application.",
|
|
"Native Evidence is linked only by exact subject digest; provider snapshot reuse remains target/provider/profile exact.",
|
|
...(input.left.graph.coverage.status === "complete" &&
|
|
input.right.graph.coverage.status === "complete"
|
|
? []
|
|
: [
|
|
"At least one source graph is incomplete; unmatched entities on the opposite side remain unknown rather than added or removed.",
|
|
]),
|
|
]);
|