From 87d5ec93d5d5d07d1029f15f755363ce5faf3b2f Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 11 Oct 2026 04:37:42 +0800 Subject: [PATCH] perf(javascript): index native Evidence links by observed digest Build each side's digest-to-Evidence index once instead of scanning native observations for every comparison item. Retain every exact match and avoid linking subjectless Evidence to nodes lacking content identity. Comparison and relationship regressions pass. Isolated alternating unprofiled runs at 2,000 nodes and observations per side have conventional medians 379.924 ms before and 246.466 ms after; the complete 4,000-item outputs match byte-for-byte. Timings are bounded workload evidence, not whole-suite performance. Flameox profile: flameox://evidence/7dca714833dc3d254a75704670a81de656b4b31d4b77a72236eecb82459b3d86 --- ...scriptApplicationVersionComparison.test.ts | 131 ++++++++++++++++++ .../javascriptApplicationVersionItems.ts | 64 ++++++--- 2 files changed, 176 insertions(+), 19 deletions(-) diff --git a/src/domain/javascript/javascriptApplicationVersionComparison.test.ts b/src/domain/javascript/javascriptApplicationVersionComparison.test.ts index c0d92cc382..d73dceaf6b 100644 --- a/src/domain/javascript/javascriptApplicationVersionComparison.test.ts +++ b/src/domain/javascript/javascriptApplicationVersionComparison.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; +import { createEvidence } from "../evidence.js"; import { createJavaScriptApplicationEdge, createJavaScriptApplicationGraph, @@ -96,7 +97,137 @@ const compare = () => rightNativeEvidence: [], }); +const unrelatedGraph = (artifactSha: string): JavaScriptApplicationGraph => { + const node = createJavaScriptApplicationNode({ + kind: "javascript-chunk", + identity: { + strategy: "canonical-path", + stability: "artifact-version", + artifact_sha256: artifactSha, + path: "unrelated.js", + }, + observations: [ + { + label: "unrelated.js", + properties: {}, + evidence: artifactEvidence(artifactSha, "unrelated.js"), + }, + ], + }); + return createJavaScriptApplicationGraph({ + schema: "JavaScriptApplicationGraph", + root_node_ids: [node.node_id], + nodes: [node], + edges: [], + coverage: completeCoverage, + limitations: [], + }); +}; + +const nativeEvidence = (sha256: string | undefined, ordinal: number) => + createEvidence( + sha256 === undefined + ? undefined + : { + path: `/fixture/native-${ordinal}.js`, + sha256, + format: "javascript", + }, + { id: "fixture", name: "Fixture", version: "1" }, + { + operation: "analyze_javascript_module", + parameters: { ordinal }, + confidence: "observed", + authority: "shipped-artifact", + result: { ordinal }, + }, + ); + describe("application version comparison result", () => { + it("links every exact digest observation only to its paired or unmatched item", () => { + const left = buildGraph(LEFT_SHA, 1); + const right = buildGraph(RIGHT_SHA, 2); + const leftEvidenceId = `ev_${"1".repeat(64)}`; + const rightEvidenceId = `ev_${"2".repeat(64)}`; + const leftBDigest = `${"0".repeat(63)}1`; + const leftNative = [ + nativeEvidence(leftBDigest, 1), + nativeEvidence(leftBDigest, 2), + nativeEvidence("9".repeat(64), 3), + ]; + const rightNative = [ + nativeEvidence("0".repeat(64), 4), + nativeEvidence(undefined, 6), + ]; + const unrelatedRight = unrelatedGraph(RIGHT_SHA); + const unmatched = compareJavaScriptApplicationVersions({ + left: { + evidenceId: leftEvidenceId, + rootArtifactSha256: LEFT_SHA, + graph: left, + }, + right: { + evidenceId: rightEvidenceId, + rootArtifactSha256: RIGHT_SHA, + graph: unrelatedRight, + }, + leftNativeEvidence: leftNative, + rightNativeEvidence: rightNative, + }); + const leftB = left.nodes.find(({ observations }) => + observations.some(({ label }) => label === "b.js"), + ); + if (leftB === undefined) throw new Error("Missing left b.js node"); + const unmatchedB = unmatched.items.find( + ({ left_node_id: id }) => id === leftB.node_id, + ); + expect(unmatchedB?.evidence_links).toEqual( + [ + leftEvidenceId, + rightEvidenceId, + leftNative[0]!.evidence_id, + leftNative[1]!.evidence_id, + ].sort(), + ); + const unrelatedItem = unmatched.items.find( + ({ right_node_id: id }) => id === unrelatedRight.nodes[0]?.node_id, + ); + expect(unrelatedItem?.evidence_links).toEqual( + [leftEvidenceId, rightEvidenceId].sort(), + ); + + const paired = compareJavaScriptApplicationVersions({ + left: { + evidenceId: leftEvidenceId, + rootArtifactSha256: LEFT_SHA, + graph: left, + }, + right: { + evidenceId: rightEvidenceId, + rootArtifactSha256: RIGHT_SHA, + graph: right, + }, + leftNativeEvidence: [nativeEvidence("0".repeat(64), 5)], + rightNativeEvidence: rightNative, + }); + const leftA = left.nodes.find(({ observations }) => + observations.some(({ label }) => label === "a.js"), + ); + if (leftA === undefined) throw new Error("Missing left a.js node"); + const pairedA = paired.items.find( + ({ left_node_id: id }) => id === leftA.node_id, + ); + expect(pairedA?.match.status).toBe("matched"); + expect(pairedA?.evidence_links).toEqual( + [ + leftEvidenceId, + rightEvidenceId, + nativeEvidence("0".repeat(64), 5).evidence_id, + rightNative[0]!.evidence_id, + ].sort(), + ); + }); + it("returns a sealed change graph that passes complete validation", () => { const result = compare(); expect( diff --git a/src/domain/javascript/javascriptApplicationVersionItems.ts b/src/domain/javascript/javascriptApplicationVersionItems.ts index 731d29b82c..e4fff66034 100644 --- a/src/domain/javascript/javascriptApplicationVersionItems.ts +++ b/src/domain/javascript/javascriptApplicationVersionItems.ts @@ -23,14 +23,27 @@ interface ItemContext { readonly rightGraph: JavaScriptApplicationGraph; readonly leftEvidenceId: string; readonly rightEvidenceId: string; - readonly leftNativeEvidence: readonly Evidence[]; - readonly rightNativeEvidence: readonly Evidence[]; + readonly leftNativeEvidenceByDigest: ReadonlyMap; + readonly rightNativeEvidenceByDigest: ReadonlyMap; readonly pairByLeft: ReadonlyMap; readonly pairByRight: ReadonlyMap; readonly leftRelationships: RelationshipIndex; readonly rightRelationships: RelationshipIndex; } +type ItemContextInput = Omit< + ItemContext, + | "leftNativeEvidenceByDigest" + | "rightNativeEvidenceByDigest" + | "pairByLeft" + | "pairByRight" + | "leftRelationships" + | "rightRelationships" +> & { + readonly leftNativeEvidence: readonly Evidence[]; + readonly rightNativeEvidence: readonly Evidence[]; +}; + interface IndexedRelationship { readonly direction: "in" | "out"; readonly relation: ApplicationEdge["relation"]; @@ -66,10 +79,7 @@ type ComparisonItemSemantic< /** Classify matched, unmatched, and ambiguous nodes without inventing absence. */ export const classifyJavaScriptApplicationVersions = ( matching: ApplicationVersionMatchingProjection, - context: Omit< - ItemContext, - "pairByLeft" | "pairByRight" | "leftRelationships" | "rightRelationships" - >, + context: ItemContextInput, ): ApplicationVersionItemProjection => { const pairByLeft = new Map( matching.pairs.map(({ left, right }) => [ @@ -84,7 +94,14 @@ export const classifyJavaScriptApplicationVersions = ( ]), ); const fullContext: ItemContext = { - ...context, + leftGraph: context.leftGraph, + rightGraph: context.rightGraph, + leftEvidenceId: context.leftEvidenceId, + rightEvidenceId: context.rightEvidenceId, + leftNativeEvidenceByDigest: indexNativeEvidence(context.leftNativeEvidence), + rightNativeEvidenceByDigest: indexNativeEvidence( + context.rightNativeEvidence, + ), pairByLeft, pairByRight, leftRelationships: indexRelationships(context.leftGraph), @@ -426,24 +443,33 @@ const itemEvidenceLinks = ( ...new Set([ context.leftEvidenceId, context.rightEvidenceId, - ...nativeLinks(left, context.leftNativeEvidence), - ...nativeLinks(right, context.rightNativeEvidence), + ...nativeLinks(left, context.leftNativeEvidenceByDigest), + ...nativeLinks(right, context.rightNativeEvidenceByDigest), ]), ].sort(compareUnicodeCodePoints); const nativeLinks = ( node: ApplicationNode | undefined, + evidenceByDigest: ReadonlyMap, +): readonly string[] => { + const identity = node === undefined ? null : contentIdentity(node); + if (identity === null) return []; + const digest = identity.replace(/^content:/u, ""); + return evidenceByDigest.get(digest) ?? []; +}; + +const indexNativeEvidence = ( evidence: readonly Evidence[], -): string[] => { - const digest = - node === undefined - ? null - : contentIdentity(node)?.replace(/^content:/u, ""); - return digest === null - ? [] - : evidence - .filter(({ subject }) => subject?.digest.sha256 === digest) - .map(({ evidence_id: id }) => id); +): ReadonlyMap => { + const evidenceByDigest = new Map(); + for (const { evidence_id: id, subject } of evidence) { + const digest = subject?.digest.sha256; + if (digest === undefined) continue; + const ids = evidenceByDigest.get(digest); + if (ids === undefined) evidenceByDigest.set(digest, [id]); + else ids.push(id); + } + return evidenceByDigest; }; const pairLimitations = (