mirror of
https://github.com/morluto/rea
synced 2026-10-26 22:28:27 -04:00
b1e85ad711
relationshipSignature rebuilt a node map and scanned every graph edge for each side of each matched pair, so comparison cost grew as pairs x (nodes + edges). Build node and adjacency indexes once per graph in ItemContext; signatures, sorting and output are unchanged. At 4000 matched modules, end-to-end compare: 5.7s -> 2.9s (2 edges per node) and 10.4s -> 5.8s (6 edges per node); 8000: 3.3x and 2.5x. Output JSON is byte-identical on the base commit.
176 lines
5.2 KiB
TypeScript
176 lines
5.2 KiB
TypeScript
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 NAMES = ["a", "b", "c", "d"] as const;
|
|
type ModuleName = (typeof NAMES)[number];
|
|
type Relation = "imports" | "contains";
|
|
type EdgeSpec = readonly [ModuleName, ModuleName, Relation];
|
|
|
|
const sourceDigest = (name: string): string =>
|
|
name.charCodeAt(0).toString(16).padStart(64, "0");
|
|
|
|
const buildGraph = (
|
|
artifactSha: string,
|
|
edges: readonly EdgeSpec[],
|
|
order: "forward" | "reverse" = "forward",
|
|
): JavaScriptApplicationGraph => {
|
|
const names = order === "forward" ? [...NAMES] : [...NAMES].reverse();
|
|
const nodes = names.map((name) =>
|
|
createJavaScriptApplicationNode({
|
|
kind: "javascript-module",
|
|
identity: {
|
|
strategy: "canonical-path",
|
|
stability: "artifact-version",
|
|
artifact_sha256: artifactSha,
|
|
path: `${name}.js`,
|
|
},
|
|
observations: [
|
|
{
|
|
label: `${name}.js`,
|
|
properties: { source_sha256: sourceDigest(name) },
|
|
evidence: artifactEvidence(artifactSha, `${name}.js`),
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
const idOf = (name: ModuleName): string => {
|
|
const node = nodes.find(({ identity }) =>
|
|
identity.strategy === "canonical-path"
|
|
? identity.path === `${name}.js`
|
|
: false,
|
|
);
|
|
if (node === undefined) throw new Error(`Missing fixture node ${name}`);
|
|
return node.node_id;
|
|
};
|
|
const ordered = order === "forward" ? [...edges] : [...edges].reverse();
|
|
return createJavaScriptApplicationGraph({
|
|
schema: "JavaScriptApplicationGraph",
|
|
root_node_ids: [idOf("a")],
|
|
nodes,
|
|
edges: ordered.map(([source, target, relation]) =>
|
|
createJavaScriptApplicationEdge({
|
|
source_node_id: idOf(source),
|
|
target_node_id: idOf(target),
|
|
relation,
|
|
properties: {},
|
|
evidence: artifactEvidence(artifactSha, `${source}.js`),
|
|
}),
|
|
),
|
|
coverage: completeCoverage,
|
|
limitations: [],
|
|
});
|
|
};
|
|
|
|
const relationshipChanges = (
|
|
left: JavaScriptApplicationGraph,
|
|
right: JavaScriptApplicationGraph,
|
|
): string[] => {
|
|
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: [],
|
|
});
|
|
expect(result.summary.added + result.summary.removed).toBe(0);
|
|
return result.items
|
|
.filter(({ dimensions }) => dimensions.includes("relationships"))
|
|
.map(({ left_node_id: id }) => {
|
|
const node = left.nodes.find(({ node_id: nodeId }) => nodeId === id);
|
|
const label = node?.observations[0]?.label;
|
|
if (label === undefined || label === null)
|
|
throw new Error("Changed item has no node");
|
|
return label;
|
|
})
|
|
.sort();
|
|
};
|
|
|
|
const BASE_EDGES: readonly EdgeSpec[] = [
|
|
["a", "b", "imports"],
|
|
["b", "c", "imports"],
|
|
["a", "c", "contains"],
|
|
["c", "b", "imports"],
|
|
];
|
|
|
|
describe("application version relationship comparison", () => {
|
|
it("reports no relationship change for identical topology in any node or edge order", () => {
|
|
expect(
|
|
relationshipChanges(
|
|
buildGraph(LEFT_SHA, BASE_EDGES),
|
|
buildGraph(RIGHT_SHA, BASE_EDGES, "reverse"),
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("flags both endpoints when an edge relation changes", () => {
|
|
const changed: readonly EdgeSpec[] = [
|
|
["a", "b", "imports"],
|
|
["b", "c", "contains"],
|
|
["a", "c", "contains"],
|
|
["c", "b", "imports"],
|
|
];
|
|
expect(
|
|
relationshipChanges(
|
|
buildGraph(LEFT_SHA, BASE_EDGES),
|
|
buildGraph(RIGHT_SHA, changed),
|
|
),
|
|
).toEqual(["b.js", "c.js"]);
|
|
});
|
|
|
|
it("flags a node whose only change is an incoming edge", () => {
|
|
const left: readonly EdgeSpec[] = [["a", "b", "imports"]];
|
|
const right: readonly EdgeSpec[] = [
|
|
["a", "b", "imports"],
|
|
["c", "b", "imports"],
|
|
];
|
|
expect(
|
|
relationshipChanges(
|
|
buildGraph(LEFT_SHA, left),
|
|
buildGraph(RIGHT_SHA, right),
|
|
),
|
|
).toEqual(["b.js", "c.js"]);
|
|
});
|
|
|
|
it("flags a node when an edge is re-pointed at a different matched neighbour", () => {
|
|
const left: readonly EdgeSpec[] = [["a", "b", "imports"]];
|
|
const right: readonly EdgeSpec[] = [["a", "c", "imports"]];
|
|
expect(
|
|
relationshipChanges(
|
|
buildGraph(LEFT_SHA, left),
|
|
buildGraph(RIGHT_SHA, right),
|
|
),
|
|
).toEqual(["a.js", "b.js", "c.js"]);
|
|
});
|
|
|
|
it("distinguishes edge direction between the same pair", () => {
|
|
const left: readonly EdgeSpec[] = [["a", "b", "imports"]];
|
|
const right: readonly EdgeSpec[] = [["b", "a", "imports"]];
|
|
expect(
|
|
relationshipChanges(
|
|
buildGraph(LEFT_SHA, left),
|
|
buildGraph(RIGHT_SHA, right),
|
|
),
|
|
).toEqual(["a.js", "b.js"]);
|
|
});
|
|
});
|