diff --git a/src/domain/javascript/javascriptRuntimeReconciliationGraph.ts b/src/domain/javascript/javascriptRuntimeReconciliationGraph.ts index d3c8f371fd..864b9d6f79 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationGraph.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationGraph.ts @@ -212,8 +212,11 @@ const graphCoverage = ( const mergeNodes = (nodes: readonly ApplicationNode[]): ApplicationNode[] => { const grouped = new Map(); - for (const node of nodes) - grouped.set(node.node_id, [...(grouped.get(node.node_id) ?? []), node]); + for (const node of nodes) { + const group = grouped.get(node.node_id); + if (group === undefined) grouped.set(node.node_id, [node]); + else group.push(node); + } const merged = [...grouped.values()].map((group) => { const first = group[0]; if (first === undefined) throw new TypeError("Empty JAG node group"); diff --git a/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts b/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts index e42af295f1..b73a83ee39 100644 --- a/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts +++ b/src/domain/javascript/javascriptSemanticGraphTraversal.test.ts @@ -316,6 +316,20 @@ it("returns every relevant unknown inline", () => { expect(result.unknowns).toHaveLength(1); }); +it("does not admit a semantic-node seed absent from the graph", () => { + const result = queryJavaScriptSemanticGraph(fixtureGraph(), { + seed: { kind: "semantic-node", node_id: `jsrg_node_${"f".repeat(64)}` }, + direction: "forward-influence", + }); + + expect(result).toMatchObject({ + status: "no-match", + seed_node_ids: [], + nodes: [], + summary: { total_seed_matches: 0, traversed_nodes: 0 }, + }); +}); + it("returns the complete deterministic forward influence result inline", () => { const graph = fixtureGraph(); const input = { diff --git a/src/domain/javascript/javascriptSemanticQuery.ts b/src/domain/javascript/javascriptSemanticQuery.ts index 6439d8954f..1870162570 100644 --- a/src/domain/javascript/javascriptSemanticQuery.ts +++ b/src/domain/javascript/javascriptSemanticQuery.ts @@ -30,10 +30,6 @@ interface Traversal { readonly modules: Set; } -interface SeedAdmission { - readonly nodeIds: string[]; -} - interface QueryResult { readonly retainedNodes: JavaScriptSemanticGraphNode[]; readonly retainedRelations: JavaScriptSemanticGraphRelation[]; @@ -47,10 +43,10 @@ export const queryJavaScriptSemanticGraph = ( const input = javaScriptSemanticQueryInputSchema.parse(rawInput); const queryId = queryIdentifier(graph, input); const seeds = resolveSeeds(graph, input); - const admission = admitSeeds(graph, seeds); - const retainedSeeds = admission.nodeIds; + const nodes = new Map(graph.nodes.map((node) => [node.node_id, node])); + const retainedSeeds = admitSeeds(nodes, seeds); const adjacency = buildAdjacency(graph.relations, input); - const traversal = traverse(graph, retainedSeeds, adjacency); + const traversal = traverse(nodes, retainedSeeds, adjacency); const result = createQueryResult(graph, traversal); const allRelevantUnknowns = relevantUnknownFrontiers( graph, @@ -122,17 +118,16 @@ const createQueryResult = ( }; const admitSeeds = ( - graph: JavaScriptSemanticGraph, + nodes: ReadonlyMap, seeds: readonly string[], -): SeedAdmission => { - const nodes = new Map(graph.nodes.map((node) => [node.node_id, node])); +): string[] => { const retained: string[] = []; for (const nodeId of seeds) { const node = nodes.get(nodeId); if (node === undefined) continue; retained.push(nodeId); } - return { nodeIds: retained }; + return retained; }; const buildAdjacency = ( @@ -222,11 +217,10 @@ const addEntry = ( }; const traverse = ( - graph: JavaScriptSemanticGraph, + nodes: ReadonlyMap, seeds: readonly string[], adjacency: ReadonlyMap, ): Traversal => { - const nodes = new Map(graph.nodes.map((node) => [node.node_id, node])); const nodeIds = new Set(seeds); const relationIds = new Set(); const functionIds = new Set(); diff --git a/tests/composition/runtime-observation/javascriptRuntimeReconciliation.test.ts b/tests/composition/runtime-observation/javascriptRuntimeReconciliation.test.ts index a6809156eb..ec59b35c7c 100644 --- a/tests/composition/runtime-observation/javascriptRuntimeReconciliation.test.ts +++ b/tests/composition/runtime-observation/javascriptRuntimeReconciliation.test.ts @@ -316,7 +316,7 @@ it("imports an operator-provided cache layer through an explicit file mapping", const application = await applicationFixture(); const cache = await createTestTempDirectory("rea-runtime-cache-static-"); const runtimeCache = await createTestTempDirectory("rea-runtime-cache-live-"); - const cacheSource = "export const cachedFeature = 'fixture';\n"; + const cacheSource = SOURCE; await mkdir(join(cache, "mapped")); await Promise.all([ writeFile(join(cache, "mapped", "chunk.js"), cacheSource), @@ -373,6 +373,16 @@ it("imports an operator-provided cache layer through an explicit file mapping", status: "unknown", reason: "layer-outside-runtime-scope", }); + const sharedFile = result.graph.nodes.find( + ({ observations }) => + observations.some(({ properties }) => properties.path === "app.js") && + observations.some( + ({ properties }) => properties.path === "mapped/chunk.js", + ), + ); + expect( + sharedFile?.observations.map(({ properties }) => properties.path), + ).toEqual(expect.arrayContaining(["app.js", "mapped/chunk.js"])); }); const applicationFixture = async (): Promise => {