@devilsdev/rag-pipeline-utils 2.2.1 → 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.
Files changed (40) hide show
  1. package/README.md +244 -11
  2. package/contracts/contract-schema.json +189 -0
  3. package/dist/index.cjs +19 -0
  4. package/dist/index.mjs +15 -1
  5. package/package.json +3 -2
  6. package/src/cli/commands/wizard.js +627 -0
  7. package/src/cli/input-sanitization-middleware.js +356 -0
  8. package/src/cli/templates/development.template.json +64 -0
  9. package/src/cli/templates/minimal.template.json +33 -0
  10. package/src/cli/templates/production.template.json +104 -0
  11. package/src/cli/templates/testing.template.json +45 -0
  12. package/src/config/connection-pool-config.json +259 -0
  13. package/src/config/memory-optimization.js +414 -0
  14. package/src/config/redaction-patterns.json +150 -0
  15. package/src/config/sanitization-rules.json +323 -0
  16. package/src/core/plugin-registry.js +266 -20
  17. package/src/dag/core/error-context.js +391 -0
  18. package/src/dag/core/execution-scheduler.js +350 -0
  19. package/src/dag/core/topology-validation.js +415 -0
  20. package/src/dag/dag-engine.js +26 -424
  21. package/src/dev/README.md +126 -0
  22. package/src/dev/dev-server.js +356 -0
  23. package/src/dev/examples/hot-reload-usage.js +355 -0
  24. package/src/dev/hot-reload.js +807 -0
  25. package/src/enterprise/sso-integration.js +67 -25
  26. package/src/index.js +19 -0
  27. package/src/observability/event-logger.js +62 -2
  28. package/src/plugins/optimized-openai-embedder.js +169 -0
  29. package/src/security/jwt-config.js +431 -0
  30. package/src/security/jwt-validator.js +701 -0
  31. package/src/utils/api-client-pool-integration.js +436 -0
  32. package/src/utils/batch-processor.js +710 -0
  33. package/src/utils/connection-pool.js +605 -0
  34. package/src/utils/contract-validator.js +460 -0
  35. package/src/utils/error-formatter.js +603 -0
  36. package/src/utils/examples/error-formatter-usage.js +351 -0
  37. package/src/utils/input-sanitizer.js +967 -0
  38. package/src/utils/memory-monitor.js +800 -0
  39. package/src/utils/secure-logger-config.js +352 -0
  40. package/src/utils/secure-logger.js +576 -0
@@ -0,0 +1,415 @@
1
+ /**
2
+ * @fileoverview DAG Topology Validation Module
3
+ *
4
+ * Provides topology validation utilities for Directed Acyclic Graphs (DAGs),
5
+ * including cycle detection, topological sorting, and structural validation.
6
+ *
7
+ * @module dag/core/topology-validation
8
+ * @author Ali Kahwaji
9
+ * @since 2.2.2
10
+ * @version 1.0.0
11
+ */
12
+
13
+ /**
14
+ * Builds forward and reverse adjacency maps for a DAG
15
+ *
16
+ * Creates two Maps:
17
+ * - Forward: Maps each node ID to a Set of its children (outgoing edges)
18
+ * - Reverse: Maps each node ID to a Set of its parents (incoming edges)
19
+ *
20
+ * @param {Map<string, DAGNode>} nodes - Map of node IDs to DAGNode instances
21
+ * @returns {{fwd: Map<string, Set<string>>, rev: Map<string, Set<string>>}}
22
+ * Adjacency maps for forward and reverse traversal
23
+ *
24
+ * @example
25
+ * const nodes = new Map([
26
+ * ['A', nodeA],
27
+ * ['B', nodeB],
28
+ * ['C', nodeC]
29
+ * ]);
30
+ * const { fwd, rev } = buildAdjacency(nodes);
31
+ * // fwd.get('A') => Set(['B', 'C']) - A points to B and C
32
+ * // rev.get('B') => Set(['A']) - B is pointed to by A
33
+ *
34
+ * @performance O(V + E) where V = number of nodes, E = number of edges
35
+ * @since 2.2.2
36
+ */
37
+ function buildAdjacency(nodes) {
38
+ const fwd = new Map(); // node -> Set of children
39
+ const rev = new Map(); // node -> Set of parents
40
+
41
+ // Initialize adjacency lists for all nodes
42
+ for (const node of nodes.values()) {
43
+ fwd.set(node.id, new Set());
44
+ rev.set(node.id, new Set());
45
+ }
46
+
47
+ // Populate adjacency lists based on node connections
48
+ for (const node of nodes.values()) {
49
+ for (const output of node.outputs) {
50
+ fwd.get(node.id).add(output.id);
51
+ // Only add reverse edge if the output node exists in the DAG
52
+ if (rev.has(output.id)) {
53
+ rev.get(output.id).add(node.id);
54
+ }
55
+ }
56
+ }
57
+
58
+ return { fwd, rev };
59
+ }
60
+
61
+ /**
62
+ * Gets sink node IDs (nodes with no outgoing edges)
63
+ *
64
+ * Sink nodes are terminal nodes in the DAG that have no children.
65
+ * They represent final outputs of the pipeline.
66
+ *
67
+ * @param {Map<string, Set<string>>} fwd - Forward adjacency map
68
+ * @returns {string[]} Array of sink node IDs
69
+ *
70
+ * @example
71
+ * const fwd = new Map([
72
+ * ['A', new Set(['B'])],
73
+ * ['B', new Set()], // B is a sink
74
+ * ['C', new Set()] // C is a sink
75
+ * ]);
76
+ * const sinks = getSinkIds(fwd);
77
+ * console.log(sinks); // ['B', 'C']
78
+ *
79
+ * @performance O(V) where V = number of nodes
80
+ * @since 2.2.2
81
+ */
82
+ function getSinkIds(fwd) {
83
+ const sinkIds = [];
84
+ for (const [nodeId, children] of fwd) {
85
+ if (children.size === 0) {
86
+ sinkIds.push(nodeId);
87
+ }
88
+ }
89
+ return sinkIds;
90
+ }
91
+
92
+ /**
93
+ * Performs topological sort with comprehensive cycle detection
94
+ *
95
+ * Uses depth-first search (DFS) with path tracking to detect cycles.
96
+ * If a cycle is detected, the error includes the complete cycle path.
97
+ *
98
+ * @param {Map<string, DAGNode>} nodes - Map of node IDs to DAGNode instances
99
+ * @returns {DAGNode[]} Topologically sorted array of nodes
100
+ *
101
+ * @throws {Error} If a cycle is detected. Error includes:
102
+ * - message: Description with cycle path (e.g., "Cycle detected involving node: A -> B -> C -> A")
103
+ * - cycle: Array of node IDs forming the cycle
104
+ * @throws {Error} If topological sort fails for other reasons
105
+ *
106
+ * @example
107
+ * // Valid DAG
108
+ * const nodes = new Map([
109
+ * ['A', nodeA], // A -> B
110
+ * ['B', nodeB], // B -> C
111
+ * ['C', nodeC] // C (sink)
112
+ * ]);
113
+ * const sorted = topologicalSort(nodes);
114
+ * console.log(sorted.map(n => n.id)); // ['C', 'B', 'A'] (reverse dependency order)
115
+ *
116
+ * @example
117
+ * // Cyclic DAG (throws error)
118
+ * const nodes = new Map([
119
+ * ['A', nodeA], // A -> B
120
+ * ['B', nodeB], // B -> C
121
+ * ['C', nodeC] // C -> A (creates cycle)
122
+ * ]);
123
+ * try {
124
+ * topologicalSort(nodes);
125
+ * } catch (error) {
126
+ * console.log(error.message); // "Cycle detected involving node: A -> B -> C -> A"
127
+ * console.log(error.cycle); // ['A', 'B', 'C', 'A']
128
+ * }
129
+ *
130
+ * @algorithm
131
+ * 1. Initialize empty order array, visited set, and visiting set
132
+ * 2. For each unvisited node, perform DFS:
133
+ * a. If node is currently being visited (in visiting set), cycle detected
134
+ * b. Mark node as visiting
135
+ * c. Recursively visit all input nodes (dependencies)
136
+ * d. Mark node as visited, remove from visiting
137
+ * e. Add node to order array
138
+ * 3. Return order array (nodes in reverse topological order)
139
+ *
140
+ * @performance O(V + E) where V = number of nodes, E = number of edges
141
+ * @complexity Cyclomatic complexity: 6 (within acceptable limits)
142
+ * @since 2.2.2
143
+ */
144
+ function topologicalSort(nodes) {
145
+ const order = [];
146
+ const visited = new Set();
147
+ const visiting = new Set(); // Track nodes currently being visited
148
+
149
+ const visit = (node, path = []) => {
150
+ if (visiting.has(node.id)) {
151
+ const cycleStart = path.indexOf(node.id);
152
+ let cyclePath =
153
+ cycleStart >= 0 ? path.slice(cycleStart).concat([node.id]) : [node.id];
154
+ // Reverse interior to show forward traversal order (A->B->C->A)
155
+ if (cyclePath.length > 2) {
156
+ const startNode = cyclePath[0];
157
+ const restNodes = cyclePath.slice(1, -1).reverse();
158
+ cyclePath = [startNode, ...restNodes, startNode];
159
+ }
160
+ const cyclePathString = cyclePath.join(' -> ');
161
+ const error = new Error(
162
+ `Cycle detected involving node: ${cyclePathString}`,
163
+ );
164
+ error.cycle = cyclePath; // Array format for test assertions
165
+ throw error;
166
+ }
167
+
168
+ if (visited.has(node.id)) return;
169
+
170
+ visiting.add(node.id);
171
+ const newPath = [...path, node.id];
172
+
173
+ try {
174
+ node.inputs.forEach((input) => visit(input, newPath));
175
+ } catch (error) {
176
+ if (error.message.includes('Cycle detected')) {
177
+ throw error; // Preserve original message and cycle property
178
+ }
179
+ throw error;
180
+ }
181
+
182
+ visiting.delete(node.id);
183
+ visited.add(node.id);
184
+ order.push(node);
185
+ };
186
+
187
+ try {
188
+ for (const node of nodes.values()) {
189
+ visit(node);
190
+ }
191
+ } catch (error) {
192
+ if (error.message.includes('Cycle detected')) {
193
+ throw error;
194
+ }
195
+ throw new Error(`DAG topological sort failed: ${error.message}`);
196
+ }
197
+
198
+ // Return nodes in dependency order (sources first, sinks last)
199
+ return order;
200
+ }
201
+
202
+ /**
203
+ * Validates basic DAG structure
204
+ *
205
+ * Performs fundamental validation checks:
206
+ * - DAG is not empty
207
+ * - No cycles exist (via topological sort)
208
+ * - At least one sink node exists
209
+ *
210
+ * This is the primary validation method called before DAG execution.
211
+ *
212
+ * @param {Map<string, DAGNode>} nodes - Map of node IDs to DAGNode instances
213
+ * @returns {boolean} True if validation passes
214
+ *
215
+ * @throws {Error} If DAG is empty - "DAG is empty - no nodes to execute"
216
+ * @throws {Error} If cycle detected - "DAG validation failed: DAG topological sort failed: Cycle detected involving node: ..."
217
+ * - error.cycle: Array of node IDs forming the cycle
218
+ * @throws {Error} If no sink nodes - "DAG has no sink nodes - no final output available"
219
+ * @throws {Error} For other validation failures
220
+ *
221
+ * @example
222
+ * // Valid DAG
223
+ * const nodes = new Map([
224
+ * ['load', loadNode],
225
+ * ['process', processNode],
226
+ * ['save', saveNode]
227
+ * ]);
228
+ * const isValid = validateDAG(nodes); // true
229
+ *
230
+ * @example
231
+ * // Empty DAG
232
+ * try {
233
+ * validateDAG(new Map());
234
+ * } catch (error) {
235
+ * console.log(error.message); // "DAG is empty - no nodes to execute"
236
+ * }
237
+ *
238
+ * @example
239
+ * // No sink nodes (all nodes have outputs)
240
+ * const nodes = new Map([
241
+ * ['A', nodeA] // A -> B
242
+ * ]);
243
+ * // Assuming all nodes have outputs
244
+ * try {
245
+ * validateDAG(nodes);
246
+ * } catch (error) {
247
+ * console.log(error.message); // "DAG has no sink nodes - no final output available"
248
+ * }
249
+ *
250
+ * @performance O(V + E) due to topological sort
251
+ * @since 2.2.2
252
+ */
253
+ function validateDAG(nodes) {
254
+ if (nodes.size === 0) {
255
+ throw new Error('DAG is empty - no nodes to execute');
256
+ }
257
+
258
+ // Check for cycles using topological sort
259
+ try {
260
+ topologicalSort(nodes);
261
+ } catch (error) {
262
+ if (error.message.includes('Cycle detected')) {
263
+ // Extract cycle information from error message or use error.cycle if available
264
+ let cycleNodes = error.cycle;
265
+ if (!cycleNodes && error.message.includes('involving node:')) {
266
+ // Parse cycle from message if cycle array not available
267
+ const match = error.message.match(/involving node: (.+)/);
268
+ if (match) {
269
+ cycleNodes = match[1].split(' -> ');
270
+ }
271
+ }
272
+ if (!cycleNodes) {
273
+ cycleNodes = ['unknown'];
274
+ }
275
+
276
+ const pretty = cycleNodes.join(' -> ');
277
+ const err = new Error(
278
+ `DAG validation failed: DAG topological sort failed: Cycle detected involving node: ${pretty}`,
279
+ );
280
+ err.cycle = cycleNodes;
281
+ throw err;
282
+ }
283
+ throw new Error(`DAG validation failed: ${error.message}`);
284
+ }
285
+
286
+ // Note: Sink node check is performed in DAG._processResults, not here
287
+ // This allows for more flexible DAG construction and execution patterns
288
+
289
+ return true;
290
+ }
291
+
292
+ /**
293
+ * Validates DAG topology with advanced options and warnings
294
+ *
295
+ * Performs comprehensive validation including:
296
+ * - Empty DAG check
297
+ * - Self-loop detection
298
+ * - Cycle detection via topological sort
299
+ * - Isolated node detection (nodes with no connections)
300
+ *
301
+ * Supports strict and non-strict modes. In non-strict mode, some issues
302
+ * (like isolated nodes) generate warnings instead of throwing errors.
303
+ *
304
+ * @param {Map<string, DAGNode>} nodes - Map of node IDs to DAGNode instances
305
+ * @param {Object} options - Validation options
306
+ * @param {boolean} [options.strict=true] - If true, throw errors for all issues.
307
+ * If false, return warnings for non-critical issues
308
+ * @returns {string[]} Array of warning messages (empty if no warnings in strict mode)
309
+ *
310
+ * @throws {Error} If DAG is empty - "DAG cannot be empty"
311
+ * @throws {Error} If self-loop detected - "Self-loop detected"
312
+ * @throws {Error} If cycle detected - "Cycle detected in DAG"
313
+ * - error.cycle: Array of node IDs forming the cycle
314
+ * @throws {Error} If orphaned node found in strict mode
315
+ *
316
+ * @example
317
+ * // Strict mode (default)
318
+ * const nodes = new Map([['isolated', isolatedNode]]);
319
+ * try {
320
+ * validateTopology(nodes, { strict: true });
321
+ * } catch (error) {
322
+ * console.log(error.message); // "Orphaned node detected: isolated"
323
+ * }
324
+ *
325
+ * @example
326
+ * // Non-strict mode (warnings)
327
+ * const nodes = new Map([
328
+ * ['A', nodeA],
329
+ * ['isolated', isolatedNode]
330
+ * ]);
331
+ * const warnings = validateTopology(nodes, { strict: false });
332
+ * console.log(warnings); // ["Orphaned node detected: isolated"]
333
+ *
334
+ * @example
335
+ * // Self-loop detection
336
+ * const nodeWithSelfLoop = new DAGNode('X', () => {});
337
+ * nodeWithSelfLoop.outputs.push(nodeWithSelfLoop);
338
+ * const nodes = new Map([['X', nodeWithSelfLoop]]);
339
+ * try {
340
+ * validateTopology(nodes);
341
+ * } catch (error) {
342
+ * console.log(error.message); // "Self-loop detected"
343
+ * }
344
+ *
345
+ * @performance O(V + E) due to topological sort and graph traversal
346
+ * @complexity Cyclomatic complexity: 5 (within acceptable limits)
347
+ * @since 2.2.2
348
+ */
349
+ function validateTopology(nodes, options = {}) {
350
+ const { strict = true } = options;
351
+ const warnings = [];
352
+
353
+ // Check for empty DAG
354
+ if (nodes.size === 0) {
355
+ throw new Error('DAG cannot be empty');
356
+ }
357
+
358
+ // Check for self-loops
359
+ for (const node of nodes.values()) {
360
+ if (node.outputs.includes(node)) {
361
+ throw new Error('Self-loop detected');
362
+ }
363
+ }
364
+
365
+ // Check for cycles by attempting topological sort
366
+ try {
367
+ topologicalSort(nodes);
368
+ } catch (error) {
369
+ if (error.message.includes('Cycle detected')) {
370
+ // Extract cycle information from error message or use error.cycle if available
371
+ let cycleNodes = error.cycle;
372
+ if (!cycleNodes && error.message.includes('involving node:')) {
373
+ // Parse cycle from message if cycle array not available
374
+ const match = error.message.match(/involving node: (.+)/);
375
+ if (match) {
376
+ cycleNodes = match[1].split(' -> ');
377
+ }
378
+ }
379
+ if (!cycleNodes) {
380
+ cycleNodes = ['unknown'];
381
+ }
382
+
383
+ const err = new Error('Cycle detected in DAG');
384
+ err.cycle = cycleNodes;
385
+ throw err;
386
+ }
387
+ throw error;
388
+ }
389
+
390
+ // Check for isolated nodes
391
+ for (const node of nodes.values()) {
392
+ if (node.inputs.length === 0 && node.outputs.length === 0) {
393
+ const message = `Orphaned node detected: ${node.id}`;
394
+ if (strict) {
395
+ throw new Error(message);
396
+ } else {
397
+ warnings.push(message);
398
+ }
399
+ }
400
+ }
401
+
402
+ return warnings;
403
+ }
404
+
405
+ // Export all functions
406
+ module.exports = {
407
+ buildAdjacency,
408
+ getSinkIds,
409
+ topologicalSort,
410
+ validateDAG,
411
+ validateTopology,
412
+ };
413
+
414
+ // Default export for convenience
415
+ module.exports.default = module.exports;