feat(javascript): expose bounded semantic tracing

This commit is contained in:
morluto
2026-07-22 23:07:51 +00:00
parent 8678b57b5e
commit 508a9ccde9
56 changed files with 2455 additions and 307 deletions
+87 -165
View File
@@ -1,17 +1,11 @@
import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { compareCodePoints } from "./javascriptApplicationGraph.js";
import type {
JavaScriptSemanticGraph,
JavaScriptSemanticGraphNode,
JavaScriptSemanticGraphRelation,
} from "./javascriptSemanticGraph.js";
import {
JAVASCRIPT_SEMANTIC_RELATION_FAMILY,
type JavaScriptSemanticGraphUnknown,
} from "./javascriptSemanticGraphSchemas.js";
import type { JavaScriptSemanticGraphUnknown } from "./javascriptSemanticGraphSchemas.js";
import { isJavaScriptSemanticOwnershipRelation as ownershipRelation } from "./javascriptSemanticQueryRelations.js";
import {
javaScriptSemanticQueryInputSchema,
javaScriptSemanticQueryResultSchema,
@@ -22,6 +16,14 @@ import {
assessJavaScriptSemanticQuery,
type JavaScriptSemanticQueryFrontier,
} from "./javascriptSemanticQueryAssessment.js";
import {
canonicalJavaScriptSemanticQueryJson as canonicalJson,
createJavaScriptSemanticQueryCursor as createCursor,
JavaScriptSemanticQueryCursorError,
javaScriptSemanticQueryIdentifier as queryIdentifier,
parseJavaScriptSemanticQueryCursor as parseCursor,
resolveJavaScriptSemanticQuerySeeds as resolveSeeds,
} from "./javascriptSemanticQueryIdentity.js";
interface TraversalEntry {
readonly relation: JavaScriptSemanticGraphRelation;
@@ -43,20 +45,22 @@ interface SeedAdmission {
readonly frontier: QueryFrontier[];
}
const canonicalJson = (value: unknown): string => {
const encoded = canonicalize(value);
if (encoded === undefined)
throw new TypeError(
"JavaScript semantic query could not canonicalize data",
);
return encoded;
};
interface QueryPage {
readonly retainedNodes: JavaScriptSemanticGraphNode[];
readonly retainedRelations: JavaScriptSemanticGraphRelation[];
readonly pageRelations: JavaScriptSemanticGraphRelation[];
readonly pageNodes: JavaScriptSemanticGraphNode[];
readonly nextCursor: string | null;
}
const digest = (value: unknown): string =>
createHash("sha256").update(canonicalJson(value)).digest("hex");
const uniqueSorted = (values: readonly string[]): string[] =>
[...new Set(values)].sort(compareCodePoints);
interface QueryPageInput {
readonly graph: JavaScriptSemanticGraph;
readonly traversal: Traversal;
readonly query: JavaScriptSemanticQueryInput;
readonly queryId: string;
readonly offset: number;
readonly retainedSeeds: readonly string[];
}
/** Run one deterministic bounded traversal over a verified semantic graph. */
export const queryJavaScriptSemanticGraph = (
@@ -75,41 +79,31 @@ export const queryJavaScriptSemanticGraph = (
...traversal.frontier,
...admission.frontier,
]);
const retainedNodes = graph.nodes.filter(({ node_id }) =>
traversal.nodeIds.has(node_id),
);
const retainedRelations = graph.relations.filter(({ relation_id }) =>
traversal.relationIds.has(relation_id),
);
if (offset > retainedRelations.length)
throw new TypeError("Semantic query cursor is beyond the retained result");
const pageRelations = retainedRelations.slice(
const page = createQueryPage({
graph,
traversal,
query: input,
queryId,
offset,
offset + input.limits.page_size,
);
const pageNodeIds = new Set([
...retainedSeeds,
...pageRelations.flatMap(({ source_node_id, target_node_id }) => [
source_node_id,
target_node_id,
]),
]);
const pageNodes = graph.nodes.filter(({ node_id }) =>
pageNodeIds.has(node_id),
);
const relevantUnknowns = relevantUnknownFrontiers(
retainedSeeds,
});
const allRelevantUnknowns = relevantUnknownFrontiers(
graph,
traversal.nodeIds,
input,
);
const relevantUnknowns = allRelevantUnknowns.slice(
0,
input.limits.max_unknowns,
);
const candidateRelations = relevantCandidateRelationCount(
graph,
traversal.nodeIds,
input,
);
const expectedMatches = expectedMatchesFor(
retainedNodes,
retainedRelations,
page.retainedNodes,
page.retainedRelations,
input,
);
const assessment = assessJavaScriptSemanticQuery({
@@ -121,12 +115,8 @@ export const queryJavaScriptSemanticGraph = (
frontier,
unknowns: relevantUnknowns,
candidateRelations,
unknownsTruncated: allRelevantUnknowns.length > relevantUnknowns.length,
});
const nextOffset = offset + pageRelations.length;
const nextCursor =
nextOffset < retainedRelations.length
? createCursor(queryId, nextOffset)
: null;
return javaScriptSemanticQueryResultSchema.parse({
schema_version: 1,
query_id: queryId,
@@ -135,8 +125,8 @@ export const queryJavaScriptSemanticGraph = (
direction: input.direction,
status: assessment.status,
seed_node_ids: retainedSeeds,
nodes: pageNodes,
relations: pageRelations,
nodes: page.pageNodes,
relations: page.pageRelations,
unknowns: relevantUnknowns,
expected_match_node_ids: expectedMatches.map(({ node_id }) => node_id),
summary: {
@@ -146,13 +136,14 @@ export const queryJavaScriptSemanticGraph = (
traversed_relations: traversal.relationIds.size,
traversed_functions: traversal.functionIds.size,
traversed_modules: traversal.modules.size,
relevant_unknowns: relevantUnknowns.length,
relevant_unknowns: allRelevantUnknowns.length,
retained_unknowns: relevantUnknowns.length,
},
coverage: assessment.coverage,
page: {
offset,
size: pageRelations.length,
next_cursor: nextCursor,
size: page.pageRelations.length,
next_cursor: page.nextCursor,
},
applied_limits: input.limits,
accepted_limit_ranges: assessment.acceptedLimitRanges,
@@ -160,96 +151,49 @@ export const queryJavaScriptSemanticGraph = (
});
};
const queryIdentifier = (
graph: JavaScriptSemanticGraph,
input: JavaScriptSemanticQueryInput,
): string => {
const { cursor: _cursor, ...semantic } = input;
return `jsrq_${digest({
source_graph_id: graph.graph_id,
...semantic,
allowed_relations:
semantic.allowed_relations === undefined
? undefined
: uniqueSorted(semantic.allowed_relations),
expected:
semantic.expected === null
? null
: {
...semantic.expected,
classes: uniqueSorted(semantic.expected.classes),
},
})}`;
};
const createCursor = (queryId: string, offset: number): string =>
`jsrqc_${String(offset)}_${digest({ query_id: queryId, offset })}`;
const parseCursor = (cursor: string | null, queryId: string): number => {
if (cursor === null) return 0;
const match = /^jsrqc_([0-9]+)_([a-f0-9]{64})$/u.exec(cursor);
const offsetText = match?.[1];
const commitment = match?.[2];
if (offsetText === undefined || commitment === undefined)
throw new TypeError("Semantic query cursor is malformed");
const offset = Number(offsetText);
if (
!Number.isSafeInteger(offset) ||
offset < 0 ||
commitment !== digest({ query_id: queryId, offset })
)
throw new TypeError("Semantic query cursor does not match this query");
return offset;
};
const resolveSeeds = (
graph: JavaScriptSemanticGraph,
input: JavaScriptSemanticQueryInput,
): string[] => {
const fingerprintFunctions = new Set(
input.seed.kind === "function"
? graph.fingerprints
.filter(({ fingerprint_sha256 }) => {
const seed = input.seed;
return (
seed.kind === "function" &&
fingerprint_sha256 === seed.fingerprint_sha256
);
})
.map(({ function_node_id }) => function_node_id)
: [],
const createQueryPage = ({
graph,
traversal,
query,
queryId,
offset,
retainedSeeds,
}: QueryPageInput): QueryPage => {
const retainedNodes = graph.nodes.filter(({ node_id }) =>
traversal.nodeIds.has(node_id),
);
return graph.nodes
.filter((node) => seedMatches(node, input, fingerprintFunctions))
.map(({ node_id }) => node_id)
.sort(compareCodePoints);
};
const seedMatches = (
node: JavaScriptSemanticGraphNode,
input: JavaScriptSemanticQueryInput,
fingerprintFunctions: ReadonlySet<string>,
): boolean => {
const seed = input.seed;
if (seed.kind === "semantic-node") return node.node_id === seed.node_id;
if (seed.kind === "application-node")
return node.application_node_ids.includes(seed.node_id);
if (seed.kind === "function") return fingerprintFunctions.has(node.node_id);
if (seed.kind === "literal")
return (
node.kind === "literal" &&
canonicalJson(node.properties.value) === canonicalJson(seed.value)
const retainedRelations = graph.relations.filter(({ relation_id }) =>
traversal.relationIds.has(relation_id),
);
if (offset > retainedRelations.length)
throw new JavaScriptSemanticQueryCursorError(
"Semantic query cursor is beyond the retained result",
);
if (seed.kind === "property")
return node.kind === "property-slot" && node.properties.name === seed.name;
if (seed.kind === "endpoint")
return node.kind === "request" && node.properties.endpoint === seed.value;
if (seed.kind === "event")
return (
["event", "listener"].includes(node.kind) &&
node.properties.event_name === seed.name
);
return node.kind === "boundary" && node.properties.field === seed.field;
const pageRelations = retainedRelations.slice(
offset,
offset + query.limits.page_size,
);
const pageNodeIds = new Set([
...retainedSeeds,
...pageRelations.flatMap(({ source_node_id, target_node_id }) => [
source_node_id,
target_node_id,
]),
]);
const pageNodes = graph.nodes.filter(({ node_id }) =>
pageNodeIds.has(node_id),
);
const nextOffset = offset + pageRelations.length;
return {
retainedNodes,
retainedRelations,
pageRelations,
pageNodes,
nextCursor:
nextOffset < retainedRelations.length
? createCursor(queryId, nextOffset)
: null,
};
};
const admitSeeds = (
@@ -395,23 +339,6 @@ const addDirectedEntries = (
);
};
const ownershipRelation = (
relation: JavaScriptSemanticGraphRelation["relation"],
): boolean =>
[
"acquires",
"aggregates",
"awaits",
"chains",
"connects-stdio",
"creates-promise",
"detaches-task",
"owns",
"releases",
"returns-task",
"spawns",
].includes(relation);
const addEntry = (
adjacency: Map<string, TraversalEntry[]>,
from: string,
@@ -585,8 +512,3 @@ const expectedMatchesFor = (
({ kind, node_id }) => classes.has(kind) && !connected.has(node_id),
);
};
/** Return a relation's declared semantic coverage family. */
export const javaScriptSemanticRelationFamily = (
relation: JavaScriptSemanticGraphRelation["relation"],
) => JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation];