perf(javascript): index change-graph nodes once per comparison

Resolve each compared graph node through a per-side index while preserving right-hand evidence selection and complete comparison results.

Refs #1065.

Co-authored-by: keemsisi <7439375+keemsisi@users.noreply.github.com>
This commit is contained in:
keemsisi 2026-10-10 06:21:13 +01:00 committed by GitHub
parent eeb83f6581
commit 33b9b31a6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 124 additions and 8 deletions

View File

@ -0,0 +1,107 @@
import { describe, expect, it } from "vitest";
import {
createJavaScriptApplicationEdge,
createJavaScriptApplicationGraph,
createJavaScriptApplicationNode,
type JavaScriptApplicationGraph,
} from "./javascriptApplicationGraph.js";
import {
artifactEvidence,
completeCoverage,
} from "./javascriptApplicationGraph.fixture.js";
import { compareJavaScriptApplicationVersions } from "./javascriptApplicationVersionComparison.js";
const LEFT_SHA = "a".repeat(64);
const RIGHT_SHA = "b".repeat(64);
const digest = (value: number): string => value.toString(16).padStart(64, "0");
const buildGraph = (
artifactSha: string,
changed: number,
): JavaScriptApplicationGraph => {
const nodes = ["a", "b", "c", "d"].map((name, index) =>
createJavaScriptApplicationNode({
kind: "javascript-module",
identity: {
strategy: "canonical-path",
stability: "artifact-version",
artifact_sha256: artifactSha,
path: `${name}.js`,
},
observations: [
{
label: `${name}.js`,
properties: {
structural_fingerprint_sha256: digest(index + 1),
structural_fingerprint_algorithm: "fixture",
source_sha256: digest(index === 1 || index === 3 ? changed : 0),
},
evidence: artifactEvidence(artifactSha, `${name}.js`),
},
],
}),
);
const [a, b] = nodes;
if (a === undefined || b === undefined)
throw new Error("Missing fixture node");
return createJavaScriptApplicationGraph({
schema: "JavaScriptApplicationGraph",
root_node_ids: [a.node_id],
nodes,
edges: [
createJavaScriptApplicationEdge({
source_node_id: a.node_id,
target_node_id: b.node_id,
relation: "imports",
properties: {},
evidence: artifactEvidence(artifactSha, "a.js"),
}),
],
coverage: completeCoverage,
limitations: [],
});
};
describe("application change graph", () => {
it("anchors each changed_from edge to its own right-hand observation", () => {
const left = buildGraph(LEFT_SHA, 1);
const right = buildGraph(RIGHT_SHA, 2);
const result = compareJavaScriptApplicationVersions({
left: {
evidenceId: `ev_${"1".repeat(64)}`,
rootArtifactSha256: LEFT_SHA,
graph: left,
},
right: {
evidenceId: `ev_${"2".repeat(64)}`,
rootArtifactSha256: RIGHT_SHA,
graph: right,
},
leftNativeEvidence: [],
rightNativeEvidence: [],
});
const changedFrom = result.graph.edges.filter(
({ relation }) => relation === "changed_from",
);
expect(changedFrom).toHaveLength(2);
for (const edge of changedFrom) {
const source = right.nodes.find(
({ node_id: id }) => id === edge.source_node_id,
);
const target = left.nodes.find(
({ node_id: id }) => id === edge.target_node_id,
);
expect(source?.observations[0]?.label).toBe(
target?.observations[0]?.label,
);
expect(edge.evidence.artifact).toEqual(
source?.observations[0]?.evidence.artifact,
);
expect(edge.evidence.location).toEqual(
source?.observations[0]?.evidence.location,
);
}
});
});

View File

@ -53,8 +53,14 @@ export const buildJavaScriptApplicationChangeGraph = (
({ source_node_id: source, target_node_id: target }) =>
retained.has(source) && retained.has(target),
);
const leftById = new Map(
input.left.nodes.map((node) => [node.node_id, node]),
);
const rightById = new Map(
input.right.nodes.map((node) => [node.node_id, node]),
);
const comparisonEdges = input.items.flatMap((item) =>
changedFromEdge(item, input, retained),
changedFromEdge(item, input, retained, leftById, rightById),
);
const candidateEdges = uniqueEdges([...sourceEdges, ...comparisonEdges]);
const rootNodeIds = preferredRoots.filter((nodeId) => retained.has(nodeId));
@ -104,6 +110,8 @@ const changedFromEdge = (
item: ApplicationVersionComparisonItem,
input: ChangeGraphInput,
retained: ReadonlySet<string>,
leftById: ReadonlyMap<string, ApplicationNode>,
rightById: ReadonlyMap<string, ApplicationNode>,
): ApplicationEdge[] => {
const left = item.left_node_id;
const right = item.right_node_id;
@ -117,7 +125,12 @@ const changedFromEdge = (
!retained.has(right)
)
return [];
const evidence = comparisonEvidence(item, input);
const evidence = comparisonEvidence(
item,
input,
leftById.get(left),
rightById.get(right),
);
return [
createJavaScriptApplicationEdge({
source_node_id: right,
@ -138,13 +151,9 @@ const changedFromEdge = (
const comparisonEvidence = (
item: ApplicationVersionComparisonItem,
input: ChangeGraphInput,
left: ApplicationNode | undefined,
right: ApplicationNode | undefined,
): ApplicationGraphEvidence => {
const right = input.right.nodes.find(
({ node_id: id }) => id === item.right_node_id,
);
const left = input.left.nodes.find(
({ node_id: id }) => id === item.left_node_id,
);
const source =
right?.observations[0]?.evidence ?? left?.observations[0]?.evidence;
const complete =