Merge pull request #663 from kaoru0822-kitauji/fix/semantic-candidate-relevance

fix(javascript): scope candidate ambiguity to query traversal
This commit is contained in:
Kaoru0822 2026-10-05 23:06:33 -07:00 committed by GitHub
commit 6ad62cec6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 205 additions and 11 deletions

View File

@ -4,6 +4,7 @@ import type { ApplicationGraphEvidence } from "./javascriptApplicationEvidenceSc
import {
JAVASCRIPT_SEMANTIC_NODE_KINDS,
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES,
JAVASCRIPT_SEMANTIC_RELATION_FAMILY,
} from "./javascriptSemanticGraphSchemas.js";
import {
createJavaScriptSemanticFingerprint,
@ -13,6 +14,7 @@ import {
createJavaScriptSemanticGraphUnknown,
type JavaScriptSemanticGraph,
type JavaScriptSemanticGraphNode,
type JavaScriptSemanticGraphRelation,
} from "./javascriptSemanticGraph.js";
import { queryJavaScriptSemanticGraph } from "./javascriptSemanticQuery.js";
import type { JsonValue } from "./jsonValue.js";
@ -396,3 +398,197 @@ it("retains all explicit unresolved candidate node identifiers", () => {
});
expect(unknown.candidate_node_ids).toHaveLength(1_001);
});
const directionalGraph = (
nodes: readonly JavaScriptSemanticGraphNode[],
relations: readonly JavaScriptSemanticGraphRelation[],
partial = false,
): JavaScriptSemanticGraph =>
createJavaScriptSemanticGraph({
schema: "JavaScriptSemanticRelationGraph",
root_artifact_sha256: SHA,
application_graph_id: JAG_ID,
root_node_ids: nodes.slice(0, 1).map(({ node_id }) => node_id),
nodes: [...nodes],
relations: [...relations],
fingerprints: [],
unknowns: [],
coverage: {
status: partial ? "partial" : "complete",
truncated: false,
omitted_nodes: partial ? 1 : 0,
omitted_relations: 0,
limits: [],
families: JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => ({
family,
status: "complete",
retained_relations: relations.filter(
({ relation }) =>
JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation] === family,
).length,
omitted_relations: 0,
unknown_ids: [],
})),
},
limitations: partial ? ["One source graph node was omitted."] : [],
});
const relationBetween = (
source: JavaScriptSemanticGraphNode,
target: JavaScriptSemanticGraphNode,
relation: JavaScriptSemanticGraphRelation["relation"],
resolution: JavaScriptSemanticGraphRelation["resolution"] = "resolved",
) =>
createJavaScriptSemanticGraphRelation({
source_node_id: source.node_id,
target_node_id: target.node_id,
relation,
resolution,
properties: {},
evidence: evidence("inferred"),
});
it.each([
{
direction: "forward-influence",
relation: "calls",
incoming: true,
relevant: false,
},
{
direction: "forward-influence",
relation: "calls",
incoming: false,
relevant: true,
},
{
direction: "backward-provenance",
relation: "calls",
incoming: false,
relevant: false,
},
{
direction: "backward-provenance",
relation: "calls",
incoming: true,
relevant: true,
},
{ direction: "callers", relation: "reads", incoming: true, relevant: false },
{ direction: "callers", relation: "calls", incoming: false, relevant: false },
{ direction: "callers", relation: "calls", incoming: true, relevant: true },
{
direction: "ownership",
relation: "reads",
incoming: false,
relevant: false,
},
{ direction: "ownership", relation: "owns", incoming: false, relevant: true },
{ direction: "ownership", relation: "owns", incoming: true, relevant: true },
] as const)(
"limits $direction ambiguity to traversable $relation candidates (incoming=$incoming)",
({ direction, relation, incoming, relevant }) => {
const seed = node("function", "directional-seed");
const reached = node("function", "directional-reached");
const other = node("function", "directional-other");
const reverse =
direction === "callers" || direction === "backward-provenance";
const resolved = relationBetween(
reverse ? reached : seed,
reverse ? seed : reached,
direction === "ownership" ? "owns" : "calls",
);
const candidate = relationBetween(
incoming ? other : reached,
incoming ? reached : other,
relation,
"candidate",
);
const nodes = [seed, reached, other];
const control = queryJavaScriptSemanticGraph(
directionalGraph(nodes, [resolved]),
{
seed: { kind: "semantic-node", node_id: seed.node_id },
direction,
},
);
expect(control).toMatchObject({
status: "found",
coverage: { status: "complete" },
});
for (const include of [false, true]) {
const result = queryJavaScriptSemanticGraph(
directionalGraph(nodes, [resolved, candidate]),
{
seed: { kind: "semantic-node", node_id: seed.node_id },
direction,
include_ambiguous_dynamic_edges: include,
},
);
expect(result).toMatchObject({
status: relevant ? "ambiguous" : "found",
coverage: { status: relevant ? "partial" : "complete" },
});
if (!relevant || !include) {
expect(result.nodes).toEqual(control.nodes);
expect(result.relations).toEqual(control.relations);
} else {
expect(result.nodes).toHaveLength(3);
expect(result.relations).toContainEqual(candidate);
}
}
},
);
it("excludes explicitly filtered candidates without hiding partial graph coverage", () => {
const seed = node("function", "filtered-seed");
const other = node("function", "filtered-other");
const candidate = relationBetween(seed, other, "reads", "candidate");
for (const partial of [false, true]) {
const result = queryJavaScriptSemanticGraph(
directionalGraph([seed, other], [candidate], partial),
{
seed: { kind: "semantic-node", node_id: seed.node_id },
direction: "forward-influence",
allowed_relations: ["calls"],
},
);
expect(result).toMatchObject({
status: "found",
coverage: { status: partial ? "partial" : "complete" },
relations: [],
});
}
});
it("keeps candidate uncertainty at the end of a long reachable path", () => {
const chain = Array.from({ length: 72 }, (_, index) =>
node("binding", `candidate-chain-${index}`),
);
const relations = chain.slice(1).map((target, index) => {
const source = chain[index];
if (source === undefined) throw new TypeError("Missing chain source");
return relationBetween(
source,
target,
"reads",
index === 70 ? "candidate" : "resolved",
);
});
const seed = chain[0];
if (seed === undefined) throw new TypeError("Missing chain seed");
for (const include of [false, true]) {
const result = queryJavaScriptSemanticGraph(
directionalGraph(chain, relations),
{
seed: { kind: "semantic-node", node_id: seed.node_id },
direction: "forward-influence",
include_ambiguous_dynamic_edges: include,
},
);
expect(result).toMatchObject({
status: "ambiguous",
coverage: { status: "partial" },
});
expect(result.nodes).toHaveLength(include ? 72 : 71);
}
});

View File

@ -281,17 +281,15 @@ const relevantCandidateRelationCount = (
nodeIds: ReadonlySet<string>,
input: JavaScriptSemanticQueryInput,
): number => {
const allowed =
input.allowed_relations === undefined
? null
: new Set(input.allowed_relations);
return graph.relations.filter(
(relation) =>
relation.resolution === "candidate" &&
(allowed === null || allowed.has(relation.relation)) &&
(nodeIds.has(relation.source_node_id) ||
nodeIds.has(relation.target_node_id)),
).length;
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 expectedMatchesFor = (