rea-agents 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -18
- package/dist/application/AndroidApplicationService.js +17 -0
- package/dist/application/AppleApplicationService.js +17 -0
- package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
- package/dist/application/BinarySession.js +9 -2
- package/dist/application/BinarySessionStatus.js +39 -1
- package/dist/application/CapabilityInventory.js +15 -0
- package/dist/application/ClientRegistrationStatus.js +11 -4
- package/dist/application/CommandShimReplay.js +6 -3
- package/dist/application/CompositeProvider.js +5 -2
- package/dist/application/DirectAnalysis.js +1 -20
- package/dist/application/DirectAnalysisStatus.js +83 -0
- package/dist/application/Doctor.js +34 -25
- package/dist/application/DoctorDiagnostics.js +1 -4
- package/dist/application/DoctorProjection.js +43 -0
- package/dist/application/DoctorScope.js +102 -0
- package/dist/application/EvidenceReferenceResolver.js +26 -0
- package/dist/application/InventoryProjectionEvidence.js +41 -0
- package/dist/application/InvestigationProviders.js +12 -0
- package/dist/application/JavaScriptApplicationEvidence.js +1 -1
- package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
- package/dist/application/JavaScriptApplicationService.js +3 -2
- package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
- package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
- package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
- package/dist/application/JavaScriptModuleRelationships.js +25 -1
- package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
- package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
- package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
- package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
- package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
- package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
- package/dist/application/JavaScriptSemanticTraceService.js +45 -0
- package/dist/application/LoopbackReplay.js +277 -133
- package/dist/application/LoopbackReplayRecorder.js +101 -0
- package/dist/application/PermissionAuthority.js +7 -3
- package/dist/application/ProcessCaptureJournal.js +95 -0
- package/dist/application/ProcessCaptureLifecycle.js +24 -13
- package/dist/application/ProcessCheckpoints.js +30 -4
- package/dist/application/ProcessCli.js +26 -4
- package/dist/application/ProcessHarness.js +20 -50
- package/dist/application/ProcessNormalization.js +3 -2
- package/dist/application/ProcessPairedExperiment.js +72 -0
- package/dist/application/ProcessReactiveCoordinator.js +218 -0
- package/dist/application/ProcessReactiveEffects.js +133 -0
- package/dist/application/ProcessReactiveObservations.js +20 -0
- package/dist/application/ProcessSampling.js +2 -0
- package/dist/application/SessionProviderRouter.js +7 -6
- package/dist/application/Setup.js +14 -13
- package/dist/application/SetupClientConfiguration.js +10 -6
- package/dist/application/SetupPlan.js +4 -3
- package/dist/application/SetupRegistrationEnvironment.js +12 -0
- package/dist/application/SetupSkill.js +44 -40
- package/dist/application/TerminalRenderer.js +3 -1
- package/dist/application/Uninstall.js +7 -1
- package/dist/application/Upgrade.js +16 -1
- package/dist/browser/CdpEndpoint.js +37 -4
- package/dist/catalogIdentity.js +16 -7
- package/dist/cli/coreAnalysisCommands.js +34 -2
- package/dist/cli/javascriptApplicationAnalysis.js +19 -0
- package/dist/cli/setupCommands.js +84 -30
- package/dist/cli/utilityCommands.js +10 -2
- package/dist/cliApplicationCommands.js +79 -16
- package/dist/cliCommandNames.js +3 -0
- package/dist/cliElectronCommands.js +23 -33
- package/dist/cliObservationOptions.js +1 -1
- package/dist/cliOutput.js +56 -0
- package/dist/cliProcessCommands.js +32 -1
- package/dist/cliSetup.js +55 -29
- package/dist/contracts/applicationToolContracts.js +54 -7
- package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
- package/dist/contracts/electronToolContracts.js +73 -4
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
- package/dist/contracts/managedToolContracts.js +7 -2
- package/dist/contracts/processCaptureExample.js +1 -0
- package/dist/contracts/replayMachineExample.js +37 -0
- package/dist/contracts/sessionStatusContract.js +26 -0
- package/dist/contracts/toolContractExamples.js +2 -0
- package/dist/contracts/toolContractTypes.js +12 -1
- package/dist/contracts/toolContracts.js +8 -11
- package/dist/contracts/toolEffects.js +3 -0
- package/dist/contracts/toolOutputSchemaGroups.js +53 -9
- package/dist/contracts/toolOutputSchemaPrimitives.js +73 -32
- package/dist/doctorRuntime.js +41 -0
- package/dist/domain/analysisErrorPresentation.js +5 -3
- package/dist/domain/analysisErrorProjection.js +7 -1
- package/dist/domain/androidApplication.js +206 -0
- package/dist/domain/appleApplication.js +221 -0
- package/dist/domain/boundedCartesianProjection.js +15 -0
- package/dist/domain/changedBehavior.js +5 -4
- package/dist/domain/completionLedgerGeneration.js +190 -0
- package/dist/domain/errors.js +4 -2
- package/dist/domain/evidenceCompletionLedger.js +113 -0
- package/dist/domain/hopperStartupFailure.js +9 -0
- package/dist/domain/javascriptApplicationAnalysis.js +44 -2
- package/dist/domain/javascriptExportShapeComparison.js +145 -0
- package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
- package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
- package/dist/domain/javascriptExportShapeSelection.js +132 -0
- package/dist/domain/javascriptExportShapeVariants.js +272 -0
- package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
- package/dist/domain/javascriptSemanticAnalysis.js +17 -36
- package/dist/domain/javascriptSemanticCallResolution.js +129 -0
- package/dist/domain/javascriptSemanticCalls.js +269 -0
- package/dist/domain/javascriptSemanticGraph.js +356 -0
- package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
- package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
- package/dist/domain/javascriptSemanticIr.js +14 -1
- package/dist/domain/javascriptSemanticPrimitives.js +36 -0
- package/dist/domain/javascriptSemanticProjection.js +36 -4
- package/dist/domain/javascriptSemanticProvenance.js +29 -0
- package/dist/domain/javascriptSemanticQuery.js +294 -0
- package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
- package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
- package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
- package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
- package/dist/domain/javascriptSemanticReturns.js +187 -0
- package/dist/domain/javascriptSemanticState.js +32 -0
- package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
- package/dist/domain/javascriptSemanticValues.js +54 -70
- package/dist/domain/processCapture.js +36 -0
- package/dist/domain/processCaptureValidation.js +101 -12
- package/dist/domain/processComparison.js +96 -24
- package/dist/domain/processObservation.js +153 -0
- package/dist/domain/processPairedExperiment.js +128 -0
- package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
- package/dist/domain/processReactiveMatching.js +210 -0
- package/dist/domain/processReactiveRuntime.js +109 -0
- package/dist/domain/processReactiveScenario.js +361 -0
- package/dist/domain/processReactiveScenarioPreflight.js +120 -0
- package/dist/domain/processReactiveTransition.js +159 -0
- package/dist/domain/processScenario.js +12 -0
- package/dist/domain/processTraceComparison.js +43 -0
- package/dist/domain/processTraceDimensionProjection.js +50 -0
- package/dist/domain/processTraceEvaluation.js +312 -0
- package/dist/domain/processTraceSpecification.js +373 -0
- package/dist/domain/reconstructionVerification.js +5 -4
- package/dist/domain/replayMachine.js +332 -0
- package/dist/domain/replayMachineRun.js +216 -0
- package/dist/domain/replayMachineRuntime.js +294 -0
- package/dist/domain/runtimeIdentification.js +210 -0
- package/dist/domain/staticRuntimeCorrelation.js +4 -2
- package/dist/evaluation/CodexAgentEval.js +156 -0
- package/dist/generatedPackageMetadata.js +2 -2
- package/dist/ghidra/GhidraClient.js +13 -2
- package/dist/ghidra/GhidraDiagnostics.js +1 -0
- package/dist/ghidra/GhidraLauncher.js +7 -3
- package/dist/ghidra/GhidraProvider.js +8 -1
- package/dist/hopper/BridgeLauncher.js +56 -14
- package/dist/hopper/HopperClient.js +23 -10
- package/dist/hopper/HopperDiagnostics.js +14 -0
- package/dist/hopper/HopperProcessDiagnostic.js +14 -0
- package/dist/hopper/HopperProvider.js +8 -1
- package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
- package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
- package/dist/identity.js +2 -1
- package/dist/main/transport.js +2 -0
- package/dist/mcpDoctor.js +310 -0
- package/dist/process/ProcessOwnership.js +200 -53
- package/dist/process/ProviderRunLineage.js +21 -0
- package/dist/server/createServer.js +8 -2
- package/dist/server/javascriptApplicationResult.js +106 -0
- package/dist/server/registerApplicationTools/characterization.js +3 -3
- package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
- package/dist/server/registerApplicationTools/compareVersions.js +12 -6
- package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
- package/dist/server/registerApplicationTools/coverage.js +3 -3
- package/dist/server/registerApplicationTools/helpers.js +37 -25
- package/dist/server/registerApplicationTools/traceFeature.js +9 -5
- package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
- package/dist/server/registerApplicationTools.js +4 -0
- package/dist/server/registerElectronTools.js +19 -5
- package/dist/server/registerEvidenceResources.js +7 -1
- package/dist/server/registerJavaScriptApplicationGraphResource.js +69 -0
- package/dist/server/registerManagedTools.js +18 -1
- package/dist/server/registerProcessComparisonTool.js +17 -7
- package/dist/server/registerReplayMachineTool.js +20 -0
- package/dist/server/registerSessionStatusTool.js +95 -25
- package/dist/server/registerSessionTools.js +12 -7
- package/dist/server/sessionAvailabilityPolicy.js +2 -1
- package/dist/server/toolResult.js +10 -4
- package/dist/serverIdentity.js +19 -0
- package/package.json +62 -29
- package/scripts/hopper-demo-x11.py +459 -61
- package/scripts/prepack.mjs +55 -0
- package/scripts/prepare.mjs +28 -0
- package/scripts/rea.mjs +26 -5
- package/skills/reverse-engineer-anything/SKILL.md +69 -228
- package/skills/reverse-engineer-anything/references/controlled-replay.md +12 -0
- package/skills/reverse-engineer-anything/references/evidence-workflows.md +33 -0
- package/skills/reverse-engineer-anything/references/javascript-applications.md +35 -0
- package/skills/reverse-engineer-anything/references/native-and-artifacts.md +36 -0
- package/skills/reverse-engineer-anything/references/runtime-observation.md +24 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import canonicalize from "canonicalize";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { javaScriptSemanticGraphSchema, } from "./javascriptSemanticGraph.js";
|
|
4
|
+
const canonicalJson = (value) => {
|
|
5
|
+
const encoded = canonicalize(value);
|
|
6
|
+
if (encoded === undefined)
|
|
7
|
+
throw new TypeError("JavaScript semantic graph could not canonicalize data");
|
|
8
|
+
return encoded;
|
|
9
|
+
};
|
|
10
|
+
/** Parse a stored semantic graph and reject unsupported versions or stale IDs. */
|
|
11
|
+
export const parseJavaScriptSemanticGraph = (input) => {
|
|
12
|
+
const envelope = z
|
|
13
|
+
.object({ schema: z.string(), schema_version: z.number() })
|
|
14
|
+
.passthrough()
|
|
15
|
+
.safeParse(input);
|
|
16
|
+
if (envelope.success &&
|
|
17
|
+
envelope.data.schema === "JavaScriptSemanticRelationGraph" &&
|
|
18
|
+
envelope.data.schema_version !== 1)
|
|
19
|
+
throw new TypeError(`Unsupported JavaScript Semantic Relation Graph schema version: ${String(envelope.data.schema_version)}`);
|
|
20
|
+
return javaScriptSemanticGraphSchema.parse(input);
|
|
21
|
+
};
|
|
22
|
+
/** Serialize a verified semantic graph as canonical JSON. */
|
|
23
|
+
export const serializeJavaScriptSemanticGraph = (input) => canonicalJson(parseJavaScriptSemanticGraph(input));
|
|
@@ -5,6 +5,13 @@ export const DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS = {
|
|
|
5
5
|
maxCallables: 20_000,
|
|
6
6
|
maxReferences: 100_000,
|
|
7
7
|
maxModuleLinks: 20_000,
|
|
8
|
+
maxReturnSites: 20_000,
|
|
9
|
+
maxCallSites: 20_000,
|
|
10
|
+
maxCallArguments: 100_000,
|
|
11
|
+
maxArgumentFlows: 100_000,
|
|
12
|
+
maxCallReturnFlows: 100_000,
|
|
13
|
+
maxClosureCaptures: 100_000,
|
|
14
|
+
maxFrontiers: 20_000,
|
|
8
15
|
maxValueDepth: 16,
|
|
9
16
|
maxUnionValues: 32,
|
|
10
17
|
maxObjectProperties: 256,
|
|
@@ -16,12 +23,18 @@ export const semanticBinding = (ir, bindingId) => ir.bindings.find(({ bindingId:
|
|
|
16
23
|
/** Fail-closed result when Babel cannot produce an inert syntax tree. */
|
|
17
24
|
export const failedJavaScriptSemanticIr = () => ({
|
|
18
25
|
schema: "JavaScriptSemanticIR",
|
|
19
|
-
schemaVersion:
|
|
26
|
+
schemaVersion: 3,
|
|
20
27
|
scopes: [],
|
|
21
28
|
bindings: [],
|
|
22
29
|
callables: [],
|
|
23
30
|
references: [],
|
|
24
31
|
moduleLinks: [],
|
|
32
|
+
callSites: [],
|
|
33
|
+
argumentFlows: [],
|
|
34
|
+
callReturnFlows: [],
|
|
35
|
+
callResultFlows: [],
|
|
36
|
+
closureCaptures: [],
|
|
37
|
+
frontiers: [],
|
|
25
38
|
coverage: { status: "failed", omittedCount: null, limitsReached: [] },
|
|
26
39
|
limitations: [
|
|
27
40
|
"JavaScript parsing failed; no semantic absence claim is available.",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import { reachSemanticValueLimit, semanticPrimitiveKey, } from "./javascriptSemanticProvenance.js";
|
|
3
|
+
import { compareCodePoints } from "./javascriptStaticAnalysisHelpers.js";
|
|
4
|
+
/** Normalize one bounded collection of possible primitive values. */
|
|
5
|
+
export const semanticPrimitiveSet = (values, state) => {
|
|
6
|
+
const unique = [
|
|
7
|
+
...new Map(values.map((value) => [semanticPrimitiveKey(value), value])).values(),
|
|
8
|
+
].sort((left, right) => compareCodePoints(semanticPrimitiveKey(left), semanticPrimitiveKey(right)));
|
|
9
|
+
if (unique.length > state.limits.maxUnionValues) {
|
|
10
|
+
reachSemanticValueLimit(state, "maxUnionValues");
|
|
11
|
+
return {
|
|
12
|
+
status: "limit-reached",
|
|
13
|
+
reason: "maxUnionValues reached.",
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const only = unique[0];
|
|
17
|
+
return unique.length === 1 && only !== undefined
|
|
18
|
+
? { status: "literal", value: only }
|
|
19
|
+
: { status: "union", values: unique };
|
|
20
|
+
};
|
|
21
|
+
/** Read the primitive candidates retained in one lattice value. */
|
|
22
|
+
export const semanticPrimitiveCandidates = (value) => value.status === "literal"
|
|
23
|
+
? [value.value]
|
|
24
|
+
: value.status === "union"
|
|
25
|
+
? value.values
|
|
26
|
+
: null;
|
|
27
|
+
/** Parse one Babel primitive literal without evaluating code. */
|
|
28
|
+
export const semanticPrimitiveValue = (node) => {
|
|
29
|
+
if (t.isStringLiteral(node) || t.isNumericLiteral(node))
|
|
30
|
+
return { found: true, value: node.value };
|
|
31
|
+
if (t.isBooleanLiteral(node))
|
|
32
|
+
return { found: true, value: node.value };
|
|
33
|
+
if (t.isNullLiteral(node))
|
|
34
|
+
return { found: true, value: null };
|
|
35
|
+
return { found: false };
|
|
36
|
+
};
|
|
@@ -13,8 +13,9 @@ export const collectSemanticCallable = (input) => {
|
|
|
13
13
|
reachSemanticLimit(state, "maxCallables");
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
+
const callableId = semanticCallableIdForNode(node) ?? "callable:unknown:-1:-1";
|
|
16
17
|
state.callables.push({
|
|
17
|
-
callableId
|
|
18
|
+
callableId,
|
|
18
19
|
kind,
|
|
19
20
|
name: callableName(node, parent),
|
|
20
21
|
containerScopeId: containerScope.scopeId,
|
|
@@ -22,7 +23,15 @@ export const collectSemanticCallable = (input) => {
|
|
|
22
23
|
? null
|
|
23
24
|
: bodyScope.scopeId,
|
|
24
25
|
location: range(node),
|
|
26
|
+
returnSites: [],
|
|
27
|
+
returnCoverage: {
|
|
28
|
+
status: "partial",
|
|
29
|
+
retainedCount: 0,
|
|
30
|
+
omittedCount: null,
|
|
31
|
+
limitsReached: [],
|
|
32
|
+
},
|
|
25
33
|
});
|
|
34
|
+
state.callableNodesById.set(callableId, node);
|
|
26
35
|
};
|
|
27
36
|
/** Collect one static import/export relationship for later composition. */
|
|
28
37
|
export const collectSemanticModuleLink = (node, state) => {
|
|
@@ -46,6 +55,7 @@ export const collectSemanticModuleLink = (node, state) => {
|
|
|
46
55
|
importedName: null,
|
|
47
56
|
localName: defaultDeclarationName(node.declaration),
|
|
48
57
|
exportedName: "default",
|
|
58
|
+
callableId: semanticCallableIdForNode(node.declaration),
|
|
49
59
|
location: range(node),
|
|
50
60
|
});
|
|
51
61
|
else if (t.isVariableDeclarator(node))
|
|
@@ -61,7 +71,7 @@ export const collectSemanticReferences = (program, state) => {
|
|
|
61
71
|
enter: (node, parent) => {
|
|
62
72
|
if (!t.isIdentifier(node) || parent === null)
|
|
63
73
|
return;
|
|
64
|
-
const role =
|
|
74
|
+
const role = semanticReferenceRole(node, parent);
|
|
65
75
|
if (role === null)
|
|
66
76
|
return;
|
|
67
77
|
const key = `${String(node.start)}:${role}:${node.name}`;
|
|
@@ -205,6 +215,7 @@ const collectNamedExports = (node, state) => {
|
|
|
205
215
|
importedName: null,
|
|
206
216
|
localName,
|
|
207
217
|
exportedName: localName,
|
|
218
|
+
callableId: declarationCallableId(node.declaration, localName),
|
|
208
219
|
location: range(node.declaration),
|
|
209
220
|
});
|
|
210
221
|
for (const specifier of node.specifiers)
|
|
@@ -265,6 +276,7 @@ const collectCommonJsExport = (node, state) => {
|
|
|
265
276
|
importedName: origin?.importedPath.at(-1) ?? null,
|
|
266
277
|
localName: t.isIdentifier(node.right) ? node.right.name : null,
|
|
267
278
|
exportedName,
|
|
279
|
+
callableId: semanticCallableIdForNode(node.right),
|
|
268
280
|
location: range(node),
|
|
269
281
|
});
|
|
270
282
|
};
|
|
@@ -273,9 +285,29 @@ const addModuleLink = (state, link) => {
|
|
|
273
285
|
reachSemanticLimit(state, "maxModuleLinks");
|
|
274
286
|
return;
|
|
275
287
|
}
|
|
276
|
-
state.moduleLinks.push(link);
|
|
288
|
+
state.moduleLinks.push({ ...link, callableId: link.callableId ?? null });
|
|
289
|
+
};
|
|
290
|
+
/** Deterministic callable identity shared by collection and return recovery. */
|
|
291
|
+
export const semanticCallableIdForNode = (node) => {
|
|
292
|
+
const kind = callableKind(node);
|
|
293
|
+
return kind === undefined
|
|
294
|
+
? null
|
|
295
|
+
: `callable:${kind}:${String(node.start ?? -1)}:${String(node.end ?? -1)}`;
|
|
296
|
+
};
|
|
297
|
+
const declarationCallableId = (declaration, localName) => {
|
|
298
|
+
if ((t.isFunctionDeclaration(declaration) ||
|
|
299
|
+
t.isClassDeclaration(declaration)) &&
|
|
300
|
+
declaration.id?.name === localName)
|
|
301
|
+
return semanticCallableIdForNode(declaration);
|
|
302
|
+
if (!t.isVariableDeclaration(declaration))
|
|
303
|
+
return null;
|
|
304
|
+
const declarator = declaration.declarations.find(({ id }) => t.isIdentifier(id, { name: localName }));
|
|
305
|
+
return declarator?.init === null || declarator?.init === undefined
|
|
306
|
+
? null
|
|
307
|
+
: semanticCallableIdForNode(declarator.init);
|
|
277
308
|
};
|
|
278
|
-
|
|
309
|
+
/** Classify one identifier occurrence consistently with semantic references. */
|
|
310
|
+
export const semanticReferenceRole = (node, parent) => {
|
|
279
311
|
if (isDeclarationIdentifier(node, parent) || isNonReferenceKey(node, parent))
|
|
280
312
|
return null;
|
|
281
313
|
if ((t.isAssignmentExpression(parent) && parent.left === node) ||
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { compareCodePoints } from "./javascriptStaticAnalysisHelpers.js";
|
|
2
|
+
/** Construct one immutable semantic provenance value. */
|
|
3
|
+
export const semanticProvenance = (status, origins, reason) => ({ status, origins, reason });
|
|
4
|
+
/** Normalize bounded exact module origins into one provenance classification. */
|
|
5
|
+
export const semanticOriginsProvenance = (origins, state) => {
|
|
6
|
+
const unique = uniqueSemanticOrigins(origins);
|
|
7
|
+
if (unique.length > state.limits.maxUnionValues)
|
|
8
|
+
return semanticLimitProvenance(state, "maxUnionValues");
|
|
9
|
+
return unique.length === 1
|
|
10
|
+
? semanticProvenance("module", unique, null)
|
|
11
|
+
: semanticProvenance("ambiguous", unique, "Multiple module origins.");
|
|
12
|
+
};
|
|
13
|
+
/** Deduplicate and canonically order module origins. */
|
|
14
|
+
export const uniqueSemanticOrigins = (origins) => [
|
|
15
|
+
...new Map(origins.map((origin) => [semanticOriginKey(origin), origin])).values(),
|
|
16
|
+
].sort((left, right) => compareCodePoints(semanticOriginKey(left), semanticOriginKey(right)));
|
|
17
|
+
/** Mark and return an explicit provenance-limit value. */
|
|
18
|
+
export const semanticLimitProvenance = (state, limit) => {
|
|
19
|
+
reachSemanticValueLimit(state, limit);
|
|
20
|
+
return semanticProvenance("limit-reached", [], `${limit} reached.`);
|
|
21
|
+
};
|
|
22
|
+
/** Record one value-lattice limit against the shared semantic analysis state. */
|
|
23
|
+
export const reachSemanticValueLimit = (state, limit) => {
|
|
24
|
+
state.limitsReached.add(limit);
|
|
25
|
+
state.omittedCount += 1;
|
|
26
|
+
};
|
|
27
|
+
const semanticOriginKey = (origin) => `${origin.specifier}\0${origin.importedPath.join("\0")}`;
|
|
28
|
+
/** Canonically distinguish primitive values across type boundaries. */
|
|
29
|
+
export const semanticPrimitiveKey = (value) => `${value === null ? "null" : typeof value}:${JSON.stringify(value)}`;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { compareCodePoints } from "./javascriptApplicationGraph.js";
|
|
2
|
+
import { isJavaScriptSemanticOwnershipRelation as ownershipRelation } from "./javascriptSemanticQueryRelations.js";
|
|
3
|
+
import { javaScriptSemanticQueryInputSchema, javaScriptSemanticQueryResultSchema, } from "./javascriptSemanticQuerySchemas.js";
|
|
4
|
+
import { assessJavaScriptSemanticQuery, } from "./javascriptSemanticQueryAssessment.js";
|
|
5
|
+
import { canonicalJavaScriptSemanticQueryJson as canonicalJson, createJavaScriptSemanticQueryCursor as createCursor, JavaScriptSemanticQueryCursorError, javaScriptSemanticQueryIdentifier as queryIdentifier, parseJavaScriptSemanticQueryCursor as parseCursor, resolveJavaScriptSemanticQuerySeeds as resolveSeeds, } from "./javascriptSemanticQueryIdentity.js";
|
|
6
|
+
/** Run one deterministic bounded traversal over a verified semantic graph. */
|
|
7
|
+
export const queryJavaScriptSemanticGraph = (graph, rawInput) => {
|
|
8
|
+
const input = javaScriptSemanticQueryInputSchema.parse(rawInput);
|
|
9
|
+
const queryId = queryIdentifier(graph, input);
|
|
10
|
+
const offset = parseCursor(input.cursor, queryId);
|
|
11
|
+
const seeds = resolveSeeds(graph, input);
|
|
12
|
+
const admission = admitSeeds(graph, seeds, input);
|
|
13
|
+
const retainedSeeds = admission.nodeIds;
|
|
14
|
+
const adjacency = buildAdjacency(graph.relations, input);
|
|
15
|
+
const traversal = traverse(graph, retainedSeeds, adjacency, input);
|
|
16
|
+
const frontier = uniqueFrontier([
|
|
17
|
+
...traversal.frontier,
|
|
18
|
+
...admission.frontier,
|
|
19
|
+
]);
|
|
20
|
+
const page = createQueryPage({
|
|
21
|
+
graph,
|
|
22
|
+
traversal,
|
|
23
|
+
query: input,
|
|
24
|
+
queryId,
|
|
25
|
+
offset,
|
|
26
|
+
retainedSeeds,
|
|
27
|
+
});
|
|
28
|
+
const allRelevantUnknowns = relevantUnknownFrontiers(graph, traversal.nodeIds, input);
|
|
29
|
+
const relevantUnknowns = allRelevantUnknowns.slice(0, input.limits.max_unknowns);
|
|
30
|
+
const candidateRelations = relevantCandidateRelationCount(graph, traversal.nodeIds, input);
|
|
31
|
+
const expectedMatches = expectedMatchesFor(page.retainedNodes, page.retainedRelations, input);
|
|
32
|
+
const assessment = assessJavaScriptSemanticQuery({
|
|
33
|
+
graph,
|
|
34
|
+
totalSeeds: seeds.length,
|
|
35
|
+
retainedSeeds: retainedSeeds.length,
|
|
36
|
+
expectedMatches: expectedMatches.length,
|
|
37
|
+
hasExpectation: input.expected !== null,
|
|
38
|
+
frontier,
|
|
39
|
+
unknowns: relevantUnknowns,
|
|
40
|
+
candidateRelations,
|
|
41
|
+
unknownsTruncated: allRelevantUnknowns.length > relevantUnknowns.length,
|
|
42
|
+
});
|
|
43
|
+
return javaScriptSemanticQueryResultSchema.parse({
|
|
44
|
+
schema_version: 1,
|
|
45
|
+
query_id: queryId,
|
|
46
|
+
source_graph_id: graph.graph_id,
|
|
47
|
+
seed: input.seed,
|
|
48
|
+
direction: input.direction,
|
|
49
|
+
status: assessment.status,
|
|
50
|
+
seed_node_ids: retainedSeeds,
|
|
51
|
+
nodes: page.pageNodes,
|
|
52
|
+
relations: page.pageRelations,
|
|
53
|
+
unknowns: relevantUnknowns,
|
|
54
|
+
expected_match_node_ids: expectedMatches.map(({ node_id }) => node_id),
|
|
55
|
+
summary: {
|
|
56
|
+
total_seed_matches: seeds.length,
|
|
57
|
+
retained_seed_matches: retainedSeeds.length,
|
|
58
|
+
traversed_nodes: traversal.nodeIds.size,
|
|
59
|
+
traversed_relations: traversal.relationIds.size,
|
|
60
|
+
traversed_functions: traversal.functionIds.size,
|
|
61
|
+
traversed_modules: traversal.modules.size,
|
|
62
|
+
relevant_unknowns: allRelevantUnknowns.length,
|
|
63
|
+
retained_unknowns: relevantUnknowns.length,
|
|
64
|
+
},
|
|
65
|
+
coverage: assessment.coverage,
|
|
66
|
+
page: {
|
|
67
|
+
offset,
|
|
68
|
+
size: page.pageRelations.length,
|
|
69
|
+
next_cursor: page.nextCursor,
|
|
70
|
+
},
|
|
71
|
+
applied_limits: input.limits,
|
|
72
|
+
accepted_limit_ranges: assessment.acceptedLimitRanges,
|
|
73
|
+
limitations: assessment.limitations,
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
const createQueryPage = ({ graph, traversal, query, queryId, offset, retainedSeeds, }) => {
|
|
77
|
+
const retainedNodes = graph.nodes.filter(({ node_id }) => traversal.nodeIds.has(node_id));
|
|
78
|
+
const retainedRelations = graph.relations.filter(({ relation_id }) => traversal.relationIds.has(relation_id));
|
|
79
|
+
if (offset > retainedRelations.length)
|
|
80
|
+
throw new JavaScriptSemanticQueryCursorError("Semantic query cursor is beyond the retained result");
|
|
81
|
+
const pageRelations = retainedRelations.slice(offset, offset + query.limits.page_size);
|
|
82
|
+
const pageNodeIds = new Set([
|
|
83
|
+
...retainedSeeds,
|
|
84
|
+
...pageRelations.flatMap(({ source_node_id, target_node_id }) => [
|
|
85
|
+
source_node_id,
|
|
86
|
+
target_node_id,
|
|
87
|
+
]),
|
|
88
|
+
]);
|
|
89
|
+
const pageNodes = graph.nodes.filter(({ node_id }) => pageNodeIds.has(node_id));
|
|
90
|
+
const nextOffset = offset + pageRelations.length;
|
|
91
|
+
return {
|
|
92
|
+
retainedNodes,
|
|
93
|
+
retainedRelations,
|
|
94
|
+
pageRelations,
|
|
95
|
+
pageNodes,
|
|
96
|
+
nextCursor: nextOffset < retainedRelations.length
|
|
97
|
+
? createCursor(queryId, nextOffset)
|
|
98
|
+
: null,
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
const admitSeeds = (graph, seeds, input) => {
|
|
102
|
+
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
|
103
|
+
const retained = [];
|
|
104
|
+
const functions = new Set();
|
|
105
|
+
const modules = new Set();
|
|
106
|
+
const frontier = [];
|
|
107
|
+
for (const nodeId of seeds) {
|
|
108
|
+
const node = nodes.get(nodeId);
|
|
109
|
+
if (node === undefined)
|
|
110
|
+
continue;
|
|
111
|
+
const reason = seedBlockedReason(node, {
|
|
112
|
+
retained,
|
|
113
|
+
functions,
|
|
114
|
+
modules,
|
|
115
|
+
input,
|
|
116
|
+
});
|
|
117
|
+
if (reason !== null) {
|
|
118
|
+
frontier.push({ node_id: nodeId, depth: 0, reason });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
retained.push(nodeId);
|
|
122
|
+
retainOwners(node, functions, modules);
|
|
123
|
+
}
|
|
124
|
+
return { nodeIds: retained, frontier: uniqueFrontier(frontier) };
|
|
125
|
+
};
|
|
126
|
+
const seedBlockedReason = (node, bounds) => {
|
|
127
|
+
if (bounds.retained.length >= bounds.input.limits.max_seed_matches)
|
|
128
|
+
return "max-seed-matches";
|
|
129
|
+
if (bounds.retained.length >= bounds.input.limits.max_nodes)
|
|
130
|
+
return "max-nodes";
|
|
131
|
+
const functionId = functionOwner(node);
|
|
132
|
+
if (functionId !== null &&
|
|
133
|
+
!bounds.functions.has(functionId) &&
|
|
134
|
+
bounds.functions.size >= bounds.input.limits.max_functions)
|
|
135
|
+
return "max-functions";
|
|
136
|
+
if (!bounds.modules.has(node.identity.module_path) &&
|
|
137
|
+
bounds.modules.size >= bounds.input.limits.max_modules)
|
|
138
|
+
return "max-modules";
|
|
139
|
+
return null;
|
|
140
|
+
};
|
|
141
|
+
const buildAdjacency = (relations, input) => {
|
|
142
|
+
const adjacency = new Map();
|
|
143
|
+
const allowed = input.allowed_relations === undefined
|
|
144
|
+
? null
|
|
145
|
+
: new Set(input.allowed_relations);
|
|
146
|
+
for (const relation of relations) {
|
|
147
|
+
if (allowed !== null && !allowed.has(relation.relation))
|
|
148
|
+
continue;
|
|
149
|
+
if (relation.resolution === "candidate" &&
|
|
150
|
+
!input.include_ambiguous_dynamic_edges)
|
|
151
|
+
continue;
|
|
152
|
+
addDirectedEntries(adjacency, relation, input.direction);
|
|
153
|
+
}
|
|
154
|
+
for (const entries of adjacency.values())
|
|
155
|
+
entries.sort((left, right) => compareCodePoints(`${left.relation.relation_id}\0${left.nextNodeId}`, `${right.relation.relation_id}\0${right.nextNodeId}`));
|
|
156
|
+
return adjacency;
|
|
157
|
+
};
|
|
158
|
+
const addDirectedEntries = (adjacency, relation, direction) => {
|
|
159
|
+
if (direction === "callers") {
|
|
160
|
+
if (relation.relation === "calls")
|
|
161
|
+
addEntry(adjacency, relation.target_node_id, relation.source_node_id, relation);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (direction === "ownership") {
|
|
165
|
+
if (!ownershipRelation(relation.relation))
|
|
166
|
+
return;
|
|
167
|
+
addEntry(adjacency, relation.source_node_id, relation.target_node_id, relation);
|
|
168
|
+
addEntry(adjacency, relation.target_node_id, relation.source_node_id, relation);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (direction === "backward-provenance")
|
|
172
|
+
addEntry(adjacency, relation.target_node_id, relation.source_node_id, relation);
|
|
173
|
+
else
|
|
174
|
+
addEntry(adjacency, relation.source_node_id, relation.target_node_id, relation);
|
|
175
|
+
};
|
|
176
|
+
const addEntry = (adjacency, from, nextNodeId, relation) => {
|
|
177
|
+
const entries = adjacency.get(from) ?? [];
|
|
178
|
+
entries.push({ relation, nextNodeId });
|
|
179
|
+
adjacency.set(from, entries);
|
|
180
|
+
};
|
|
181
|
+
const traverse = (graph, seeds, adjacency, input) => {
|
|
182
|
+
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
|
183
|
+
const nodeIds = new Set(seeds);
|
|
184
|
+
const relationIds = new Set();
|
|
185
|
+
const functionIds = new Set();
|
|
186
|
+
const modules = new Set();
|
|
187
|
+
const frontier = [];
|
|
188
|
+
for (const nodeId of nodeIds)
|
|
189
|
+
retainOwners(nodes.get(nodeId), functionIds, modules);
|
|
190
|
+
const depths = new Map([...nodeIds].map((nodeId) => [nodeId, 0]));
|
|
191
|
+
const queue = [...nodeIds];
|
|
192
|
+
const bounds = {
|
|
193
|
+
nodes,
|
|
194
|
+
nodeIds,
|
|
195
|
+
relationIds,
|
|
196
|
+
functionIds,
|
|
197
|
+
modules,
|
|
198
|
+
input,
|
|
199
|
+
};
|
|
200
|
+
for (let offset = 0; offset < queue.length; offset += 1) {
|
|
201
|
+
const current = queue[offset];
|
|
202
|
+
if (current === undefined)
|
|
203
|
+
continue;
|
|
204
|
+
const depth = depths.get(current) ?? 0;
|
|
205
|
+
for (const entry of adjacency.get(current) ?? []) {
|
|
206
|
+
const reason = blockedReason(entry, depth, bounds);
|
|
207
|
+
if (reason !== null) {
|
|
208
|
+
frontier.push({ node_id: current, depth, reason });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
relationIds.add(entry.relation.relation_id);
|
|
212
|
+
if (nodeIds.has(entry.nextNodeId))
|
|
213
|
+
continue;
|
|
214
|
+
const node = nodes.get(entry.nextNodeId);
|
|
215
|
+
if (node === undefined)
|
|
216
|
+
continue;
|
|
217
|
+
nodeIds.add(entry.nextNodeId);
|
|
218
|
+
retainOwners(node, functionIds, modules);
|
|
219
|
+
depths.set(entry.nextNodeId, depth + 1);
|
|
220
|
+
queue.push(entry.nextNodeId);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
nodeIds,
|
|
225
|
+
relationIds,
|
|
226
|
+
functionIds,
|
|
227
|
+
modules,
|
|
228
|
+
frontier: uniqueFrontier(frontier),
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
const blockedReason = (entry, depth, bounds) => {
|
|
232
|
+
if (depth >= bounds.input.limits.max_depth)
|
|
233
|
+
return "max-depth";
|
|
234
|
+
if (!bounds.relationIds.has(entry.relation.relation_id) &&
|
|
235
|
+
bounds.relationIds.size >= bounds.input.limits.max_edges)
|
|
236
|
+
return "max-edges";
|
|
237
|
+
if (bounds.nodeIds.has(entry.nextNodeId))
|
|
238
|
+
return null;
|
|
239
|
+
if (bounds.nodeIds.size >= bounds.input.limits.max_nodes)
|
|
240
|
+
return "max-nodes";
|
|
241
|
+
const node = bounds.nodes.get(entry.nextNodeId);
|
|
242
|
+
if (node === undefined)
|
|
243
|
+
return null;
|
|
244
|
+
const functionId = functionOwner(node);
|
|
245
|
+
if (functionId !== null &&
|
|
246
|
+
!bounds.functionIds.has(functionId) &&
|
|
247
|
+
bounds.functionIds.size >= bounds.input.limits.max_functions)
|
|
248
|
+
return "max-functions";
|
|
249
|
+
if (!bounds.modules.has(node.identity.module_path) &&
|
|
250
|
+
bounds.modules.size >= bounds.input.limits.max_modules)
|
|
251
|
+
return "max-modules";
|
|
252
|
+
return null;
|
|
253
|
+
};
|
|
254
|
+
const retainOwners = (node, functions, modules) => {
|
|
255
|
+
if (node === undefined)
|
|
256
|
+
return;
|
|
257
|
+
const functionId = functionOwner(node);
|
|
258
|
+
if (functionId !== null)
|
|
259
|
+
functions.add(functionId);
|
|
260
|
+
modules.add(node.identity.module_path);
|
|
261
|
+
};
|
|
262
|
+
const functionOwner = (node) => node.kind === "function" ? node.node_id : node.function_node_id;
|
|
263
|
+
const uniqueFrontier = (frontier) => [
|
|
264
|
+
...new Map(frontier.map((item) => [
|
|
265
|
+
`${item.node_id}\0${String(item.depth)}\0${item.reason}`,
|
|
266
|
+
item,
|
|
267
|
+
])).values(),
|
|
268
|
+
].sort((left, right) => compareCodePoints(canonicalJson(left), canonicalJson(right)));
|
|
269
|
+
const relevantUnknownFrontiers = (graph, nodeIds, input) => {
|
|
270
|
+
const allowed = input.allowed_relations === undefined
|
|
271
|
+
? null
|
|
272
|
+
: new Set(input.allowed_relations);
|
|
273
|
+
return graph.unknowns.filter((unknown) => (unknown.node_id === null || nodeIds.has(unknown.node_id)) &&
|
|
274
|
+
(allowed === null ||
|
|
275
|
+
unknown.relation_kinds.some((relation) => allowed.has(relation))));
|
|
276
|
+
};
|
|
277
|
+
const relevantCandidateRelationCount = (graph, nodeIds, input) => {
|
|
278
|
+
const allowed = input.allowed_relations === undefined
|
|
279
|
+
? null
|
|
280
|
+
: new Set(input.allowed_relations);
|
|
281
|
+
return graph.relations.filter((relation) => relation.resolution === "candidate" &&
|
|
282
|
+
(allowed === null || allowed.has(relation.relation)) &&
|
|
283
|
+
(nodeIds.has(relation.source_node_id) ||
|
|
284
|
+
nodeIds.has(relation.target_node_id))).length;
|
|
285
|
+
};
|
|
286
|
+
const expectedMatchesFor = (nodes, relations, input) => {
|
|
287
|
+
if (input.expected === null)
|
|
288
|
+
return [];
|
|
289
|
+
const classes = new Set(input.expected.classes);
|
|
290
|
+
const connected = new Set(relations.map((relation) => input.expected?.role === "source"
|
|
291
|
+
? relation.target_node_id
|
|
292
|
+
: relation.source_node_id));
|
|
293
|
+
return nodes.filter(({ kind, node_id }) => classes.has(kind) && !connected.has(node_id));
|
|
294
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { compareCodePoints } from "./javascriptApplicationGraph.js";
|
|
2
|
+
/** Classify a bounded semantic traversal without promoting unknowns to absence. */
|
|
3
|
+
export const assessJavaScriptSemanticQuery = (input) => ({
|
|
4
|
+
status: queryStatus(input),
|
|
5
|
+
coverage: queryCoverage(input),
|
|
6
|
+
limitations: queryLimitations(input),
|
|
7
|
+
acceptedLimitRanges: {
|
|
8
|
+
max_seed_matches: { minimum: 1, maximum: 1_000 },
|
|
9
|
+
max_nodes: { minimum: 1, maximum: 50_000 },
|
|
10
|
+
max_edges: { minimum: 1, maximum: 100_000 },
|
|
11
|
+
max_depth: { minimum: 0, maximum: 64 },
|
|
12
|
+
max_functions: { minimum: 1, maximum: 10_000 },
|
|
13
|
+
max_modules: { minimum: 1, maximum: 10_000 },
|
|
14
|
+
max_unknowns: { minimum: 0, maximum: 10_000 },
|
|
15
|
+
page_size: { minimum: 1, maximum: 1_000 },
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
const queryCoverage = (input) => ({
|
|
19
|
+
status: input.graph.coverage.status === "unavailable"
|
|
20
|
+
? "unavailable"
|
|
21
|
+
: input.frontier.length > 0 || input.unknownsTruncated
|
|
22
|
+
? "truncated"
|
|
23
|
+
: input.unknowns.length > 0 ||
|
|
24
|
+
input.candidateRelations > 0 ||
|
|
25
|
+
input.graph.coverage.status !== "complete"
|
|
26
|
+
? "partial"
|
|
27
|
+
: "complete",
|
|
28
|
+
frontier: [...input.frontier],
|
|
29
|
+
});
|
|
30
|
+
const queryStatus = (input) => {
|
|
31
|
+
if (input.graph.coverage.status === "unavailable")
|
|
32
|
+
return "unsupported";
|
|
33
|
+
if (input.frontier.length > 0 || input.unknownsTruncated)
|
|
34
|
+
return "truncated";
|
|
35
|
+
if (input.candidateRelations > 0)
|
|
36
|
+
return "ambiguous";
|
|
37
|
+
if (input.totalSeeds === 0 ||
|
|
38
|
+
(input.hasExpectation && input.expectedMatches === 0))
|
|
39
|
+
return input.unknowns.length > 0 ||
|
|
40
|
+
input.graph.coverage.status !== "complete"
|
|
41
|
+
? "partial"
|
|
42
|
+
: "no-match";
|
|
43
|
+
return input.totalSeeds > 1 ? "ambiguous" : "found";
|
|
44
|
+
};
|
|
45
|
+
const queryLimitations = (input) => uniqueSorted([
|
|
46
|
+
"Semantic graph reachability is a static inference and does not prove runtime execution.",
|
|
47
|
+
...input.graph.limitations,
|
|
48
|
+
...(input.totalSeeds === input.retainedSeeds
|
|
49
|
+
? []
|
|
50
|
+
: [
|
|
51
|
+
"Seed matches exceeded caller bounds; omitted starts remain unexplored.",
|
|
52
|
+
]),
|
|
53
|
+
...(input.frontier.length === 0
|
|
54
|
+
? []
|
|
55
|
+
: [
|
|
56
|
+
"Traversal stopped at explicit caller limits; frontier facts remain unknown.",
|
|
57
|
+
]),
|
|
58
|
+
...(input.unknowns.length === 0
|
|
59
|
+
? []
|
|
60
|
+
: [
|
|
61
|
+
"Relevant dynamic, unsupported, incomplete, or ambiguous semantics remain unknown.",
|
|
62
|
+
]),
|
|
63
|
+
...(input.unknownsTruncated
|
|
64
|
+
? [
|
|
65
|
+
"Relevant unknown frontiers exceeded the caller limit; omitted unknowns remain unresolved.",
|
|
66
|
+
]
|
|
67
|
+
: []),
|
|
68
|
+
...(input.candidateRelations === 0
|
|
69
|
+
? []
|
|
70
|
+
: [
|
|
71
|
+
"Candidate relations remain ambiguous and do not establish a resolved semantic path.",
|
|
72
|
+
]),
|
|
73
|
+
]);
|
|
74
|
+
const uniqueSorted = (values) => [...new Set(values)].sort(compareCodePoints);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import canonicalize from "canonicalize";
|
|
3
|
+
import { compareCodePoints } from "./javascriptApplicationGraph.js";
|
|
4
|
+
/** Caller cursor that cannot identify a page of the current semantic query. */
|
|
5
|
+
export class JavaScriptSemanticQueryCursorError extends TypeError {
|
|
6
|
+
}
|
|
7
|
+
/** Canonical JSON used by semantic query commitments and ordering. */
|
|
8
|
+
export const canonicalJavaScriptSemanticQueryJson = (value) => {
|
|
9
|
+
const encoded = canonicalize(value);
|
|
10
|
+
if (encoded === undefined)
|
|
11
|
+
throw new TypeError("JavaScript semantic query could not canonicalize data");
|
|
12
|
+
return encoded;
|
|
13
|
+
};
|
|
14
|
+
const digest = (value) => createHash("sha256")
|
|
15
|
+
.update(canonicalJavaScriptSemanticQueryJson(value))
|
|
16
|
+
.digest("hex");
|
|
17
|
+
const uniqueSorted = (values) => [...new Set(values)].sort(compareCodePoints);
|
|
18
|
+
/** Derive the stable identity for one graph-bound query without its cursor. */
|
|
19
|
+
export const javaScriptSemanticQueryIdentifier = (graph, input) => {
|
|
20
|
+
const { cursor: _cursor, ...semantic } = input;
|
|
21
|
+
return `jsrq_${digest({
|
|
22
|
+
source_graph_id: graph.graph_id,
|
|
23
|
+
...semantic,
|
|
24
|
+
allowed_relations: semantic.allowed_relations === undefined
|
|
25
|
+
? undefined
|
|
26
|
+
: uniqueSorted(semantic.allowed_relations),
|
|
27
|
+
expected: semantic.expected === null
|
|
28
|
+
? null
|
|
29
|
+
: {
|
|
30
|
+
...semantic.expected,
|
|
31
|
+
classes: uniqueSorted(semantic.expected.classes),
|
|
32
|
+
},
|
|
33
|
+
})}`;
|
|
34
|
+
};
|
|
35
|
+
/** Commit one deterministic relation-page offset to a query identity. */
|
|
36
|
+
export const createJavaScriptSemanticQueryCursor = (queryId, offset) => `jsrqc_${String(offset)}_${digest({ query_id: queryId, offset })}`;
|
|
37
|
+
/** Parse a query-bound cursor and reject stale, malformed, or foreign values. */
|
|
38
|
+
export const parseJavaScriptSemanticQueryCursor = (cursor, queryId) => {
|
|
39
|
+
if (cursor === null)
|
|
40
|
+
return 0;
|
|
41
|
+
const match = /^jsrqc_([0-9]+)_([a-f0-9]{64})$/u.exec(cursor);
|
|
42
|
+
const offsetText = match?.[1];
|
|
43
|
+
const commitment = match?.[2];
|
|
44
|
+
if (offsetText === undefined || commitment === undefined)
|
|
45
|
+
throw new JavaScriptSemanticQueryCursorError("Semantic query cursor is malformed");
|
|
46
|
+
const offset = Number(offsetText);
|
|
47
|
+
if (!Number.isSafeInteger(offset) ||
|
|
48
|
+
offset < 0 ||
|
|
49
|
+
commitment !== digest({ query_id: queryId, offset }))
|
|
50
|
+
throw new JavaScriptSemanticQueryCursorError("Semantic query cursor does not match this query");
|
|
51
|
+
return offset;
|
|
52
|
+
};
|
|
53
|
+
/** Resolve deterministic seed candidates without applying traversal limits. */
|
|
54
|
+
export const resolveJavaScriptSemanticQuerySeeds = (graph, input) => {
|
|
55
|
+
const fingerprintFunctions = new Set(input.seed.kind === "function"
|
|
56
|
+
? graph.fingerprints
|
|
57
|
+
.filter(({ fingerprint_sha256 }) => input.seed.kind === "function" &&
|
|
58
|
+
fingerprint_sha256 === input.seed.fingerprint_sha256)
|
|
59
|
+
.map(({ function_node_id }) => function_node_id)
|
|
60
|
+
: []);
|
|
61
|
+
return graph.nodes
|
|
62
|
+
.filter((node) => seedMatches(node, input, fingerprintFunctions))
|
|
63
|
+
.map(({ node_id }) => node_id)
|
|
64
|
+
.sort(compareCodePoints);
|
|
65
|
+
};
|
|
66
|
+
const seedMatches = (node, input, fingerprintFunctions) => {
|
|
67
|
+
const seed = input.seed;
|
|
68
|
+
if (seed.kind === "semantic-node")
|
|
69
|
+
return node.node_id === seed.node_id;
|
|
70
|
+
if (seed.kind === "application-node")
|
|
71
|
+
return node.application_node_ids.includes(seed.node_id);
|
|
72
|
+
if (seed.kind === "function")
|
|
73
|
+
return fingerprintFunctions.has(node.node_id);
|
|
74
|
+
if (seed.kind === "literal")
|
|
75
|
+
return (node.kind === "literal" &&
|
|
76
|
+
canonicalJavaScriptSemanticQueryJson(node.properties.value) ===
|
|
77
|
+
canonicalJavaScriptSemanticQueryJson(seed.value));
|
|
78
|
+
if (seed.kind === "property")
|
|
79
|
+
return node.kind === "property-slot" && node.properties.name === seed.name;
|
|
80
|
+
if (seed.kind === "endpoint")
|
|
81
|
+
return node.kind === "request" && node.properties.endpoint === seed.value;
|
|
82
|
+
if (seed.kind === "event")
|
|
83
|
+
return (["event", "listener"].includes(node.kind) &&
|
|
84
|
+
node.properties.event_name === seed.name);
|
|
85
|
+
return node.kind === "boundary" && node.properties.field === seed.field;
|
|
86
|
+
};
|