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.
This commit is contained in:
morluto 2026-10-11 02:32:11 +08:00
parent bb764d26bd
commit dce81ca400
2 changed files with 126 additions and 9 deletions

View File

@ -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");

View File

@ -283,15 +283,32 @@ const relevantCandidateRelationCount = (
nodeIds: ReadonlySet<string>,
input: JavaScriptSemanticQueryInput,
): number => {
const adjacency = buildAdjacency(
graph.relations.filter(({ resolution }) => resolution === "candidate"),
{ ...input, include_ambiguous_dynamic_edges: true },
);
const relevant = new Set<string>();
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 = (