@grafema/util 0.3.0-beta
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/LICENSE +190 -0
- package/dist/api/GraphAPI.d.ts +87 -0
- package/dist/api/GraphAPI.d.ts.map +1 -0
- package/dist/api/GraphAPI.js +212 -0
- package/dist/api/GraphAPI.js.map +1 -0
- package/dist/api/GuaranteeAPI.d.ts +147 -0
- package/dist/api/GuaranteeAPI.d.ts.map +1 -0
- package/dist/api/GuaranteeAPI.js +290 -0
- package/dist/api/GuaranteeAPI.js.map +1 -0
- package/dist/config/ConfigLoader.d.ts +214 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +441 -0
- package/dist/config/ConfigLoader.js.map +1 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +5 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/CoverageAnalyzer.d.ts +65 -0
- package/dist/core/CoverageAnalyzer.d.ts.map +1 -0
- package/dist/core/CoverageAnalyzer.js +199 -0
- package/dist/core/CoverageAnalyzer.js.map +1 -0
- package/dist/core/FileExplainer.d.ts +101 -0
- package/dist/core/FileExplainer.d.ts.map +1 -0
- package/dist/core/FileExplainer.js +140 -0
- package/dist/core/FileExplainer.js.map +1 -0
- package/dist/core/FileOverview.d.ts +124 -0
- package/dist/core/FileOverview.d.ts.map +1 -0
- package/dist/core/FileOverview.js +279 -0
- package/dist/core/FileOverview.js.map +1 -0
- package/dist/core/GrafemaUri.d.ts +66 -0
- package/dist/core/GrafemaUri.d.ts.map +1 -0
- package/dist/core/GrafemaUri.js +191 -0
- package/dist/core/GrafemaUri.js.map +1 -0
- package/dist/core/GraphBackend.d.ts +158 -0
- package/dist/core/GraphBackend.d.ts.map +1 -0
- package/dist/core/GraphBackend.js +85 -0
- package/dist/core/GraphBackend.js.map +1 -0
- package/dist/core/GraphFreshnessChecker.d.ts +33 -0
- package/dist/core/GraphFreshnessChecker.d.ts.map +1 -0
- package/dist/core/GraphFreshnessChecker.js +104 -0
- package/dist/core/GraphFreshnessChecker.js.map +1 -0
- package/dist/core/GuaranteeManager.d.ts +254 -0
- package/dist/core/GuaranteeManager.d.ts.map +1 -0
- package/dist/core/GuaranteeManager.js +447 -0
- package/dist/core/GuaranteeManager.js.map +1 -0
- package/dist/core/HashUtils.d.ts +24 -0
- package/dist/core/HashUtils.d.ts.map +1 -0
- package/dist/core/HashUtils.js +46 -0
- package/dist/core/HashUtils.js.map +1 -0
- package/dist/core/IncrementalReanalyzer.d.ts +33 -0
- package/dist/core/IncrementalReanalyzer.d.ts.map +1 -0
- package/dist/core/IncrementalReanalyzer.js +67 -0
- package/dist/core/IncrementalReanalyzer.js.map +1 -0
- package/dist/core/ResourceRegistry.d.ts +17 -0
- package/dist/core/ResourceRegistry.d.ts.map +1 -0
- package/dist/core/ResourceRegistry.js +32 -0
- package/dist/core/ResourceRegistry.js.map +1 -0
- package/dist/core/SemanticId.d.ts +159 -0
- package/dist/core/SemanticId.d.ts.map +1 -0
- package/dist/core/SemanticId.js +291 -0
- package/dist/core/SemanticId.js.map +1 -0
- package/dist/core/VersionManager.d.ts +166 -0
- package/dist/core/VersionManager.d.ts.map +1 -0
- package/dist/core/VersionManager.js +239 -0
- package/dist/core/VersionManager.js.map +1 -0
- package/dist/core/brandNodeInternal.d.ts +14 -0
- package/dist/core/brandNodeInternal.d.ts.map +1 -0
- package/dist/core/brandNodeInternal.js +4 -0
- package/dist/core/brandNodeInternal.js.map +1 -0
- package/dist/core/nodes/GuaranteeNode.d.ts +76 -0
- package/dist/core/nodes/GuaranteeNode.d.ts.map +1 -0
- package/dist/core/nodes/GuaranteeNode.js +118 -0
- package/dist/core/nodes/GuaranteeNode.js.map +1 -0
- package/dist/core/nodes/IssueNode.d.ts +73 -0
- package/dist/core/nodes/IssueNode.d.ts.map +1 -0
- package/dist/core/nodes/IssueNode.js +130 -0
- package/dist/core/nodes/IssueNode.js.map +1 -0
- package/dist/core/nodes/NodeKind.d.ts +104 -0
- package/dist/core/nodes/NodeKind.d.ts.map +1 -0
- package/dist/core/nodes/NodeKind.js +166 -0
- package/dist/core/nodes/NodeKind.js.map +1 -0
- package/dist/diagnostics/DiagnosticCollector.d.ts +103 -0
- package/dist/diagnostics/DiagnosticCollector.d.ts.map +1 -0
- package/dist/diagnostics/DiagnosticCollector.js +133 -0
- package/dist/diagnostics/DiagnosticCollector.js.map +1 -0
- package/dist/diagnostics/DiagnosticReporter.d.ts +122 -0
- package/dist/diagnostics/DiagnosticReporter.d.ts.map +1 -0
- package/dist/diagnostics/DiagnosticReporter.js +300 -0
- package/dist/diagnostics/DiagnosticReporter.js.map +1 -0
- package/dist/diagnostics/DiagnosticWriter.d.ts +31 -0
- package/dist/diagnostics/DiagnosticWriter.d.ts.map +1 -0
- package/dist/diagnostics/DiagnosticWriter.js +44 -0
- package/dist/diagnostics/DiagnosticWriter.js.map +1 -0
- package/dist/diagnostics/categories.d.ts +57 -0
- package/dist/diagnostics/categories.d.ts.map +1 -0
- package/dist/diagnostics/categories.js +71 -0
- package/dist/diagnostics/categories.js.map +1 -0
- package/dist/diagnostics/index.d.ts +17 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +15 -0
- package/dist/diagnostics/index.js.map +1 -0
- package/dist/errors/GrafemaError.d.ts +200 -0
- package/dist/errors/GrafemaError.d.ts.map +1 -0
- package/dist/errors/GrafemaError.js +209 -0
- package/dist/errors/GrafemaError.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/instructions/index.d.ts +8 -0
- package/dist/instructions/index.d.ts.map +1 -0
- package/dist/instructions/index.js +20 -0
- package/dist/instructions/index.js.map +1 -0
- package/dist/instructions/onboarding.md +133 -0
- package/dist/knowledge/KnowledgeBase.d.ts +113 -0
- package/dist/knowledge/KnowledgeBase.d.ts.map +1 -0
- package/dist/knowledge/KnowledgeBase.js +420 -0
- package/dist/knowledge/KnowledgeBase.js.map +1 -0
- package/dist/knowledge/SemanticAddressResolver.d.ts +59 -0
- package/dist/knowledge/SemanticAddressResolver.d.ts.map +1 -0
- package/dist/knowledge/SemanticAddressResolver.js +160 -0
- package/dist/knowledge/SemanticAddressResolver.js.map +1 -0
- package/dist/knowledge/git-ingest.d.ts +58 -0
- package/dist/knowledge/git-ingest.d.ts.map +1 -0
- package/dist/knowledge/git-ingest.js +301 -0
- package/dist/knowledge/git-ingest.js.map +1 -0
- package/dist/knowledge/git-queries.d.ts +86 -0
- package/dist/knowledge/git-queries.d.ts.map +1 -0
- package/dist/knowledge/git-queries.js +177 -0
- package/dist/knowledge/git-queries.js.map +1 -0
- package/dist/knowledge/index.d.ts +14 -0
- package/dist/knowledge/index.d.ts.map +1 -0
- package/dist/knowledge/index.js +10 -0
- package/dist/knowledge/index.js.map +1 -0
- package/dist/knowledge/parser.d.ts +39 -0
- package/dist/knowledge/parser.d.ts.map +1 -0
- package/dist/knowledge/parser.js +292 -0
- package/dist/knowledge/parser.js.map +1 -0
- package/dist/knowledge/types.d.ts +133 -0
- package/dist/knowledge/types.d.ts.map +1 -0
- package/dist/knowledge/types.js +8 -0
- package/dist/knowledge/types.js.map +1 -0
- package/dist/logging/Logger.d.ts +98 -0
- package/dist/logging/Logger.d.ts.map +1 -0
- package/dist/logging/Logger.js +274 -0
- package/dist/logging/Logger.js.map +1 -0
- package/dist/notation/archetypes.d.ts +36 -0
- package/dist/notation/archetypes.d.ts.map +1 -0
- package/dist/notation/archetypes.js +173 -0
- package/dist/notation/archetypes.js.map +1 -0
- package/dist/notation/fold.d.ts +25 -0
- package/dist/notation/fold.d.ts.map +1 -0
- package/dist/notation/fold.js +598 -0
- package/dist/notation/fold.js.map +1 -0
- package/dist/notation/index.d.ts +18 -0
- package/dist/notation/index.d.ts.map +1 -0
- package/dist/notation/index.js +16 -0
- package/dist/notation/index.js.map +1 -0
- package/dist/notation/lodExtractor.d.ts +32 -0
- package/dist/notation/lodExtractor.d.ts.map +1 -0
- package/dist/notation/lodExtractor.js +149 -0
- package/dist/notation/lodExtractor.js.map +1 -0
- package/dist/notation/nameShortener.d.ts +22 -0
- package/dist/notation/nameShortener.d.ts.map +1 -0
- package/dist/notation/nameShortener.js +24 -0
- package/dist/notation/nameShortener.js.map +1 -0
- package/dist/notation/perspectives.d.ts +11 -0
- package/dist/notation/perspectives.d.ts.map +1 -0
- package/dist/notation/perspectives.js +16 -0
- package/dist/notation/perspectives.js.map +1 -0
- package/dist/notation/renderer.d.ts +31 -0
- package/dist/notation/renderer.d.ts.map +1 -0
- package/dist/notation/renderer.js +315 -0
- package/dist/notation/renderer.js.map +1 -0
- package/dist/notation/traceRenderer.d.ts +39 -0
- package/dist/notation/traceRenderer.d.ts.map +1 -0
- package/dist/notation/traceRenderer.js +358 -0
- package/dist/notation/traceRenderer.js.map +1 -0
- package/dist/notation/types.d.ts +66 -0
- package/dist/notation/types.d.ts.map +1 -0
- package/dist/notation/types.js +10 -0
- package/dist/notation/types.js.map +1 -0
- package/dist/queries/NodeContext.d.ts +81 -0
- package/dist/queries/NodeContext.d.ts.map +1 -0
- package/dist/queries/NodeContext.js +196 -0
- package/dist/queries/NodeContext.js.map +1 -0
- package/dist/queries/findCallsInFunction.d.ts +62 -0
- package/dist/queries/findCallsInFunction.d.ts.map +1 -0
- package/dist/queries/findCallsInFunction.js +169 -0
- package/dist/queries/findCallsInFunction.js.map +1 -0
- package/dist/queries/findContainingFunction.d.ts +57 -0
- package/dist/queries/findContainingFunction.d.ts.map +1 -0
- package/dist/queries/findContainingFunction.js +91 -0
- package/dist/queries/findContainingFunction.js.map +1 -0
- package/dist/queries/index.d.ts +18 -0
- package/dist/queries/index.d.ts.map +1 -0
- package/dist/queries/index.js +14 -0
- package/dist/queries/index.js.map +1 -0
- package/dist/queries/traceDataflow.d.ts +65 -0
- package/dist/queries/traceDataflow.d.ts.map +1 -0
- package/dist/queries/traceDataflow.js +754 -0
- package/dist/queries/traceDataflow.js.map +1 -0
- package/dist/queries/traceValues.d.ts +70 -0
- package/dist/queries/traceValues.d.ts.map +1 -0
- package/dist/queries/traceValues.js +373 -0
- package/dist/queries/traceValues.js.map +1 -0
- package/dist/queries/types.d.ts +166 -0
- package/dist/queries/types.d.ts.map +1 -0
- package/dist/queries/types.js +10 -0
- package/dist/queries/types.js.map +1 -0
- package/dist/schema/GraphSchemaExtractor.d.ts +53 -0
- package/dist/schema/GraphSchemaExtractor.d.ts.map +1 -0
- package/dist/schema/GraphSchemaExtractor.js +125 -0
- package/dist/schema/GraphSchemaExtractor.js.map +1 -0
- package/dist/schema/InterfaceSchemaExtractor.d.ts +73 -0
- package/dist/schema/InterfaceSchemaExtractor.d.ts.map +1 -0
- package/dist/schema/InterfaceSchemaExtractor.js +113 -0
- package/dist/schema/InterfaceSchemaExtractor.js.map +1 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +3 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/storage/backends/RFDBServerBackend.d.ts +356 -0
- package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -0
- package/dist/storage/backends/RFDBServerBackend.js +748 -0
- package/dist/storage/backends/RFDBServerBackend.js.map +1 -0
- package/dist/storage/backends/typeValidation.d.ts +47 -0
- package/dist/storage/backends/typeValidation.d.ts.map +1 -0
- package/dist/storage/backends/typeValidation.js +141 -0
- package/dist/storage/backends/typeValidation.js.map +1 -0
- package/dist/utils/findRfdbBinary.d.ts +67 -0
- package/dist/utils/findRfdbBinary.d.ts.map +1 -0
- package/dist/utils/findRfdbBinary.js +261 -0
- package/dist/utils/findRfdbBinary.js.map +1 -0
- package/dist/utils/lazyDownload.d.ts +43 -0
- package/dist/utils/lazyDownload.d.ts.map +1 -0
- package/dist/utils/lazyDownload.js +175 -0
- package/dist/utils/lazyDownload.js.map +1 -0
- package/dist/utils/moduleResolution.d.ts +134 -0
- package/dist/utils/moduleResolution.d.ts.map +1 -0
- package/dist/utils/moduleResolution.js +189 -0
- package/dist/utils/moduleResolution.js.map +1 -0
- package/dist/utils/resolveNodeFile.d.ts +13 -0
- package/dist/utils/resolveNodeFile.d.ts.map +1 -0
- package/dist/utils/resolveNodeFile.js +18 -0
- package/dist/utils/resolveNodeFile.js.map +1 -0
- package/dist/utils/startRfdbServer.d.ts +63 -0
- package/dist/utils/startRfdbServer.d.ts.map +1 -0
- package/dist/utils/startRfdbServer.js +142 -0
- package/dist/utils/startRfdbServer.js.map +1 -0
- package/dist/validation/PathValidator.d.ts +80 -0
- package/dist/validation/PathValidator.d.ts.map +1 -0
- package/dist/validation/PathValidator.js +252 -0
- package/dist/validation/PathValidator.js.map +1 -0
- package/dist/version.d.ts +11 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +26 -0
- package/dist/version.js.map +1 -0
- package/package.json +50 -0
- package/src/api/GraphAPI.ts +307 -0
- package/src/api/GuaranteeAPI.ts +402 -0
- package/src/config/ConfigLoader.ts +653 -0
- package/src/config/index.ts +13 -0
- package/src/core/CoverageAnalyzer.ts +243 -0
- package/src/core/FileExplainer.ts +179 -0
- package/src/core/FileOverview.ts +397 -0
- package/src/core/GrafemaUri.ts +216 -0
- package/src/core/GraphBackend.ts +266 -0
- package/src/core/GraphFreshnessChecker.ts +145 -0
- package/src/core/GuaranteeManager.ts +684 -0
- package/src/core/HashUtils.ts +48 -0
- package/src/core/IncrementalReanalyzer.ts +106 -0
- package/src/core/ResourceRegistry.ts +39 -0
- package/src/core/SemanticId.ts +423 -0
- package/src/core/VersionManager.ts +405 -0
- package/src/core/brandNodeInternal.ts +16 -0
- package/src/core/nodes/GuaranteeNode.ts +162 -0
- package/src/core/nodes/IssueNode.ts +177 -0
- package/src/core/nodes/NodeKind.ts +192 -0
- package/src/diagnostics/DiagnosticCollector.ts +170 -0
- package/src/diagnostics/DiagnosticReporter.ts +395 -0
- package/src/diagnostics/DiagnosticWriter.ts +50 -0
- package/src/diagnostics/categories.ts +104 -0
- package/src/diagnostics/index.ts +30 -0
- package/src/errors/GrafemaError.ts +297 -0
- package/src/index.ts +261 -0
- package/src/instructions/index.ts +21 -0
- package/src/instructions/onboarding.md +133 -0
- package/src/knowledge/KnowledgeBase.ts +486 -0
- package/src/knowledge/SemanticAddressResolver.ts +191 -0
- package/src/knowledge/git-ingest.ts +402 -0
- package/src/knowledge/git-queries.ts +269 -0
- package/src/knowledge/index.ts +29 -0
- package/src/knowledge/parser.ts +294 -0
- package/src/knowledge/types.ts +146 -0
- package/src/logging/Logger.ts +303 -0
- package/src/notation/archetypes.ts +189 -0
- package/src/notation/fold.ts +696 -0
- package/src/notation/index.ts +27 -0
- package/src/notation/lodExtractor.ts +177 -0
- package/src/notation/nameShortener.ts +24 -0
- package/src/notation/perspectives.ts +18 -0
- package/src/notation/renderer.ts +394 -0
- package/src/notation/traceRenderer.ts +458 -0
- package/src/notation/types.ts +79 -0
- package/src/queries/NodeContext.ts +280 -0
- package/src/queries/findCallsInFunction.ts +249 -0
- package/src/queries/findContainingFunction.ts +124 -0
- package/src/queries/index.ts +44 -0
- package/src/queries/traceDataflow.ts +838 -0
- package/src/queries/traceValues.ts +531 -0
- package/src/queries/types.ts +191 -0
- package/src/schema/GraphSchemaExtractor.ts +177 -0
- package/src/schema/InterfaceSchemaExtractor.ts +173 -0
- package/src/schema/index.ts +5 -0
- package/src/storage/backends/RFDBServerBackend.ts +895 -0
- package/src/storage/backends/typeValidation.ts +154 -0
- package/src/utils/findRfdbBinary.ts +288 -0
- package/src/utils/lazyDownload.ts +206 -0
- package/src/utils/moduleResolution.ts +271 -0
- package/src/utils/resolveNodeFile.ts +18 -0
- package/src/utils/startRfdbServer.ts +197 -0
- package/src/validation/PathValidator.ts +334 -0
- package/src/version.ts +28 -0
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFDBServerBackend - Graph backend using RFDB server via Unix socket
|
|
3
|
+
*
|
|
4
|
+
* Replaces ReginaFlowBackend's direct NAPI binding with socket-based
|
|
5
|
+
* communication to a shared RFDB server. This allows multiple processes
|
|
6
|
+
* (MCP server, analysis workers) to share the same graph database.
|
|
7
|
+
*
|
|
8
|
+
* Socket path defaults to `{dbPath}/../rfdb.sock` (e.g., .grafema/rfdb.sock),
|
|
9
|
+
* ensuring each project has its own socket and avoiding conflicts when
|
|
10
|
+
* multiple MCP instances run simultaneously.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const backend = new RFDBServerBackend({
|
|
14
|
+
* dbPath: '/project/.grafema/graph.rfdb' // socket will be /project/.grafema/rfdb.sock
|
|
15
|
+
* });
|
|
16
|
+
* await backend.connect();
|
|
17
|
+
* await backend.addNodes([...]);
|
|
18
|
+
* await backend.flush();
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { RFDBClient, type BatchHandle } from '@grafema/rfdb-client';
|
|
22
|
+
import type { ChildProcess } from 'child_process';
|
|
23
|
+
import { join, dirname } from 'path';
|
|
24
|
+
|
|
25
|
+
import type { WireNode, WireEdge, FieldDeclaration, CommitDelta, AttrQuery as RFDBAttrQuery, DatalogExplainResult, ServerStats, CypherResult } from '@grafema/types';
|
|
26
|
+
import type { NodeType, EdgeType } from '@grafema/types';
|
|
27
|
+
import { startRfdbServer } from '../../utils/startRfdbServer.js';
|
|
28
|
+
import { GRAFEMA_VERSION, getSchemaVersion } from '../../version.js';
|
|
29
|
+
import type { BaseNodeRecord, EdgeRecord, AnyBrandedNode } from '@grafema/types';
|
|
30
|
+
import { brandNodeInternal } from '../../core/brandNodeInternal.js';
|
|
31
|
+
import type { AttrQuery, GraphStats, GraphExport } from '../../core/GraphBackend.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Options for RFDBServerBackend
|
|
35
|
+
*/
|
|
36
|
+
export interface RFDBServerBackendOptions {
|
|
37
|
+
socketPath?: string;
|
|
38
|
+
dbPath?: string;
|
|
39
|
+
/**
|
|
40
|
+
* If true, automatically start the server if not running.
|
|
41
|
+
* If false, require explicit `grafema server start`.
|
|
42
|
+
* Default: true (for backwards compatibility)
|
|
43
|
+
*/
|
|
44
|
+
autoStart?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* If true, suppress all console output (for clean CLI progress).
|
|
47
|
+
* Default: false
|
|
48
|
+
*/
|
|
49
|
+
silent?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Name identifying this client in server logs (e.g. 'cli', 'mcp', 'core').
|
|
52
|
+
* Default: 'core'
|
|
53
|
+
*/
|
|
54
|
+
clientName?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Input node format (flexible)
|
|
59
|
+
*/
|
|
60
|
+
export interface InputNode {
|
|
61
|
+
id: string;
|
|
62
|
+
type?: string;
|
|
63
|
+
nodeType?: string;
|
|
64
|
+
node_type?: string;
|
|
65
|
+
name?: string;
|
|
66
|
+
file?: string;
|
|
67
|
+
exported?: boolean;
|
|
68
|
+
[key: string]: unknown;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Input edge format (flexible)
|
|
73
|
+
*/
|
|
74
|
+
export interface InputEdge {
|
|
75
|
+
src: string;
|
|
76
|
+
dst: string;
|
|
77
|
+
type?: string;
|
|
78
|
+
edgeType?: string;
|
|
79
|
+
edge_type?: string;
|
|
80
|
+
[key: string]: unknown;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Query for finding nodes
|
|
85
|
+
*/
|
|
86
|
+
export interface NodeQuery {
|
|
87
|
+
nodeType?: NodeType;
|
|
88
|
+
type?: NodeType;
|
|
89
|
+
name?: string;
|
|
90
|
+
file?: string;
|
|
91
|
+
substringMatch?: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Backend statistics
|
|
96
|
+
*/
|
|
97
|
+
export interface BackendStats extends GraphStats {
|
|
98
|
+
nodesByType: Record<string, number>;
|
|
99
|
+
edgesByType: Record<string, number>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class RFDBServerBackend {
|
|
103
|
+
readonly socketPath: string;
|
|
104
|
+
readonly dbPath: string | undefined;
|
|
105
|
+
private readonly autoStart: boolean;
|
|
106
|
+
private readonly silent: boolean;
|
|
107
|
+
private readonly _clientName: string;
|
|
108
|
+
private client: RFDBClient | null;
|
|
109
|
+
private serverProcess: ChildProcess | null;
|
|
110
|
+
connected: boolean; // Public for compatibility
|
|
111
|
+
private protocolVersion: number = 2; // Negotiated protocol version
|
|
112
|
+
private edgeTypes: Set<string>;
|
|
113
|
+
private _cachedNodeCounts: Record<string, number> | undefined;
|
|
114
|
+
private _cachedEdgeCounts: Record<string, number> | undefined;
|
|
115
|
+
|
|
116
|
+
constructor(options: RFDBServerBackendOptions = {}) {
|
|
117
|
+
this.dbPath = options.dbPath;
|
|
118
|
+
this.autoStart = options.autoStart ?? true; // Default true for backwards compat
|
|
119
|
+
this.silent = options.silent ?? false;
|
|
120
|
+
this._clientName = options.clientName ?? 'core';
|
|
121
|
+
// Default socket path: next to the database in .grafema folder
|
|
122
|
+
// This ensures each project has its own socket, avoiding conflicts
|
|
123
|
+
if (options.socketPath) {
|
|
124
|
+
this.socketPath = options.socketPath;
|
|
125
|
+
} else if (this.dbPath) {
|
|
126
|
+
this.socketPath = join(dirname(this.dbPath), 'rfdb.sock');
|
|
127
|
+
} else {
|
|
128
|
+
this.socketPath = '/tmp/rfdb.sock'; // fallback, not recommended
|
|
129
|
+
}
|
|
130
|
+
this.client = null;
|
|
131
|
+
this.serverProcess = null;
|
|
132
|
+
this.connected = false;
|
|
133
|
+
this.edgeTypes = new Set();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Log message if not in silent mode.
|
|
138
|
+
*/
|
|
139
|
+
private log(message: string): void {
|
|
140
|
+
if (!this.silent) {
|
|
141
|
+
console.error(message);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Log error (always shown, even in silent mode).
|
|
147
|
+
*/
|
|
148
|
+
private logError(message: string, error?: unknown): void {
|
|
149
|
+
console.error(message, error ?? '');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Connect to RFDB server.
|
|
154
|
+
* If autoStart is true (default), starts the server if not running.
|
|
155
|
+
* If autoStart is false, requires explicit `grafema server start`.
|
|
156
|
+
*/
|
|
157
|
+
async connect(): Promise<void> {
|
|
158
|
+
if (this.connected) return;
|
|
159
|
+
|
|
160
|
+
// Try to connect first
|
|
161
|
+
this.client = new RFDBClient(this.socketPath, this._clientName);
|
|
162
|
+
|
|
163
|
+
// Attach error handler to prevent unhandled 'error' events
|
|
164
|
+
// This is important for stale sockets (socket file exists but server is dead)
|
|
165
|
+
this.client.on('error', (err: Error) => {
|
|
166
|
+
this.logError('[RFDBServerBackend] Client error:', err.message);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
await this.client.connect();
|
|
171
|
+
// Verify server is responsive
|
|
172
|
+
await this.client.ping();
|
|
173
|
+
this.connected = true;
|
|
174
|
+
await this._negotiateProtocol();
|
|
175
|
+
this.log(`[RFDBServerBackend] Connected to RFDB server at ${this.socketPath} (protocol v${this.protocolVersion})`);
|
|
176
|
+
return;
|
|
177
|
+
} catch {
|
|
178
|
+
// Server not running or stale socket
|
|
179
|
+
if (!this.autoStart) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`RFDB server not running at ${this.socketPath}\n` +
|
|
182
|
+
`Start the server first: grafema server start`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
this.log(`[RFDBServerBackend] RFDB server not running, starting...`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Start the server (only if autoStart is true)
|
|
189
|
+
await this._startServer();
|
|
190
|
+
|
|
191
|
+
// Connect again with fresh client
|
|
192
|
+
this.client = new RFDBClient(this.socketPath, this._clientName);
|
|
193
|
+
this.client.on('error', (err: Error) => {
|
|
194
|
+
this.logError('[RFDBServerBackend] Client error:', err.message);
|
|
195
|
+
});
|
|
196
|
+
await this.client.connect();
|
|
197
|
+
await this.client.ping();
|
|
198
|
+
this.connected = true;
|
|
199
|
+
await this._negotiateProtocol();
|
|
200
|
+
this.log(`[RFDBServerBackend] Connected to RFDB server at ${this.socketPath} (protocol v${this.protocolVersion})`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Alias for connect()
|
|
205
|
+
*/
|
|
206
|
+
async initialize(): Promise<void> {
|
|
207
|
+
return this.connect();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Start RFDB server process using shared utility.
|
|
212
|
+
*/
|
|
213
|
+
private async _startServer(): Promise<void> {
|
|
214
|
+
if (!this.dbPath) {
|
|
215
|
+
throw new Error('dbPath required to start RFDB server');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await startRfdbServer({
|
|
219
|
+
dbPath: this.dbPath,
|
|
220
|
+
socketPath: this.socketPath,
|
|
221
|
+
pidPath: join(dirname(this.socketPath), 'rfdb.pid'),
|
|
222
|
+
waitTimeoutMs: 5000,
|
|
223
|
+
logger: this.silent ? undefined : { debug: (m: string) => this.log(m) },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Negotiate protocol version with server.
|
|
229
|
+
* Requests v3 (semantic IDs), falls back to v2 if server doesn't support it.
|
|
230
|
+
* Called after ping() confirmed connectivity, so failures here indicate
|
|
231
|
+
* the server doesn't support hello/v3, not network issues.
|
|
232
|
+
*/
|
|
233
|
+
private async _negotiateProtocol(): Promise<void> {
|
|
234
|
+
if (!this.client) return;
|
|
235
|
+
try {
|
|
236
|
+
const hello = await this.client.hello(3);
|
|
237
|
+
this.protocolVersion = hello.protocolVersion;
|
|
238
|
+
this._checkServerVersion(hello.serverVersion);
|
|
239
|
+
} catch {
|
|
240
|
+
// Server predates hello command or doesn't support v3 — safe v2 fallback
|
|
241
|
+
this.protocolVersion = 2;
|
|
242
|
+
this.log('[RFDBServerBackend] WARNING: Server does not support version negotiation. Consider updating rfdb-server.');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Validate server version against expected client version.
|
|
248
|
+
* Warns on mismatch but never fails — version differences are informational.
|
|
249
|
+
*/
|
|
250
|
+
private _checkServerVersion(serverVersion: string): void {
|
|
251
|
+
if (!serverVersion) return;
|
|
252
|
+
const expected = getSchemaVersion(GRAFEMA_VERSION);
|
|
253
|
+
const actual = getSchemaVersion(serverVersion);
|
|
254
|
+
if (actual !== expected) {
|
|
255
|
+
this.log(
|
|
256
|
+
`[RFDBServerBackend] WARNING: rfdb-server version mismatch — ` +
|
|
257
|
+
`server v${serverVersion}, expected v${GRAFEMA_VERSION}. ` +
|
|
258
|
+
`Update with: grafema server restart`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Close client connection. Server continues running to serve other clients.
|
|
265
|
+
*/
|
|
266
|
+
async close(): Promise<void> {
|
|
267
|
+
// Request server flush before disconnecting
|
|
268
|
+
if (this.client) {
|
|
269
|
+
try {
|
|
270
|
+
await this.client.flush();
|
|
271
|
+
} catch {
|
|
272
|
+
// Ignore flush errors on close - best effort
|
|
273
|
+
}
|
|
274
|
+
await this.client.close();
|
|
275
|
+
this.client = null;
|
|
276
|
+
}
|
|
277
|
+
this.connected = false;
|
|
278
|
+
|
|
279
|
+
// NOTE: We intentionally do NOT kill the server process.
|
|
280
|
+
// The server continues running to serve other clients (MCP, other CLI invocations).
|
|
281
|
+
// This is by design for multi-client architecture.
|
|
282
|
+
// Server lifecycle is managed separately (system process, or manual grafema server stop).
|
|
283
|
+
this.serverProcess = null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Clear the database
|
|
288
|
+
*/
|
|
289
|
+
async clear(): Promise<void> {
|
|
290
|
+
if (!this.client) throw new Error('Not connected');
|
|
291
|
+
await this.client.clear();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Flush data to disk
|
|
296
|
+
*/
|
|
297
|
+
async flush(): Promise<void> {
|
|
298
|
+
if (!this.client) throw new Error('Not connected');
|
|
299
|
+
await this.client.flush();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Declare metadata fields for server-side indexing.
|
|
304
|
+
* Persisted in metadata.json — survives database reopen.
|
|
305
|
+
*/
|
|
306
|
+
async declareFields(fields: FieldDeclaration[]): Promise<number> {
|
|
307
|
+
if (!this.client) throw new Error('Not connected');
|
|
308
|
+
return this.client.declareFields(fields);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ===========================================================================
|
|
312
|
+
// Node Operations
|
|
313
|
+
// ===========================================================================
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Add a single node
|
|
317
|
+
*/
|
|
318
|
+
async addNode(node: InputNode): Promise<void> {
|
|
319
|
+
return this.addNodes([node]);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Add multiple nodes
|
|
324
|
+
*/
|
|
325
|
+
async addNodes(nodes: InputNode[]): Promise<void> {
|
|
326
|
+
if (!this.client) throw new Error('Not connected');
|
|
327
|
+
if (!nodes.length) return;
|
|
328
|
+
|
|
329
|
+
const useV3 = this.protocolVersion >= 3;
|
|
330
|
+
const wireNodes: WireNode[] = nodes.map(n => {
|
|
331
|
+
// Extract metadata from node
|
|
332
|
+
const { id, type, nodeType, node_type, name, file, exported, ...rest } = n;
|
|
333
|
+
|
|
334
|
+
const wire: WireNode = {
|
|
335
|
+
id: String(id),
|
|
336
|
+
nodeType: (nodeType || node_type || type || 'UNKNOWN') as NodeType,
|
|
337
|
+
name: name || '',
|
|
338
|
+
file: file || '',
|
|
339
|
+
exported: exported || false,
|
|
340
|
+
metadata: useV3
|
|
341
|
+
? JSON.stringify(rest)
|
|
342
|
+
: JSON.stringify({ originalId: String(id), ...rest }),
|
|
343
|
+
};
|
|
344
|
+
if (useV3) {
|
|
345
|
+
wire.semanticId = String(id);
|
|
346
|
+
}
|
|
347
|
+
return wire;
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await this.client.addNodes(wireNodes);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Add a single edge
|
|
355
|
+
*/
|
|
356
|
+
async addEdge(edge: InputEdge): Promise<void> {
|
|
357
|
+
return this.addEdges([edge]);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Add multiple edges
|
|
362
|
+
*/
|
|
363
|
+
async addEdges(edges: InputEdge[], skipValidation = false): Promise<void> {
|
|
364
|
+
if (!this.client) throw new Error('Not connected');
|
|
365
|
+
if (!edges.length) return;
|
|
366
|
+
|
|
367
|
+
// Track edge types
|
|
368
|
+
for (const e of edges) {
|
|
369
|
+
const edgeType = e.edgeType || e.edge_type || e.etype || e.type;
|
|
370
|
+
if (typeof edgeType === 'string') this.edgeTypes.add(edgeType);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const useV3 = this.protocolVersion >= 3;
|
|
374
|
+
const wireEdges: WireEdge[] = edges.map(e => {
|
|
375
|
+
const { src, dst, type, edgeType, edge_type, etype, metadata, ...rest } = e;
|
|
376
|
+
|
|
377
|
+
// Flatten metadata: spread both edge-level properties and nested metadata
|
|
378
|
+
const flatMetadata = useV3
|
|
379
|
+
? { ...rest, ...(typeof metadata === 'object' && metadata !== null ? metadata : {}) }
|
|
380
|
+
: { _origSrc: String(src), _origDst: String(dst), ...rest, ...(typeof metadata === 'object' && metadata !== null ? metadata : {}) };
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
src: String(src),
|
|
384
|
+
dst: String(dst),
|
|
385
|
+
edgeType: (edgeType || edge_type || etype || type || 'UNKNOWN') as EdgeType,
|
|
386
|
+
metadata: JSON.stringify(flatMetadata),
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
await this.client.addEdges(wireEdges, skipValidation);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Get a node by ID
|
|
395
|
+
*/
|
|
396
|
+
async getNode(id: string): Promise<BaseNodeRecord | null> {
|
|
397
|
+
if (!this.client) throw new Error('Not connected');
|
|
398
|
+
const node = await this.client.getNode(String(id));
|
|
399
|
+
if (!node) return null;
|
|
400
|
+
|
|
401
|
+
return this._parseNode(node);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Check if node exists
|
|
406
|
+
*/
|
|
407
|
+
async nodeExists(id: string): Promise<boolean> {
|
|
408
|
+
if (!this.client) throw new Error('Not connected');
|
|
409
|
+
return this.client.nodeExists(id);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Delete a node
|
|
414
|
+
*/
|
|
415
|
+
async deleteNode(id: string): Promise<void> {
|
|
416
|
+
if (!this.client) throw new Error('Not connected');
|
|
417
|
+
await this.client.deleteNode(id);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Find nodes by attributes
|
|
422
|
+
*/
|
|
423
|
+
async findByAttr(query: AttrQuery): Promise<string[]> {
|
|
424
|
+
if (!this.client) throw new Error('Not connected');
|
|
425
|
+
return this.client.findByAttr(query);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Parse a node from wire format to JS format
|
|
430
|
+
*/
|
|
431
|
+
private _parseNode(wireNode: WireNode): AnyBrandedNode {
|
|
432
|
+
const metadata: Record<string, unknown> = wireNode.metadata ? JSON.parse(wireNode.metadata) : {};
|
|
433
|
+
|
|
434
|
+
// Parse nested JSON strings
|
|
435
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
436
|
+
if (typeof value === 'string' && (value.startsWith('[') || value.startsWith('{'))) {
|
|
437
|
+
try {
|
|
438
|
+
metadata[key] = JSON.parse(value);
|
|
439
|
+
} catch {
|
|
440
|
+
// Not JSON, keep as string
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Prefer metadata.semanticId (original v1 format preserved by RFDB server),
|
|
446
|
+
// then v3 semanticId, then v2 originalId metadata hack, then raw id
|
|
447
|
+
const humanId = (metadata.semanticId as string) || wireNode.semanticId || (metadata.originalId as string) || wireNode.id;
|
|
448
|
+
|
|
449
|
+
// Exclude standard fields from metadata to prevent overwriting wireNode values
|
|
450
|
+
// REG-325: Metadata spread was overwriting name with LITERAL node data
|
|
451
|
+
const {
|
|
452
|
+
id: _id,
|
|
453
|
+
type: _type,
|
|
454
|
+
name: _name,
|
|
455
|
+
file: _file,
|
|
456
|
+
exported: _exported,
|
|
457
|
+
nodeType: _nodeType,
|
|
458
|
+
originalId: _originalId, // Already extracted above
|
|
459
|
+
semanticId: _semanticId, // Exclude from safeMetadata (used for humanId above)
|
|
460
|
+
...safeMetadata
|
|
461
|
+
} = metadata;
|
|
462
|
+
|
|
463
|
+
const parsed = {
|
|
464
|
+
id: humanId,
|
|
465
|
+
type: wireNode.nodeType,
|
|
466
|
+
name: wireNode.name,
|
|
467
|
+
file: wireNode.file,
|
|
468
|
+
exported: wireNode.exported,
|
|
469
|
+
...safeMetadata,
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// Re-brand nodes coming from database
|
|
473
|
+
return brandNodeInternal(parsed);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Parse an edge from wire format to EdgeRecord
|
|
478
|
+
*/
|
|
479
|
+
private _parseEdge(wireEdge: WireEdge): EdgeRecord {
|
|
480
|
+
const meta: Record<string, unknown> = wireEdge.metadata ? JSON.parse(wireEdge.metadata) : {};
|
|
481
|
+
// v3: server resolves src/dst to semantic IDs, use directly
|
|
482
|
+
// v2: fall back to _origSrc/_origDst metadata hack
|
|
483
|
+
const { _origSrc, _origDst, ...rest } = meta;
|
|
484
|
+
const src = this.protocolVersion >= 3
|
|
485
|
+
? wireEdge.src
|
|
486
|
+
: (_origSrc as string) || wireEdge.src;
|
|
487
|
+
const dst = this.protocolVersion >= 3
|
|
488
|
+
? wireEdge.dst
|
|
489
|
+
: (_origDst as string) || wireEdge.dst;
|
|
490
|
+
return {
|
|
491
|
+
src,
|
|
492
|
+
dst,
|
|
493
|
+
type: wireEdge.edgeType,
|
|
494
|
+
metadata: Object.keys(rest).length > 0 ? rest : undefined,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Async generator for querying nodes
|
|
500
|
+
*/
|
|
501
|
+
async *queryNodes(query: NodeQuery): AsyncGenerator<BaseNodeRecord, void, unknown> {
|
|
502
|
+
if (!this.client) throw new Error('Not connected');
|
|
503
|
+
|
|
504
|
+
// Build query for server
|
|
505
|
+
const serverQuery: NodeQuery = {};
|
|
506
|
+
if (query.nodeType) serverQuery.nodeType = query.nodeType;
|
|
507
|
+
if (query.type) serverQuery.nodeType = query.type;
|
|
508
|
+
if (query.name) serverQuery.name = query.name;
|
|
509
|
+
if (query.file) serverQuery.file = query.file;
|
|
510
|
+
if (query.substringMatch) serverQuery.substringMatch = query.substringMatch;
|
|
511
|
+
|
|
512
|
+
// Use findByType if only nodeType specified
|
|
513
|
+
if (serverQuery.nodeType && Object.keys(serverQuery).length === 1) {
|
|
514
|
+
const ids = await this.client.findByType(serverQuery.nodeType);
|
|
515
|
+
for (const id of ids) {
|
|
516
|
+
const node = await this.getNode(id);
|
|
517
|
+
if (node) yield node;
|
|
518
|
+
}
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Otherwise use client's queryNodes
|
|
523
|
+
for await (const wireNode of this.client.queryNodes(serverQuery as unknown as RFDBAttrQuery)) {
|
|
524
|
+
yield this._parseNode(wireNode);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Get ALL nodes matching query (collects from queryNodes into array)
|
|
530
|
+
*/
|
|
531
|
+
async getAllNodes(query: NodeQuery = {}): Promise<BaseNodeRecord[]> {
|
|
532
|
+
const nodes: BaseNodeRecord[] = [];
|
|
533
|
+
for await (const node of this.queryNodes(query)) {
|
|
534
|
+
nodes.push(node);
|
|
535
|
+
}
|
|
536
|
+
return nodes;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ===========================================================================
|
|
540
|
+
// Edge Operations
|
|
541
|
+
// ===========================================================================
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Delete an edge
|
|
545
|
+
*/
|
|
546
|
+
async deleteEdge(src: string, dst: string, type: string): Promise<void> {
|
|
547
|
+
if (!this.client) throw new Error('Not connected');
|
|
548
|
+
await this.client.deleteEdge(src, dst, type as EdgeType);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Get all edges
|
|
553
|
+
*/
|
|
554
|
+
async getAllEdges(): Promise<EdgeRecord[]> {
|
|
555
|
+
return this.getAllEdgesAsync();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Get all edges (async version)
|
|
560
|
+
*/
|
|
561
|
+
async getAllEdgesAsync(): Promise<EdgeRecord[]> {
|
|
562
|
+
if (!this.client) throw new Error('Not connected');
|
|
563
|
+
const edges = await this.client.getAllEdges();
|
|
564
|
+
return edges.map(e => this._parseEdge(e));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Get outgoing edges from a node
|
|
569
|
+
*/
|
|
570
|
+
async getOutgoingEdges(nodeId: string, edgeTypes: EdgeType[] | null = null): Promise<EdgeRecord[]> {
|
|
571
|
+
if (!this.client) throw new Error('Not connected');
|
|
572
|
+
const edges = await this.client.getOutgoingEdges(nodeId, edgeTypes || undefined);
|
|
573
|
+
return edges.map(e => this._parseEdge(e));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Get incoming edges to a node
|
|
578
|
+
*/
|
|
579
|
+
async getIncomingEdges(nodeId: string, edgeTypes: EdgeType[] | null = null): Promise<EdgeRecord[]> {
|
|
580
|
+
if (!this.client) throw new Error('Not connected');
|
|
581
|
+
const edges = await this.client.getIncomingEdges(nodeId, edgeTypes || undefined);
|
|
582
|
+
return edges.map(e => this._parseEdge(e));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ===========================================================================
|
|
586
|
+
// Graph Traversal
|
|
587
|
+
// ===========================================================================
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* BFS traversal
|
|
591
|
+
*/
|
|
592
|
+
async bfs(startIds: string[], maxDepth: number, edgeTypes: EdgeType[]): Promise<string[]> {
|
|
593
|
+
if (!this.client) throw new Error('Not connected');
|
|
594
|
+
return this.client.bfs(startIds, maxDepth, edgeTypes);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* DFS traversal
|
|
599
|
+
*/
|
|
600
|
+
async dfs(startIds: string[], maxDepth: number, edgeTypes: EdgeType[] = []): Promise<string[]> {
|
|
601
|
+
if (!this.client) throw new Error('Not connected');
|
|
602
|
+
return this.client.dfs(startIds, maxDepth, edgeTypes);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Reachability query - find all nodes reachable from start nodes
|
|
607
|
+
*/
|
|
608
|
+
async reachability(
|
|
609
|
+
startIds: string[],
|
|
610
|
+
maxDepth: number,
|
|
611
|
+
edgeTypes: EdgeType[] = [],
|
|
612
|
+
backward: boolean = false
|
|
613
|
+
): Promise<string[]> {
|
|
614
|
+
if (!this.client) throw new Error('Not connected');
|
|
615
|
+
return this.client.reachability(startIds, maxDepth, edgeTypes, backward);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ===========================================================================
|
|
619
|
+
// Statistics
|
|
620
|
+
// ===========================================================================
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Get node count
|
|
624
|
+
*/
|
|
625
|
+
async nodeCount(): Promise<number> {
|
|
626
|
+
if (!this.client) throw new Error('Not connected');
|
|
627
|
+
return this.client.nodeCount();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Get edge count
|
|
632
|
+
*/
|
|
633
|
+
async edgeCount(): Promise<number> {
|
|
634
|
+
if (!this.client) throw new Error('Not connected');
|
|
635
|
+
return this.client.edgeCount();
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Get statistics
|
|
640
|
+
*/
|
|
641
|
+
async getStats(): Promise<BackendStats> {
|
|
642
|
+
if (!this.client) throw new Error('Not connected');
|
|
643
|
+
const nodeCount = await this.client.nodeCount();
|
|
644
|
+
const edgeCount = await this.client.edgeCount();
|
|
645
|
+
const nodeCounts = await this.client.countNodesByType();
|
|
646
|
+
const edgeCounts = await this.client.countEdgesByType();
|
|
647
|
+
|
|
648
|
+
return {
|
|
649
|
+
nodeCount,
|
|
650
|
+
edgeCount,
|
|
651
|
+
nodesByType: nodeCounts,
|
|
652
|
+
edgesByType: edgeCounts,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Get full server stats including shard diagnostics.
|
|
658
|
+
* Uses the GetStats wire command which returns all metrics in one call.
|
|
659
|
+
*/
|
|
660
|
+
async getServerStats(): Promise<ServerStats> {
|
|
661
|
+
if (!this.client) throw new Error('Not connected');
|
|
662
|
+
return this.client.getStats();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Count nodes by type (sync, returns cached value)
|
|
667
|
+
*/
|
|
668
|
+
async countNodesByType(_types: string[] | null = null): Promise<Record<string, number>> {
|
|
669
|
+
if (!this.client) throw new Error('Not connected');
|
|
670
|
+
return this.client.countNodesByType();
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Count edges by type
|
|
675
|
+
*/
|
|
676
|
+
async countEdgesByType(_edgeTypes: string[] | null = null): Promise<Record<string, number>> {
|
|
677
|
+
if (!this.client) throw new Error('Not connected');
|
|
678
|
+
return this.client.countEdgesByType();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Refresh cached counts (call after analysis)
|
|
683
|
+
*/
|
|
684
|
+
async refreshCounts(): Promise<void> {
|
|
685
|
+
if (!this.client) throw new Error('Not connected');
|
|
686
|
+
this._cachedNodeCounts = await this.client.countNodesByType();
|
|
687
|
+
this._cachedEdgeCounts = await this.client.countEdgesByType();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ===========================================================================
|
|
691
|
+
// Datalog Queries
|
|
692
|
+
// ===========================================================================
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Check a guarantee (Datalog rule) and return violations.
|
|
696
|
+
* @param explain Pass literal `true` to get explain data.
|
|
697
|
+
*/
|
|
698
|
+
async checkGuarantee(ruleSource: string): Promise<Array<{ bindings: Array<{ name: string; value: string }> }>>;
|
|
699
|
+
async checkGuarantee(ruleSource: string, explain: true): Promise<DatalogExplainResult>;
|
|
700
|
+
async checkGuarantee(ruleSource: string, explain?: boolean): Promise<Array<{ bindings: Array<{ name: string; value: string }> }> | DatalogExplainResult> {
|
|
701
|
+
if (!this.client) throw new Error('Not connected');
|
|
702
|
+
if (explain) {
|
|
703
|
+
return await this.client.checkGuarantee(ruleSource, true);
|
|
704
|
+
}
|
|
705
|
+
const violations = await this.client.checkGuarantee(ruleSource);
|
|
706
|
+
// Convert bindings from {X: "value"} to [{name: "X", value: "value"}]
|
|
707
|
+
return violations.map(v => ({
|
|
708
|
+
bindings: Object.entries(v.bindings).map(([name, value]) => ({ name, value }))
|
|
709
|
+
}));
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Load Datalog rules
|
|
714
|
+
*/
|
|
715
|
+
async datalogLoadRules(source: string): Promise<number> {
|
|
716
|
+
if (!this.client) throw new Error('Not connected');
|
|
717
|
+
return await this.client.datalogLoadRules(source);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Clear Datalog rules
|
|
722
|
+
*/
|
|
723
|
+
async datalogClearRules(): Promise<void> {
|
|
724
|
+
if (!this.client) throw new Error('Not connected');
|
|
725
|
+
await this.client.datalogClearRules();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Run a Datalog query.
|
|
730
|
+
* @param explain Pass literal `true` to get explain data.
|
|
731
|
+
*/
|
|
732
|
+
async datalogQuery(query: string): Promise<Array<{ bindings: Array<{ name: string; value: string }> }>>;
|
|
733
|
+
async datalogQuery(query: string, explain: true): Promise<DatalogExplainResult>;
|
|
734
|
+
async datalogQuery(query: string, explain?: boolean): Promise<Array<{ bindings: Array<{ name: string; value: string }> }> | DatalogExplainResult> {
|
|
735
|
+
if (!this.client) throw new Error('Not connected');
|
|
736
|
+
if (explain) {
|
|
737
|
+
return await this.client.datalogQuery(query, true);
|
|
738
|
+
}
|
|
739
|
+
const results = await this.client.datalogQuery(query);
|
|
740
|
+
// Convert bindings from {X: "value"} to [{name: "X", value: "value"}]
|
|
741
|
+
return results.map(r => ({
|
|
742
|
+
bindings: Object.entries(r.bindings).map(([name, value]) => ({ name, value }))
|
|
743
|
+
}));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Execute unified Datalog query or program.
|
|
748
|
+
* Auto-detects whether input is rules or direct query.
|
|
749
|
+
* @param explain Pass literal `true` to get explain data.
|
|
750
|
+
*/
|
|
751
|
+
async executeDatalog(source: string): Promise<Array<{ bindings: Array<{ name: string; value: string }> }>>;
|
|
752
|
+
async executeDatalog(source: string, explain: true): Promise<DatalogExplainResult>;
|
|
753
|
+
async executeDatalog(source: string, explain?: boolean): Promise<Array<{ bindings: Array<{ name: string; value: string }> }> | DatalogExplainResult> {
|
|
754
|
+
if (!this.client) throw new Error('Not connected');
|
|
755
|
+
if (explain) {
|
|
756
|
+
return await this.client.executeDatalog(source, true);
|
|
757
|
+
}
|
|
758
|
+
const results = await this.client.executeDatalog(source);
|
|
759
|
+
return results.map(r => ({
|
|
760
|
+
bindings: Object.entries(r.bindings).map(([name, value]) => ({ name, value }))
|
|
761
|
+
}));
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Run a Cypher query.
|
|
766
|
+
*/
|
|
767
|
+
async cypherQuery(query: string): Promise<CypherResult> {
|
|
768
|
+
if (!this.client) throw new Error('Not connected');
|
|
769
|
+
return await this.client.cypherQuery(query);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// ===========================================================================
|
|
773
|
+
// Batch Operations (RFD-16: CommitBatch protocol)
|
|
774
|
+
// ===========================================================================
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Begin a batch operation. While batching, addNodes/addEdges buffer locally.
|
|
778
|
+
* Call commitBatch() to send all buffered data atomically.
|
|
779
|
+
*/
|
|
780
|
+
beginBatch(): void {
|
|
781
|
+
if (!this.client) throw new Error('Not connected to RFDB server');
|
|
782
|
+
this.client.beginBatch();
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Commit the current batch to the server atomically.
|
|
787
|
+
* Returns a CommitDelta describing what changed.
|
|
788
|
+
*
|
|
789
|
+
* @param tags - Optional tags for the commit
|
|
790
|
+
* @param deferIndex - When true, server writes data but skips index rebuild.
|
|
791
|
+
*/
|
|
792
|
+
async commitBatch(tags?: string[], deferIndex?: boolean, protectedTypes?: string[], changedFiles?: string[]): Promise<CommitDelta> {
|
|
793
|
+
if (!this.client) throw new Error('Not connected to RFDB server');
|
|
794
|
+
return this.client.commitBatch(tags, deferIndex, protectedTypes, changedFiles);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Synchronously batch a node. Must be inside beginBatch/commitBatch.
|
|
799
|
+
* Bypasses async wrapper for direct batch insertion.
|
|
800
|
+
*/
|
|
801
|
+
batchNode(node: InputNode): void {
|
|
802
|
+
if (!this.client) throw new Error('Not connected');
|
|
803
|
+
const { id, type, nodeType, node_type, name, file, exported, ...rest } = node;
|
|
804
|
+
const useV3 = this.protocolVersion >= 3;
|
|
805
|
+
const wire: Record<string, unknown> = {
|
|
806
|
+
id: String(id),
|
|
807
|
+
nodeType: (nodeType || node_type || type || 'UNKNOWN'),
|
|
808
|
+
name: name || '',
|
|
809
|
+
file: file || '',
|
|
810
|
+
exported: exported || false,
|
|
811
|
+
metadata: useV3 ? JSON.stringify(rest) : JSON.stringify({ originalId: String(id), ...rest }),
|
|
812
|
+
};
|
|
813
|
+
if (useV3) {
|
|
814
|
+
wire.semanticId = String(id);
|
|
815
|
+
}
|
|
816
|
+
this.client.batchNode(wire as Parameters<typeof this.client.batchNode>[0]);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Synchronously batch an edge. Must be inside beginBatch/commitBatch.
|
|
821
|
+
*/
|
|
822
|
+
batchEdge(edge: InputEdge): void {
|
|
823
|
+
if (!this.client) throw new Error('Not connected');
|
|
824
|
+
const { src, dst, type, edgeType, edge_type, etype, metadata, ...rest } = edge;
|
|
825
|
+
const edgeTypeStr = edgeType || edge_type || (etype as string) || type;
|
|
826
|
+
if (typeof edgeTypeStr === 'string') this.edgeTypes.add(edgeTypeStr);
|
|
827
|
+
const flatMetadata = { ...rest, ...(typeof metadata === 'object' && metadata !== null ? metadata as Record<string, unknown> : {}) };
|
|
828
|
+
this.client.batchEdge({
|
|
829
|
+
src: String(src),
|
|
830
|
+
dst: String(dst),
|
|
831
|
+
edgeType: (edgeTypeStr || 'UNKNOWN'),
|
|
832
|
+
metadata: JSON.stringify(flatMetadata),
|
|
833
|
+
} as Record<string, unknown>);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Abort the current batch, discarding all buffered data.
|
|
838
|
+
*/
|
|
839
|
+
abortBatch(): void {
|
|
840
|
+
if (!this.client) throw new Error('Not connected to RFDB server');
|
|
841
|
+
this.client.abortBatch();
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Rebuild all secondary indexes after deferred-index commits (REG-487).
|
|
846
|
+
* Call this once after a series of commitBatch(tags, true) calls.
|
|
847
|
+
*/
|
|
848
|
+
async rebuildIndexes(): Promise<void> {
|
|
849
|
+
if (!this.client) throw new Error('Not connected to RFDB server');
|
|
850
|
+
await this.client.rebuildIndexes();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Create an isolated batch handle for concurrent-safe batching (REG-487).
|
|
855
|
+
* Each handle has its own buffers — safe for parallel workers.
|
|
856
|
+
*/
|
|
857
|
+
createBatch(): BatchHandle {
|
|
858
|
+
if (!this.client) throw new Error('Not connected to RFDB server');
|
|
859
|
+
return this.client.createBatch();
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// ===========================================================================
|
|
863
|
+
// Export/Import
|
|
864
|
+
// ===========================================================================
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Export graph (for tests)
|
|
868
|
+
*/
|
|
869
|
+
async export(): Promise<GraphExport> {
|
|
870
|
+
const nodes = await this.getAllNodes();
|
|
871
|
+
const edges = await this.getAllEdgesAsync();
|
|
872
|
+
return {
|
|
873
|
+
nodes: nodes as unknown as GraphExport['nodes'],
|
|
874
|
+
edges: edges as unknown as GraphExport['edges'],
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Find nodes by predicate (for compatibility)
|
|
880
|
+
*/
|
|
881
|
+
async findNodes(predicate: (node: BaseNodeRecord) => boolean): Promise<BaseNodeRecord[]> {
|
|
882
|
+
const allNodes = await this.getAllNodes();
|
|
883
|
+
return allNodes.filter(predicate);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// ===========================================================================
|
|
887
|
+
// Graph property (for compatibility)
|
|
888
|
+
// ===========================================================================
|
|
889
|
+
|
|
890
|
+
get graph(): this {
|
|
891
|
+
return this;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
export default RFDBServerBackend;
|