perf(javascript): validate the version change graph once (#1370)

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.
This commit is contained in:
keemsisi 2026-10-09 23:34:21 +01:00 committed by GitHub
parent 810107a2ad
commit 037709a52c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 160 additions and 6 deletions

View File

@ -1,6 +1,6 @@
import {
createJavaScriptApplicationEdge,
createJavaScriptApplicationGraph,
createImmutableJavaScriptApplicationGraphSteps,
createJavaScriptApplicationNode,
type JavaScriptApplicationGraph,
} from "./javascriptApplicationGraph.js";
@ -59,7 +59,7 @@ export const buildJavaScriptApplicationChangeGraph = (
const candidateEdges = uniqueEdges([...sourceEdges, ...comparisonEdges]);
const rootNodeIds = preferredRoots.filter((nodeId) => retained.has(nodeId));
const fallbackRoot = mergedNodes[0]?.node_id;
const graph = createJavaScriptApplicationGraph({
const steps = createImmutableJavaScriptApplicationGraphSteps({
schema: "JavaScriptApplicationGraph",
root_node_ids:
rootNodeIds.length > 0
@ -77,7 +77,9 @@ export const buildJavaScriptApplicationChangeGraph = (
"The change graph contains compared entities and their retained relationships; it is not an executable application.",
]),
});
return { graph };
let next = steps.next();
while (!next.done) next = steps.next();
return { graph: next.value };
};
const nodeCandidates = (

View File

@ -0,0 +1,140 @@
import { describe, expect, it } from "vitest";
import {
createJavaScriptApplicationEdge,
createJavaScriptApplicationGraph,
createJavaScriptApplicationNode,
isValidatedImmutableJavaScriptApplicationGraph,
parseJavaScriptApplicationGraph,
type JavaScriptApplicationGraph,
} from "./javascriptApplicationGraph.js";
import {
artifactEvidence,
completeCoverage,
} from "./javascriptApplicationGraph.fixture.js";
import { compareJavaScriptApplicationVersions } from "./javascriptApplicationVersionComparison.js";
import {
applicationVersionComparisonResultSchema,
ownedApplicationVersionComparisonResultSchema,
} from "./javascriptApplicationVersionComparisonSchemas.js";
const LEFT_SHA = "a".repeat(64);
const RIGHT_SHA = "b".repeat(64);
const buildGraph = (
artifactSha: string,
version: number,
): JavaScriptApplicationGraph => {
const nodes = ["a", "b", "c"].map((name) =>
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: name
.charCodeAt(0)
.toString(16)
.padStart(64, "0"),
structural_fingerprint_algorithm: "fixture",
source_sha256: (name === "b" ? version : 0)
.toString(16)
.padStart(64, "0"),
},
evidence: artifactEvidence(artifactSha, `${name}.js`),
},
],
}),
);
const [a, b, c] = nodes;
if (a === undefined || b === undefined || c === 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"),
}),
createJavaScriptApplicationEdge({
source_node_id: b.node_id,
target_node_id: c.node_id,
relation: "imports",
properties: {},
evidence: artifactEvidence(artifactSha, "b.js"),
}),
],
coverage: completeCoverage,
limitations: [],
});
};
const compare = () =>
compareJavaScriptApplicationVersions({
left: {
evidenceId: `ev_${"1".repeat(64)}`,
rootArtifactSha256: LEFT_SHA,
graph: buildGraph(LEFT_SHA, 1),
},
right: {
evidenceId: `ev_${"2".repeat(64)}`,
rootArtifactSha256: RIGHT_SHA,
graph: buildGraph(RIGHT_SHA, 2),
},
leftNativeEvidence: [],
rightNativeEvidence: [],
});
describe("application version comparison result", () => {
it("returns a sealed change graph that passes complete validation", () => {
const result = compare();
expect(
result.graph.edges.some(({ relation }) => relation === "changed_from"),
).toBe(true);
expect(isValidatedImmutableJavaScriptApplicationGraph(result.graph)).toBe(
true,
);
expect(Object.isFrozen(result.graph.edges)).toBe(true);
expect(parseJavaScriptApplicationGraph(result.graph)).toEqual(result.graph);
expect(applicationVersionComparisonResultSchema.parse(result)).toEqual(
result,
);
});
it("does not reuse a change graph without an owned proof", () => {
const result = compare();
const copy = structuredClone(result.graph);
expect(isValidatedImmutableJavaScriptApplicationGraph(copy)).toBe(false);
expect(() =>
ownedApplicationVersionComparisonResultSchema.parse({
...result,
graph: copy,
}),
).toThrow();
expect(
applicationVersionComparisonResultSchema.parse({ ...result, graph: copy })
.graph,
).toEqual(result.graph);
});
it("keeps coverage validation for owned results", () => {
const result = compare();
expect(() =>
ownedApplicationVersionComparisonResultSchema.parse({
...result,
coverage: { ...result.coverage, status: "partial" },
}),
).toThrow("Comparison coverage must match source graph completeness");
});
});

View File

@ -3,7 +3,7 @@ import { uniqueSorted } from "../canonicalOrdering.js";
import type { Evidence } from "../evidence.js";
import { buildJavaScriptApplicationChangeGraph } from "./javascriptApplicationChangeGraph.js";
import {
applicationVersionComparisonResultSchema,
ownedApplicationVersionComparisonResultSchema,
type ApplicationVersionComparisonResult,
} from "./javascriptApplicationVersionComparisonSchemas.js";
import type { JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js";
@ -76,7 +76,7 @@ export const compareJavaScriptApplicationVersions = (
evidence_links: evidenceLinks,
limitations: comparisonLimitations(input),
};
return applicationVersionComparisonResultSchema.parse({
return ownedApplicationVersionComparisonResultSchema.parse({
...semantic,
comparison_id: `javc_${digestCanonicalValue(semantic, "Application version comparison")}`,
});

View File

@ -2,7 +2,11 @@ import { z } from "zod";
import { emptyArraySchema } from "../emptyArraySchema.js";
import { evidenceSchema } from "../evidence.js";
import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js";
import {
isValidatedImmutableJavaScriptApplicationGraph,
javascriptApplicationGraphSchema,
type JavaScriptApplicationGraph,
} from "./javascriptApplicationGraph.js";
import { JAVASCRIPT_APPLICATION_NODE_KINDS } from "./javascriptApplicationGraphSchemas.js";
import { prefixedDigestSchema } from "../digests.js";
@ -234,6 +238,14 @@ export const applicationVersionComparisonResultSchema = z
});
});
/** Reuse the exact owned change graph while validating the rest of the result. */
export const ownedApplicationVersionComparisonResultSchema =
applicationVersionComparisonResultSchema.safeExtend({
graph: z.custom<JavaScriptApplicationGraph>(
isValidatedImmutableJavaScriptApplicationGraph,
),
});
export type ApplicationVersionComparisonItem = z.infer<
typeof comparisonItemSchema
>;