From dce81ca400eda369fe65b3de6e5a9aef4dfdaa50 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:32:11 +0800 Subject: [PATCH] perf(javascript): count candidate relevance without a second adjacency Scan eligible candidate relations directly for query ambiguity instead of allocating and sorting another traversal index. Preserve direction, relation filters and cycle handling. Semantic graph and builder suites plus public application workflow CLI tests passed. Full query outputs matched on a generated 20000-node graph; the single repeated-query timing run is indicative only. --- .../javascriptSemanticGraphQuery.test.ts | 100 ++++++++++++++++++ .../javascript/javascriptSemanticQuery.ts | 35 ++++-- 2 files changed, 126 insertions(+), 9 deletions(-) diff --git a/src/domain/javascript/javascriptSemanticGraphQuery.test.ts b/src/domain/javascript/javascriptSemanticGraphQuery.test.ts index 9a99872d4..17d03a1d1 100644 --- a/src/domain/javascript/javascriptSemanticGraphQuery.test.ts +++ b/src/domain/javascript/javascriptSemanticGraphQuery.test.ts @@ -4,6 +4,7 @@ import { z } from "zod"; import { digestCanonicalValue } from "../canonicalDigest.js"; import type { ApplicationGraphEvidence } from "./javascriptApplicationEvidenceSchemas.js"; import { + JAVASCRIPT_SEMANTIC_RELATION_FAMILY, JAVASCRIPT_SEMANTIC_NODE_KINDS, JAVASCRIPT_SEMANTIC_RELATION_FAMILIES, JAVASCRIPT_SEMANTIC_RELATIONS, @@ -27,6 +28,7 @@ import { parseJavaScriptSemanticGraph, serializeJavaScriptSemanticGraph, } from "./javascriptSemanticGraphSerialization.js"; +import { queryJavaScriptSemanticGraph } from "./javascriptSemanticQuery.js"; const SHA = "a".repeat(64); const JAG_ID = `jag_${"b".repeat(64)}`; @@ -323,6 +325,104 @@ const registryForGraph = ( return registry; }; +const graphWithCandidateRelations = (): JavaScriptSemanticGraph => { + const base = fixtureGraph(); + const evidenceContexts = registryForGraph(base); + const relations = base.relations.map( + ({ relation_id: _relationId, ...relation }) => + createJavaScriptSemanticGraphRelation( + { + ...relation, + resolution: "candidate", + evidence: resolveJavaScriptSemanticEvidence(base, relation.evidence), + }, + evidenceContexts, + ), + ); + const binding = base.nodes.find(({ label }) => label === "binding"); + const callable = base.nodes.find(({ label }) => label === "function"); + if (binding === undefined || callable === undefined) + throw new Error("Expected candidate-cycle endpoints"); + for (const [source, target] of [ + [binding, callable], + [callable, binding], + ] as const) + relations.push( + createJavaScriptSemanticGraphRelation( + { + source_node_id: source.node_id, + target_node_id: target.node_id, + relation: "owns", + resolution: "candidate", + properties: {}, + evidence: evidence("inferred"), + }, + evidenceContexts, + ), + ); + + const { graph_id: _graphId, ...input } = base; + return createJavaScriptSemanticGraph({ + ...input, + evidence_contexts: evidenceContexts.contexts, + relations, + coverage: { + ...input.coverage, + families: input.coverage.families.map((family) => ({ + ...family, + retained_relations: relations.filter( + ({ relation }) => + JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation] === family.family, + ).length, + })), + }, + }); +}; + +it("counts candidate edges by query direction and relation filter", () => { + const graph = graphWithCandidateRelations(); + const binding = graph.nodes.find(({ label }) => label === "binding"); + if (binding === undefined) throw new Error("Expected binding seed"); + const query = ( + direction: "forward-influence" | "backward-provenance", + allowedRelations: (typeof JAVASCRIPT_SEMANTIC_RELATIONS)[number][], + ) => + queryJavaScriptSemanticGraph(graph, { + seed: { kind: "semantic-node", node_id: binding.node_id }, + direction, + allowed_relations: allowedRelations, + }); + + expect(query("forward-influence", ["captures"]).status).toBe("ambiguous"); + expect(query("forward-influence", ["defines"]).status).toBe("found"); + expect(query("backward-provenance", ["defines"]).status).toBe("ambiguous"); + expect(query("backward-provenance", ["captures"]).status).toBe("found"); +}); + +it("counts candidate ownership cycles once and traverses them finitely", () => { + const graph = graphWithCandidateRelations(); + const binding = graph.nodes.find(({ label }) => label === "binding"); + if (binding === undefined) throw new Error("Expected binding seed"); + const result = queryJavaScriptSemanticGraph(graph, { + seed: { kind: "semantic-node", node_id: binding.node_id }, + direction: "ownership", + allowed_relations: ["owns"], + include_ambiguous_dynamic_edges: true, + }); + + expect(result.status).toBe("ambiguous"); + expect(result.relations).toHaveLength(2); + expect(result.summary.traversed_nodes).toBe(2); + expect(result.summary.traversed_relations).toBe(2); + expect( + queryJavaScriptSemanticGraph(graph, { + seed: { kind: "semantic-node", node_id: binding.node_id }, + direction: "ownership", + allowed_relations: ["constructs-request"], + }).status, + ).toBe("found"); +}); + it("defines every required relation family and canonicalizes records", () => { expect(JAVASCRIPT_SEMANTIC_RELATIONS).toContain("argument-to-parameter"); expect(JAVASCRIPT_SEMANTIC_RELATIONS).toContain("detaches-task"); diff --git a/src/domain/javascript/javascriptSemanticQuery.ts b/src/domain/javascript/javascriptSemanticQuery.ts index 187016257..54c407102 100644 --- a/src/domain/javascript/javascriptSemanticQuery.ts +++ b/src/domain/javascript/javascriptSemanticQuery.ts @@ -283,15 +283,32 @@ const relevantCandidateRelationCount = ( nodeIds: ReadonlySet, input: JavaScriptSemanticQueryInput, ): number => { - const adjacency = buildAdjacency( - graph.relations.filter(({ resolution }) => resolution === "candidate"), - { ...input, include_ambiguous_dynamic_edges: true }, - ); - const relevant = new Set(); - for (const nodeId of nodeIds) - for (const { relation } of adjacency.get(nodeId) ?? []) - relevant.add(relation.relation_id); - return relevant.size; + const allowed = + input.allowed_relations === undefined + ? null + : new Set(input.allowed_relations); + let relevant = 0; + for (const relation of graph.relations) { + if ( + relation.resolution !== "candidate" || + (allowed !== null && !allowed.has(relation.relation)) + ) + continue; + const sourceIsRelevant = nodeIds.has(relation.source_node_id); + const targetIsRelevant = nodeIds.has(relation.target_node_id); + if ( + (input.direction === "callers" && + relation.relation === "calls" && + targetIsRelevant) || + (input.direction === "ownership" && + ownershipRelation(relation.relation) && + (sourceIsRelevant || targetIsRelevant)) || + (input.direction === "backward-provenance" && targetIsRelevant) || + (input.direction === "forward-influence" && sourceIsRelevant) + ) + relevant += 1; + } + return relevant; }; const expectedMatchesFor = (