@devilsdev/rag-pipeline-utils 2.3.1 → 2.4.1

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.
Files changed (160) hide show
  1. package/README.md +94 -480
  2. package/contracts/loader-contract.json +11 -6
  3. package/contracts/reranker-contract.json +18 -26
  4. package/dist/index.cjs +21190 -47
  5. package/dist/index.d.ts +298 -68
  6. package/dist/index.mjs +9 -76
  7. package/package.json +37 -11
  8. package/src/agentic/agentic-pipeline.js +235 -0
  9. package/src/agentic/iterative-retriever.js +213 -0
  10. package/src/agentic/query-planner.js +208 -0
  11. package/src/agentic/self-critique.js +102 -0
  12. package/src/ai/adaptive-retrieval.js +0 -3
  13. package/src/ai/federated-learning.js +0 -3
  14. package/src/ai/federation.js +1 -6
  15. package/src/ai/index.js +9 -20
  16. package/src/ai/model-training.js +0 -3
  17. package/src/ai/multimodal-processing.js +0 -3
  18. package/src/ai/multimodal.js +1 -6
  19. package/src/ai/orchestrator.js +1 -6
  20. package/src/ai/retrieval-engine.js +1 -6
  21. package/src/chunking/chunking-engine.js +142 -0
  22. package/src/chunking/strategies/fixed-size.js +54 -0
  23. package/src/chunking/strategies/recursive.js +127 -0
  24. package/src/chunking/strategies/semantic.js +128 -0
  25. package/src/chunking/strategies/sentence.js +32 -0
  26. package/src/chunking/strategies/structure-aware.js +138 -0
  27. package/src/citation/citation-tracker.js +161 -0
  28. package/src/citation/hallucination-detector.js +74 -0
  29. package/src/citation/source-mapper.js +95 -0
  30. package/src/cli/commands/ai-ml/adaptive-commands.js +178 -0
  31. package/src/cli/commands/ai-ml/federated-commands.js +207 -0
  32. package/src/cli/commands/ai-ml/index.js +17 -0
  33. package/src/cli/commands/ai-ml/multimodal-commands.js +187 -0
  34. package/src/cli/commands/ai-ml/shared.js +51 -0
  35. package/src/cli/commands/ai-ml/training-commands.js +228 -0
  36. package/src/cli/commands/ai-ml.js +176 -757
  37. package/src/cli/commands/plugin-hub/certification-commands.js +221 -0
  38. package/src/cli/commands/plugin-hub/event-handlers.js +64 -0
  39. package/src/cli/commands/plugin-hub/hub-commands.js +290 -0
  40. package/src/cli/commands/plugin-hub/review-commands.js +78 -0
  41. package/src/cli/commands/plugin-hub.js +106 -782
  42. package/src/cli/diagnostics/config-checker.js +81 -0
  43. package/src/cli/diagnostics/dependency-checker.js +69 -0
  44. package/src/cli/diagnostics/environment-checker.js +45 -0
  45. package/src/cli/diagnostics/performance-checker.js +51 -0
  46. package/src/cli/diagnostics/plugin-checker.js +106 -0
  47. package/src/cli/diagnostics/security-checker.js +61 -0
  48. package/src/cli/doctor-command.js +70 -369
  49. package/src/cli/enhanced-cli-commands.js +162 -706
  50. package/src/cli/handlers/config-handler.js +126 -0
  51. package/src/cli/handlers/diagnostics-handler.js +113 -0
  52. package/src/cli/handlers/ingest-handler.js +42 -0
  53. package/src/cli/handlers/init-handler.js +51 -0
  54. package/src/cli/handlers/query-handler.js +45 -0
  55. package/src/cli/handlers/run-handler.js +181 -0
  56. package/src/cli/interactive-wizard.js +99 -723
  57. package/src/cli/shared/cli-utils.js +89 -0
  58. package/src/cli/wizard/configuration.js +148 -0
  59. package/src/cli/wizard/observability.js +79 -0
  60. package/src/cli/wizard/performance.js +76 -0
  61. package/src/cli/wizard/plugin-selection.js +297 -0
  62. package/src/cli/wizard/preview-save.js +172 -0
  63. package/src/cli/wizard/project-setup.js +66 -0
  64. package/src/config/memory-optimization.js +3 -3
  65. package/src/connectors/anthropic-connector.js +179 -0
  66. package/src/connectors/base-connector.js +94 -0
  67. package/src/connectors/cohere-connector.js +143 -0
  68. package/src/connectors/local-embedder.js +171 -0
  69. package/src/connectors/memory-retriever.js +150 -0
  70. package/src/connectors/ollama-connector.js +205 -0
  71. package/src/connectors/openai-connector.js +173 -0
  72. package/src/core/create-pipeline.js +21 -1
  73. package/src/core/observability/tracing.js +2 -1
  74. package/src/core/pipeline-factory.js +0 -1
  75. package/src/core/plugin-registry.js +14 -14
  76. package/src/cost/cost-calculator.js +146 -0
  77. package/src/cost/cost-tracker.js +260 -0
  78. package/src/cost/token-budget.js +245 -0
  79. package/src/dag/core/execution-scheduler.js +2 -1
  80. package/src/dag/core/topology-validation.js +0 -3
  81. package/src/dag/dag-engine.js +3 -5
  82. package/src/dag/dag-node.js +0 -1
  83. package/src/dag/enhanced-dag-engine.js +171 -99
  84. package/src/dashboard/dashboard-generator.js +642 -0
  85. package/src/dashboard/metrics-aggregator.js +409 -0
  86. package/src/debug/execution-tracer.js +210 -0
  87. package/src/debug/trace-inspector.js +281 -0
  88. package/src/dev/dev-server.js +5 -5
  89. package/src/dev/hot-reload.js +3 -2
  90. package/src/dx/integration-templates.js +0 -3
  91. package/src/ecosystem/plugin-analytics-dashboard.js +2 -2
  92. package/src/ecosystem/plugin-hub.js +2 -2
  93. package/src/enterprise/audit-logging.js +2 -2
  94. package/src/enterprise/sso-integration.js +52 -32
  95. package/src/evaluate/context-metrics.js +127 -0
  96. package/src/evaluate/evaluator.js +4 -2
  97. package/src/evaluate/faithfulness.js +117 -0
  98. package/src/evaluate/groundedness.js +60 -0
  99. package/src/evaluate/pipeline-evaluator.js +156 -0
  100. package/src/evaluate/relevance.js +129 -0
  101. package/src/evaluate/scoring.js +27 -0
  102. package/src/graph/entity-extractor.js +183 -0
  103. package/src/graph/graph-index.js +113 -0
  104. package/src/graph/graph-retriever.js +244 -0
  105. package/src/graph/knowledge-graph.js +256 -0
  106. package/src/guardrails/guardrails-pipeline.js +136 -0
  107. package/src/guardrails/post-generation-guard.js +170 -0
  108. package/src/guardrails/pre-retrieval-guard.js +161 -0
  109. package/src/guardrails/retrieval-guard.js +126 -0
  110. package/src/index.js +255 -9
  111. package/src/loader/html-loader.js +4 -15
  112. package/src/loader/markdown-loader.js +4 -15
  113. package/src/mcp/mcp-server.js +199 -0
  114. package/src/mcp/mcp-tool-builder.js +121 -0
  115. package/src/observability/audit-logger.js +0 -2
  116. package/src/observability/event-logger.js +2 -1
  117. package/src/observability/security-monitor.js +0 -2
  118. package/src/observability/telemetry.js +0 -2
  119. package/src/reranker/cascade-reranker.js +79 -0
  120. package/src/reranker/embedding-reranker.js +124 -0
  121. package/src/reranker/llm-reranker.js +6 -3
  122. package/src/reranker/scoring-reranker.js +123 -0
  123. package/src/retrieval/bm25-search.js +157 -0
  124. package/src/retrieval/hybrid-retriever.js +111 -0
  125. package/src/retrieval/rank-fusion.js +62 -0
  126. package/src/security/jwt-config.js +4 -3
  127. package/src/security/jwt-validator.js +2 -1
  128. package/src/security/plugin-sandbox.js +0 -3
  129. package/src/security/plugin-signature-verifier.js +4 -6
  130. package/src/security/validator.js +0 -2
  131. package/src/streaming/embedding-stream.js +197 -0
  132. package/src/streaming/sse-adapter.js +168 -0
  133. package/src/streaming/stream-router.js +210 -0
  134. package/src/streaming/streaming-embedder.js +231 -0
  135. package/src/streaming/websocket-adapter.js +171 -0
  136. package/src/templates/scaffold.js +147 -0
  137. package/src/templates/template-registry.js +272 -0
  138. package/src/utils/contract-validator.js +2 -1
  139. package/src/utils/input-sanitizer.js +80 -99
  140. package/src/utils/performance-monitor.js +0 -3
  141. package/src/utils/plugin-scaffolder.js +9 -16
  142. package/src/utils/rate-limiter.js +0 -1
  143. package/src/utils/retry-policy.js +0 -3
  144. package/src/utils/safe-expression-evaluator.js +0 -3
  145. package/src/utils/secure-logger-config.js +2 -4
  146. package/src/utils/secure-logging.js +0 -3
  147. package/src/utils/structured-logger.js +0 -3
  148. package/src/ai/index-fixed.js +0 -509
  149. package/src/ai/module-template.js +0 -37
  150. package/src/ci/workflow-analyzer.js +0 -71
  151. package/src/cli/plugin-marketplace-commands.js +0 -869
  152. package/src/dev/README.md +0 -126
  153. package/src/dev/examples/hot-reload-usage.js +0 -355
  154. package/src/mocks/openai-embedder.js +0 -119
  155. package/src/mocks/openai-llm.js +0 -77
  156. package/src/mocks/pdf-loader.js +0 -21
  157. package/src/mocks/pinecone-retriever.js +0 -106
  158. package/src/scripts/script-utilities.js +0 -36
  159. package/src/utils/ci/diagnostic-reporter.js +0 -57
  160. package/src/utils/examples/error-formatter-usage.js +0 -351
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Fast entity lookup index for the knowledge graph.
5
+ *
6
+ * Maintains secondary indexes by type, name, and arbitrary properties
7
+ * for efficient querying without full graph traversal.
8
+ */
9
+ class GraphIndex {
10
+ constructor() {
11
+ /** @type {Map<string, Set<string>>} type -> Set of entity IDs */
12
+ this.byType = new Map();
13
+ /** @type {Map<string, string>} normalized name -> entity ID */
14
+ this.byName = new Map();
15
+ /** @type {Map<string, Map<*, Set<string>>>} propertyKey -> Map of value -> Set of entity IDs */
16
+ this.byProperty = new Map();
17
+ }
18
+
19
+ /**
20
+ * Index an entity by type, name, and each of its properties.
21
+ * @param {{id: string, type: string, properties: Object}} entity
22
+ */
23
+ indexEntity(entity) {
24
+ const { id, type, properties } = entity;
25
+
26
+ // Index by type
27
+ if (!this.byType.has(type)) {
28
+ this.byType.set(type, new Set());
29
+ }
30
+ this.byType.get(type).add(id);
31
+
32
+ // Index by normalized name
33
+ const name = (properties && properties.name) || entity.name || id;
34
+ const normalized = String(name).toLowerCase();
35
+ this.byName.set(normalized, id);
36
+
37
+ // Index by each property
38
+ if (properties) {
39
+ for (const [key, value] of Object.entries(properties)) {
40
+ if (!this.byProperty.has(key)) {
41
+ this.byProperty.set(key, new Map());
42
+ }
43
+ const valueMap = this.byProperty.get(key);
44
+ if (!valueMap.has(value)) {
45
+ valueMap.set(value, new Set());
46
+ }
47
+ valueMap.get(value).add(id);
48
+ }
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Fuzzy search for entities whose name contains the query (case-insensitive).
54
+ *
55
+ * Results are sorted by relevance: exact matches first, then partial matches.
56
+ *
57
+ * @param {string} query - Search query
58
+ * @returns {string[]} Array of matching entity IDs
59
+ */
60
+ searchByName(query) {
61
+ if (!query || typeof query !== 'string') {
62
+ return [];
63
+ }
64
+
65
+ const normalized = query.toLowerCase();
66
+ const exact = [];
67
+ const partial = [];
68
+
69
+ for (const [name, entityId] of this.byName.entries()) {
70
+ if (name === normalized) {
71
+ exact.push(entityId);
72
+ } else if (name.includes(normalized)) {
73
+ partial.push(entityId);
74
+ }
75
+ }
76
+
77
+ return [...exact, ...partial];
78
+ }
79
+
80
+ /**
81
+ * Return all entity IDs of a given type.
82
+ * @param {string} type - Entity type
83
+ * @returns {string[]} Array of entity IDs
84
+ */
85
+ searchByType(type) {
86
+ const ids = this.byType.get(type);
87
+ return ids ? [...ids] : [];
88
+ }
89
+
90
+ /**
91
+ * Return entity IDs matching a property key-value pair.
92
+ * @param {string} key - Property key
93
+ * @param {*} value - Property value
94
+ * @returns {string[]} Array of entity IDs
95
+ */
96
+ searchByProperty(key, value) {
97
+ const valueMap = this.byProperty.get(key);
98
+ if (!valueMap) return [];
99
+ const ids = valueMap.get(value);
100
+ return ids ? [...ids] : [];
101
+ }
102
+
103
+ /**
104
+ * Clear all indexes.
105
+ */
106
+ clear() {
107
+ this.byType.clear();
108
+ this.byName.clear();
109
+ this.byProperty.clear();
110
+ }
111
+ }
112
+
113
+ module.exports = { GraphIndex };
@@ -0,0 +1,244 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+ const { KnowledgeGraph } = require('./knowledge-graph');
5
+ const { EntityExtractor } = require('./entity-extractor');
6
+ const { GraphIndex } = require('./graph-index');
7
+ const { tokenize } = require('../evaluate/scoring');
8
+
9
+ /**
10
+ * Default configuration for the GraphRetriever.
11
+ * @type {Object}
12
+ */
13
+ const DEFAULT_CONFIG = {
14
+ traversalDepth: 2,
15
+ maxResults: 10,
16
+ entityWeight: 0.6,
17
+ textWeight: 0.4,
18
+ };
19
+
20
+ /**
21
+ * Retriever that uses a knowledge graph for document retrieval.
22
+ *
23
+ * Combines entity-based graph traversal with token-overlap text scoring
24
+ * to rank and retrieve relevant documents from a corpus.
25
+ */
26
+ class GraphRetriever extends EventEmitter {
27
+ /**
28
+ * @param {Object} [options={}] - Retriever options
29
+ * @param {number} [options.traversalDepth=2] - Graph traversal depth
30
+ * @param {number} [options.maxResults=10] - Maximum results to return
31
+ * @param {number} [options.entityWeight=0.6] - Weight for entity-graph score
32
+ * @param {number} [options.textWeight=0.4] - Weight for text-overlap score
33
+ * @param {Object} [options.extractorOptions] - Options for the EntityExtractor
34
+ */
35
+ constructor(options = {}) {
36
+ super();
37
+ this.config = { ...DEFAULT_CONFIG, ...options };
38
+ this.graph = new KnowledgeGraph();
39
+ this.extractor = new EntityExtractor(options.extractorOptions);
40
+ this.index = new GraphIndex();
41
+ /** @type {Map<string, {id: string, content: string, metadata: Object}>} */
42
+ this.documents = new Map();
43
+ }
44
+
45
+ /**
46
+ * Store documents, extract entities and relationships, and build the graph.
47
+ *
48
+ * @param {Array<{id: string, content: string, metadata?: Object}>} documents
49
+ * @returns {Promise<void>}
50
+ */
51
+ async store(documents) {
52
+ if (!Array.isArray(documents)) return;
53
+
54
+ for (const doc of documents) {
55
+ const id = doc.id || `doc_${this.documents.size + 1}`;
56
+ this.documents.set(id, {
57
+ id,
58
+ content: doc.content || '',
59
+ metadata: doc.metadata || {},
60
+ });
61
+ }
62
+
63
+ const { entities, relationships } = this.extractor.extractFromDocuments(
64
+ documents.map((doc) => ({
65
+ id: doc.id || `doc_${this.documents.size}`,
66
+ content: doc.content || '',
67
+ metadata: doc.metadata || {},
68
+ })),
69
+ );
70
+
71
+ for (const entity of entities) {
72
+ const entityId = entity.name.toLowerCase().trim().replace(/\s+/g, '_');
73
+ this.graph.addEntity(entityId, entity.type, {
74
+ name: entity.name,
75
+ frequency: entity.frequency,
76
+ sources: entity.sources || [],
77
+ });
78
+ this.index.indexEntity({
79
+ id: entityId,
80
+ type: entity.type,
81
+ name: entity.name,
82
+ properties: {
83
+ name: entity.name,
84
+ frequency: entity.frequency,
85
+ },
86
+ });
87
+ }
88
+
89
+ for (const rel of relationships) {
90
+ const fromId = rel.from.toLowerCase().trim().replace(/\s+/g, '_');
91
+ const toId = rel.to.toLowerCase().trim().replace(/\s+/g, '_');
92
+
93
+ if (this.graph.getEntity(fromId) && this.graph.getEntity(toId)) {
94
+ this.graph.addRelationship(fromId, toId, rel.type, {
95
+ context: rel.context,
96
+ source: rel.source,
97
+ });
98
+ }
99
+ }
100
+
101
+ this.emit('stored', {
102
+ documentCount: documents.length,
103
+ stats: this.graph.getStats(),
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Retrieve documents relevant to a query using graph + text scoring.
109
+ *
110
+ * @param {string|{query: string, topK?: number}} queryOrObj - Query string or options object
111
+ * @param {number} [k] - Number of results (overrides config.maxResults)
112
+ * @returns {Promise<Array<{id: string, score: number, content: string, metadata: Object}>>}
113
+ */
114
+ async retrieve(queryOrObj, k) {
115
+ const query =
116
+ typeof queryOrObj === 'string' ? queryOrObj : queryOrObj.query;
117
+ const topK =
118
+ k ||
119
+ (typeof queryOrObj === 'object' && queryOrObj.topK) ||
120
+ this.config.maxResults;
121
+
122
+ // Extract entities from query
123
+ const queryEntities = this.extractor.extract(query);
124
+ const queryTokens = tokenize(query);
125
+
126
+ // Score documents
127
+ /** @type {Map<string, {graphScore: number, textScore: number, entities: string[], relationships: Array}>} */
128
+ const docScores = new Map();
129
+
130
+ // Initialize all documents with zero scores
131
+ for (const [docId] of this.documents) {
132
+ docScores.set(docId, {
133
+ graphScore: 0,
134
+ textScore: 0,
135
+ entities: [],
136
+ relationships: [],
137
+ });
138
+ }
139
+
140
+ // Graph-based scoring: find matching entities and traverse
141
+ for (const qEntity of queryEntities) {
142
+ const normalized = qEntity.name.toLowerCase().trim();
143
+ const matchingIds = this.index.searchByName(normalized);
144
+
145
+ for (const entityId of matchingIds) {
146
+ const traversal = this.graph.traverse(
147
+ entityId,
148
+ this.config.traversalDepth,
149
+ );
150
+
151
+ for (const entity of traversal.entities) {
152
+ const sources =
153
+ (entity.properties && entity.properties.sources) || [];
154
+ for (const sourceDocId of sources) {
155
+ if (docScores.has(sourceDocId)) {
156
+ const scores = docScores.get(sourceDocId);
157
+ scores.graphScore += 1;
158
+ if (!scores.entities.includes(entity.id)) {
159
+ scores.entities.push(entity.id);
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ for (const rel of traversal.relationships) {
166
+ for (const [docId, scores] of docScores) {
167
+ const docEntities = scores.entities;
168
+ if (
169
+ docEntities.includes(rel.from) ||
170
+ docEntities.includes(rel.to)
171
+ ) {
172
+ scores.relationships.push(rel);
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ // Text-based scoring: token overlap
180
+ if (queryTokens.length > 0) {
181
+ for (const [docId, doc] of this.documents) {
182
+ const docTokens = tokenize(doc.content);
183
+ if (docTokens.length === 0) continue;
184
+
185
+ const querySet = new Set(queryTokens);
186
+ let overlap = 0;
187
+ for (const token of docTokens) {
188
+ if (querySet.has(token)) {
189
+ overlap += 1;
190
+ }
191
+ }
192
+ const textScore = overlap / Math.max(queryTokens.length, 1);
193
+
194
+ if (docScores.has(docId)) {
195
+ docScores.get(docId).textScore = textScore;
196
+ }
197
+ }
198
+ }
199
+
200
+ // Combine scores and sort
201
+ const results = [];
202
+ for (const [docId, scores] of docScores) {
203
+ const doc = this.documents.get(docId);
204
+ const combinedScore =
205
+ this.config.entityWeight * scores.graphScore +
206
+ this.config.textWeight * scores.textScore;
207
+
208
+ if (combinedScore > 0) {
209
+ results.push({
210
+ id: docId,
211
+ score: combinedScore,
212
+ content: doc.content,
213
+ metadata: {
214
+ ...doc.metadata,
215
+ entities: scores.entities,
216
+ relationships: scores.relationships,
217
+ },
218
+ });
219
+ }
220
+ }
221
+
222
+ results.sort((a, b) => b.score - a.score);
223
+ return results.slice(0, topK);
224
+ }
225
+
226
+ /**
227
+ * Return basic graph statistics.
228
+ * @returns {{entityCount: number, relationshipCount: number}}
229
+ */
230
+ getGraphStats() {
231
+ return this.graph.getStats();
232
+ }
233
+
234
+ /**
235
+ * Clear all stored data, the graph, and indexes.
236
+ */
237
+ clear() {
238
+ this.graph.clear();
239
+ this.index.clear();
240
+ this.documents.clear();
241
+ }
242
+ }
243
+
244
+ module.exports = { GraphRetriever, DEFAULT_CONFIG };
@@ -0,0 +1,256 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+
5
+ /**
6
+ * In-memory knowledge graph with entities and relationships.
7
+ *
8
+ * Stores typed entities and directed relationships between them,
9
+ * supporting traversal, pattern-based querying, and adjacency lookups.
10
+ */
11
+ class KnowledgeGraph extends EventEmitter {
12
+ /**
13
+ * @param {Object} [options={}] - Graph configuration options
14
+ */
15
+ constructor(options = {}) {
16
+ super();
17
+ /** @type {Map<string, {id: string, type: string, properties: Object, labels: string[]}>} */
18
+ this.entities = new Map();
19
+ /** @type {Map<string, {id: string, from: string, to: string, type: string, properties: Object}>} */
20
+ this.relationships = new Map();
21
+ /** @type {Map<string, Set<string>>} */
22
+ this.adjacency = new Map();
23
+ this.options = options;
24
+ this._relationshipCounter = 0;
25
+ }
26
+
27
+ /**
28
+ * Add an entity to the graph.
29
+ * @param {string} id - Unique entity identifier
30
+ * @param {string} type - Entity type (e.g. 'person', 'date', 'url')
31
+ * @param {Object} [properties={}] - Arbitrary key-value properties
32
+ * @returns {{id: string, type: string, properties: Object, labels: string[]}}
33
+ */
34
+ addEntity(id, type, properties = {}) {
35
+ const entity = {
36
+ id,
37
+ type,
38
+ properties: { ...properties },
39
+ labels: properties.labels || [type],
40
+ };
41
+ this.entities.set(id, entity);
42
+
43
+ if (!this.adjacency.has(id)) {
44
+ this.adjacency.set(id, new Set());
45
+ }
46
+
47
+ this.emit('entityAdded', entity);
48
+ return entity;
49
+ }
50
+
51
+ /**
52
+ * Add a directed relationship between two entities.
53
+ * @param {string} fromId - Source entity ID
54
+ * @param {string} toId - Target entity ID
55
+ * @param {string} type - Relationship type (e.g. 'co-occurs', 'references')
56
+ * @param {Object} [properties={}] - Arbitrary key-value properties
57
+ * @returns {{id: string, from: string, to: string, type: string, properties: Object}}
58
+ * @throws {Error} If either entity does not exist
59
+ */
60
+ addRelationship(fromId, toId, type, properties = {}) {
61
+ if (!this.entities.has(fromId)) {
62
+ throw new Error(`Entity not found: ${fromId}`);
63
+ }
64
+ if (!this.entities.has(toId)) {
65
+ throw new Error(`Entity not found: ${toId}`);
66
+ }
67
+
68
+ this._relationshipCounter += 1;
69
+ const id = `rel_${this._relationshipCounter}`;
70
+
71
+ const relationship = {
72
+ id,
73
+ from: fromId,
74
+ to: toId,
75
+ type,
76
+ properties: { ...properties },
77
+ };
78
+
79
+ this.relationships.set(id, relationship);
80
+
81
+ if (!this.adjacency.has(fromId)) {
82
+ this.adjacency.set(fromId, new Set());
83
+ }
84
+ if (!this.adjacency.has(toId)) {
85
+ this.adjacency.set(toId, new Set());
86
+ }
87
+ this.adjacency.get(fromId).add(id);
88
+ this.adjacency.get(toId).add(id);
89
+
90
+ this.emit('relationshipAdded', relationship);
91
+ return relationship;
92
+ }
93
+
94
+ /**
95
+ * Retrieve an entity by ID.
96
+ * @param {string} id - Entity ID
97
+ * @returns {{id: string, type: string, properties: Object, labels: string[]}|null}
98
+ */
99
+ getEntity(id) {
100
+ return this.entities.get(id) || null;
101
+ }
102
+
103
+ /**
104
+ * Get relationships for an entity, optionally filtered.
105
+ * @param {string} entityId - Entity ID
106
+ * @param {Object} [options={}] - Filter options
107
+ * @param {string} [options.type] - Filter by relationship type
108
+ * @param {'outgoing'|'incoming'|'both'} [options.direction='both'] - Direction filter
109
+ * @returns {Array<{id: string, from: string, to: string, type: string, properties: Object}>}
110
+ */
111
+ getRelationships(entityId, options = {}) {
112
+ const { type, direction = 'both' } = options;
113
+ const relIds = this.adjacency.get(entityId);
114
+ if (!relIds) {
115
+ return [];
116
+ }
117
+
118
+ const results = [];
119
+ for (const relId of relIds) {
120
+ const rel = this.relationships.get(relId);
121
+ if (!rel) continue;
122
+
123
+ if (type && rel.type !== type) continue;
124
+
125
+ if (direction === 'outgoing' && rel.from !== entityId) continue;
126
+ if (direction === 'incoming' && rel.to !== entityId) continue;
127
+
128
+ results.push(rel);
129
+ }
130
+ return results;
131
+ }
132
+
133
+ /**
134
+ * BFS traversal from startId up to a given depth.
135
+ * @param {string} startId - Starting entity ID
136
+ * @param {number} [depth=2] - Maximum traversal depth
137
+ * @returns {{entities: Array, relationships: Array, paths: Array<string[]>}}
138
+ */
139
+ traverse(startId, depth = 2) {
140
+ const visited = new Set();
141
+ const resultEntities = [];
142
+ const resultRelationships = [];
143
+ const paths = [];
144
+
145
+ /** @type {Array<{id: string, depth: number, path: string[]}>} */
146
+ const queue = [{ id: startId, depth: 0, path: [startId] }];
147
+ visited.add(startId);
148
+
149
+ const startEntity = this.entities.get(startId);
150
+ if (startEntity) {
151
+ resultEntities.push(startEntity);
152
+ }
153
+
154
+ while (queue.length > 0) {
155
+ const { id, depth: currentDepth, path } = queue.shift();
156
+
157
+ if (currentDepth >= depth) {
158
+ paths.push(path);
159
+ continue;
160
+ }
161
+
162
+ const relIds = this.adjacency.get(id);
163
+ if (!relIds || relIds.size === 0) {
164
+ paths.push(path);
165
+ continue;
166
+ }
167
+
168
+ let expanded = false;
169
+ for (const relId of relIds) {
170
+ const rel = this.relationships.get(relId);
171
+ if (!rel) continue;
172
+
173
+ const neighborId = rel.from === id ? rel.to : rel.from;
174
+ if (visited.has(neighborId)) continue;
175
+
176
+ visited.add(neighborId);
177
+ expanded = true;
178
+
179
+ const neighborEntity = this.entities.get(neighborId);
180
+ if (neighborEntity) {
181
+ resultEntities.push(neighborEntity);
182
+ }
183
+ resultRelationships.push(rel);
184
+
185
+ const newPath = [...path, neighborId];
186
+ queue.push({ id: neighborId, depth: currentDepth + 1, path: newPath });
187
+ }
188
+
189
+ if (!expanded) {
190
+ paths.push(path);
191
+ }
192
+ }
193
+
194
+ return {
195
+ entities: resultEntities,
196
+ relationships: resultRelationships,
197
+ paths,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Query the graph by pattern matching.
203
+ * @param {Object} pattern - Query pattern
204
+ * @param {string} [pattern.entityType] - Filter entities by type
205
+ * @param {string} [pattern.relationshipType] - Filter relationships by type
206
+ * @param {Object} [pattern.properties] - Filter by property key-value pairs
207
+ * @returns {{entities: Array, relationships: Array}}
208
+ */
209
+ query(pattern) {
210
+ const { entityType, relationshipType, properties } = pattern || {};
211
+
212
+ let matchedEntities = [...this.entities.values()];
213
+ if (entityType) {
214
+ matchedEntities = matchedEntities.filter((e) => e.type === entityType);
215
+ }
216
+ if (properties) {
217
+ matchedEntities = matchedEntities.filter((e) =>
218
+ Object.entries(properties).every(
219
+ ([key, value]) => e.properties[key] === value,
220
+ ),
221
+ );
222
+ }
223
+
224
+ let matchedRelationships = [...this.relationships.values()];
225
+ if (relationshipType) {
226
+ matchedRelationships = matchedRelationships.filter(
227
+ (r) => r.type === relationshipType,
228
+ );
229
+ }
230
+
231
+ return { entities: matchedEntities, relationships: matchedRelationships };
232
+ }
233
+
234
+ /**
235
+ * Return basic graph statistics.
236
+ * @returns {{entityCount: number, relationshipCount: number}}
237
+ */
238
+ getStats() {
239
+ return {
240
+ entityCount: this.entities.size,
241
+ relationshipCount: this.relationships.size,
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Clear all graph data.
247
+ */
248
+ clear() {
249
+ this.entities.clear();
250
+ this.relationships.clear();
251
+ this.adjacency.clear();
252
+ this._relationshipCounter = 0;
253
+ }
254
+ }
255
+
256
+ module.exports = { KnowledgeGraph };