mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04:00
feat(javascript): expose bounded semantic tracing
This commit is contained in:
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user