@devilsdev/rag-pipeline-utils 2.1.11 → 2.2.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 (110) hide show
  1. package/.ragrc.schema.json +218 -0
  2. package/README.md +651 -393
  3. package/bin/cli.js +8 -14
  4. package/dist/index.d.ts +237 -0
  5. package/dist/index.mjs +22 -7
  6. package/package.json +17 -13
  7. package/src/ai/adaptive-retrieval.js +191 -0
  8. package/src/ai/federated-learning.js +214 -0
  9. package/src/ai/federation/federated-learning-coordinator.js +345 -0
  10. package/src/ai/federation.js +76 -0
  11. package/src/ai/index-fixed.js +509 -0
  12. package/src/ai/index.js +33 -0
  13. package/src/ai/model-trainer.js +217 -0
  14. package/src/ai/model-training.js +205 -0
  15. package/src/ai/module-template.js +37 -0
  16. package/src/ai/multimodal/multi-modal-processor.js +345 -0
  17. package/src/ai/multimodal-processing.js +268 -0
  18. package/src/ai/multimodal.js +71 -0
  19. package/src/ai/orchestrator.js +70 -0
  20. package/src/ai/retrieval/adaptive-retrieval-engine.js +398 -0
  21. package/src/ai/retrieval-engine.js +69 -0
  22. package/src/ai/training/model-training-orchestrator.js +228 -0
  23. package/src/ci/workflow-analyzer.js +71 -0
  24. package/src/cli/commands/ai-ml.js +967 -0
  25. package/src/cli/commands/docs.js +228 -0
  26. package/src/cli/commands/dx.js +538 -0
  27. package/src/cli/commands/plugin-hub.js +875 -0
  28. package/src/cli/doctor-command.js +639 -0
  29. package/src/cli/enhanced-cli-commands.js +945 -0
  30. package/src/cli/interactive-wizard.js +997 -0
  31. package/src/cli/plugin-marketplace-commands.js +869 -0
  32. package/src/cli.js +17 -0
  33. package/src/config/enhanced-ragrc-schema.js +632 -0
  34. package/src/config/load-config.js +54 -0
  35. package/src/config/load-plugin-config.js +69 -0
  36. package/src/config/normalize-config.js +105 -0
  37. package/src/config/schema-validator.js +315 -0
  38. package/src/config/validate-plugin-schema.js +35 -0
  39. package/src/config/validate-schema.js +113 -0
  40. package/src/contracts/plugin-contracts.js +33 -0
  41. package/src/core/create-pipeline.js +99 -0
  42. package/src/core/dependency-injection.js +242 -0
  43. package/src/core/observability/event-logger.js +241 -0
  44. package/src/core/observability/instrumented-pipeline.js +609 -0
  45. package/src/core/observability/metrics.js +497 -0
  46. package/src/core/observability/tracing.js +781 -0
  47. package/src/core/performance/benchmark.js +516 -0
  48. package/src/core/performance/parallel-processor.js +240 -0
  49. package/src/core/performance/streaming-safeguards.js +611 -0
  50. package/src/core/pipeline-factory.js +60 -0
  51. package/src/core/plugin-contracts.js +89 -0
  52. package/src/core/plugin-marketplace/plugin-metadata.js +485 -0
  53. package/src/core/plugin-marketplace/plugin-publisher.js +574 -0
  54. package/src/core/plugin-marketplace/plugin-registry-format.js +419 -0
  55. package/src/core/plugin-marketplace/version-resolver.js +61 -0
  56. package/src/core/plugin-registry.js +265 -0
  57. package/src/dag/dag-engine.js +954 -0
  58. package/src/dag/dag-node.js +76 -0
  59. package/src/dag/enhanced-dag-engine.js +822 -0
  60. package/src/dx/index.js +40 -0
  61. package/src/dx/integration-templates.js +665 -0
  62. package/src/dx/performance-profiler.js +968 -0
  63. package/src/dx/realtime-debugger.js +755 -0
  64. package/src/dx/visual-pipeline-builder.js +578 -0
  65. package/src/ecosystem/plugin-analytics-dashboard.js +778 -0
  66. package/src/ecosystem/plugin-certification.js +580 -0
  67. package/src/ecosystem/plugin-hub.js +821 -0
  68. package/src/enterprise/audit-logging.js +967 -0
  69. package/src/enterprise/data-governance.js +408 -0
  70. package/src/enterprise/multi-tenancy.js +496 -0
  71. package/src/enterprise/sso-integration.js +903 -0
  72. package/src/evaluate/evaluator.js +52 -0
  73. package/src/evaluate/scoring.js +91 -0
  74. package/src/index.js +67 -0
  75. package/src/ingest.js +59 -0
  76. package/src/loader/csv-loader.js +38 -0
  77. package/src/loader/directory-loader.js +48 -0
  78. package/src/loader/html-loader.js +62 -0
  79. package/src/loader/markdown-loader.js +67 -0
  80. package/src/mocks/openai-embedder.js +119 -0
  81. package/src/mocks/openai-llm.js +77 -0
  82. package/src/mocks/pdf-loader.js +21 -0
  83. package/src/mocks/pinecone-retriever.js +106 -0
  84. package/src/observability/audit-logger.js +758 -0
  85. package/src/observability/dashboard-config.json +675 -0
  86. package/src/observability/event-logger.js +310 -0
  87. package/src/observability/metrics.js +691 -0
  88. package/src/observability/security-monitor.js +830 -0
  89. package/src/observability/slo-monitor.js +319 -0
  90. package/src/observability/telemetry.js +686 -0
  91. package/src/pipeline/pipeline.js +18 -0
  92. package/src/plugins/sample-embedder.js +53 -0
  93. package/src/plugins/sample-llm.js +30 -0
  94. package/src/plugins/sample-loader.js +24 -0
  95. package/src/plugins/sample-retriever.js +84 -0
  96. package/src/query.js +117 -0
  97. package/src/reranker/llm-reranker.js +85 -0
  98. package/src/scripts/script-utilities.js +36 -0
  99. package/src/security/plugin-sandbox.js +586 -0
  100. package/src/security/plugin-signature-verifier.js +414 -0
  101. package/src/security/validator.js +36 -0
  102. package/src/utils/ci/diagnostic-reporter.js +57 -0
  103. package/src/utils/logger.js +211 -0
  104. package/src/utils/performance-monitor.js +561 -0
  105. package/src/utils/plugin-scaffolder.js +447 -0
  106. package/src/utils/retry-policy.js +443 -0
  107. package/src/utils/retry.js +107 -0
  108. package/src/utils/safe-expression-evaluator.js +579 -0
  109. package/src/utils/structured-logger.js +361 -0
  110. package/src/utils/validate-plugin-contract.js +42 -0
@@ -0,0 +1,954 @@
1
+ /**
2
+ * Version: 2.0.2
3
+ * Path: /src/dag/dag-engine.js
4
+ * Description: DAG executor for chained RAG pipelines with cycle detection, error handling, and optional concurrency
5
+ * Author: Ali Kahwaji
6
+ */
7
+
8
+ const DAGNode = require('./dag-node.js');
9
+
10
+ class DAG {
11
+ constructor() {
12
+ this.nodes = new Map();
13
+ }
14
+
15
+ addNode(id, _fn, options = {}) {
16
+ if (this.nodes.has(id)) {
17
+ throw new Error(`Node with ID "${id}" already exists`);
18
+ }
19
+ const node = new DAGNode(id, _fn, options);
20
+ this.nodes.set(id, node);
21
+ return node;
22
+ }
23
+
24
+ connect(fromId, toId) {
25
+ const from = this.nodes.get(fromId);
26
+ const to = this.nodes.get(toId);
27
+ if (!from || !to) throw new Error(`Invalid edge: ${fromId} → ${toId}`);
28
+ from.outputs.push(to);
29
+ to.inputs.push(from);
30
+ }
31
+
32
+ /**
33
+ * Parse and normalize execute parameters
34
+ * @param {any} seedOrOptions - Seed data or options object
35
+ * @param {Object} _options - Additional options (if first param is seed)
36
+ * @returns {Object} - Normalized {seed, options} object
37
+ */
38
+ _parseExecuteParams(seedOrOptions, _options = {}) {
39
+ // Handle parameter overloading
40
+ let seed, options;
41
+ if (
42
+ seedOrOptions &&
43
+ typeof seedOrOptions === 'object' &&
44
+ !Array.isArray(seedOrOptions) &&
45
+ ('concurrency' in seedOrOptions ||
46
+ 'timeout' in seedOrOptions ||
47
+ 'continueOnError' in seedOrOptions ||
48
+ 'enableCheckpoints' in seedOrOptions ||
49
+ 'checkpointId' in seedOrOptions ||
50
+ 'requiredNodes' in seedOrOptions ||
51
+ 'returnFormat' in seedOrOptions ||
52
+ 'retryFailedNodes' in seedOrOptions ||
53
+ 'maxRetries' in seedOrOptions ||
54
+ 'gracefulDegradation' in seedOrOptions)
55
+ ) {
56
+ // First parameter is options
57
+ options = seedOrOptions;
58
+ seed = options.seed;
59
+ } else {
60
+ // First parameter is seed
61
+ seed = seedOrOptions;
62
+ options = _options;
63
+ }
64
+
65
+ // Extract options with defaults
66
+ const normalizedOptions = {
67
+ concurrency: parseInt(process.env.RAG_MAX_CONCURRENCY, 10) || null,
68
+ timeout: null,
69
+ continueOnError: false,
70
+ gracefulDegradation: false,
71
+ enableCheckpoints: false,
72
+ checkpointId: null,
73
+ requiredNodes: [],
74
+ returnFormat: 'auto',
75
+ retryFailedNodes: false,
76
+ maxRetries: 3,
77
+ ...options,
78
+ };
79
+
80
+ return { seed, options: normalizedOptions };
81
+ }
82
+
83
+ /**
84
+ * Execute all nodes in topological order with concurrency control
85
+ * @param {Array} order - Topologically sorted nodes
86
+ * @param {Object} context - Execution context
87
+ * @returns {Promise<void>}
88
+ */
89
+ async _runAllNodes(order, context) {
90
+ const {
91
+ concurrency,
92
+ results,
93
+ errors,
94
+ fwd,
95
+ continueOnError,
96
+ requiredIds,
97
+ seed,
98
+ gracefulDegradation,
99
+ requiredNodes,
100
+ retryFailedNodes,
101
+ maxRetries,
102
+ } = context;
103
+
104
+ const semaphore = concurrency ? new Array(concurrency).fill(null) : null;
105
+ let semaphoreIndex = 0;
106
+
107
+ const executeWithSemaphore = async (node) => {
108
+ if (semaphore) {
109
+ // Wait for available slot
110
+ while (semaphore[semaphoreIndex % semaphore.length] !== null) {
111
+ await new Promise((resolve) => setTimeout(resolve, 10));
112
+ }
113
+ const slotIndex = semaphoreIndex % semaphore.length;
114
+ semaphoreIndex++;
115
+
116
+ try {
117
+ const result = await this.executeNode(node, {
118
+ results,
119
+ errors,
120
+ fwd,
121
+ continueOnError,
122
+ requiredIds,
123
+ seed,
124
+ gracefulDegradation,
125
+ requiredNodes,
126
+ retryFailedNodes,
127
+ maxRetries,
128
+ });
129
+ // Only set result if it's not undefined (failed optional nodes return undefined)
130
+ if (result !== undefined) {
131
+ results.set(node.id, result);
132
+ }
133
+ } finally {
134
+ semaphore[slotIndex] = null;
135
+ }
136
+ } else {
137
+ const result = await this.executeNode(node, {
138
+ results,
139
+ errors,
140
+ fwd,
141
+ continueOnError,
142
+ requiredIds,
143
+ seed,
144
+ gracefulDegradation,
145
+ requiredNodes,
146
+ retryFailedNodes,
147
+ maxRetries,
148
+ });
149
+ // Only set result if it's not the special failed-optional-node symbol
150
+ if (
151
+ typeof result !== 'symbol' ||
152
+ result.description !== 'failed-optional-node'
153
+ ) {
154
+ results.set(node.id, result);
155
+ }
156
+ }
157
+ };
158
+
159
+ // Execute nodes respecting dependencies
160
+ for (const node of order) {
161
+ // Check if all dependencies are satisfied
162
+ const canExecute = this._canExecuteNode(node, {
163
+ results,
164
+ errors,
165
+ gracefulDegradation,
166
+ requiredNodes,
167
+ });
168
+
169
+ if (canExecute) {
170
+ await executeWithSemaphore(node);
171
+ }
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Check if a node can be executed based on dependency satisfaction
177
+ * @param {DAGNode} node - Node to check
178
+ * @param {Object} context - Execution context
179
+ * @returns {boolean}
180
+ */
181
+ _canExecuteNode(node, context) {
182
+ const { results, errors, gracefulDegradation, requiredNodes } = context;
183
+
184
+ return node.inputs.every((dep) => {
185
+ // If dependency has result or error, it's satisfied
186
+ if (results.has(dep.id) || errors.has(dep.id)) {
187
+ return true;
188
+ }
189
+ // If graceful degradation is enabled and dependency is not required, it's satisfied
190
+ if (
191
+ gracefulDegradation &&
192
+ requiredNodes &&
193
+ !requiredNodes.includes(dep.id)
194
+ ) {
195
+ return true;
196
+ }
197
+ return false;
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Initialize execution state and validate DAG
203
+ * @returns {Object} - Execution state object
204
+ */
205
+ _initializeExecutionState() {
206
+ this.validate();
207
+
208
+ // Initialize execution state
209
+ const results = new Map();
210
+ const errors = new Map();
211
+ const order = this.topoSort();
212
+ const { fwd, rev } = this._buildAdjacency();
213
+ const requiredIds = this._getRequiredIds(fwd, rev);
214
+
215
+ return { results, errors, order, fwd, rev, requiredIds };
216
+ }
217
+
218
+ /**
219
+ * Handle execution errors with appropriate wrapping
220
+ * @param {Error} error - Error to handle
221
+ * @throws {Error} - Appropriately wrapped error
222
+ */
223
+ _handleExecutionError(error) {
224
+ // Error policy: wrap everything in try/catch
225
+ // If caught error is a node error (message starts with "Node " OR error.nodeId) → rethrow unchanged
226
+ if (error.message.startsWith('Node ') || error.nodeId) {
227
+ throw error;
228
+ }
229
+
230
+ // If caught error is validation or aggregate → wrap once
231
+ if (
232
+ error.cycle ||
233
+ /^DAG validation failed/.test(error.message) ||
234
+ error.errors
235
+ ) {
236
+ const wrappedError = new Error(`DAG execution failed: ${error.message}`);
237
+ if (error.cycle) wrappedError.cycle = error.cycle;
238
+ if (error.errors) wrappedError.errors = error.errors;
239
+ if (error.nodeId) wrappedError.nodeId = error.nodeId;
240
+ if (error.timestamp) wrappedError.timestamp = error.timestamp;
241
+ throw wrappedError;
242
+ }
243
+
244
+ // Do NOT wrap timeout errors or 'no sink nodes' errors
245
+ if (
246
+ error.message === 'Execution timeout' ||
247
+ error.message === 'DAG has no sink nodes - no final output available'
248
+ ) {
249
+ throw error;
250
+ }
251
+
252
+ // Otherwise wrap with 'DAG execution failed: ...'
253
+ const wrappedError = new Error(`DAG execution failed: ${error.message}`);
254
+ if (error.errors) wrappedError.errors = error.errors;
255
+ if (error.nodeId) wrappedError.nodeId = error.nodeId;
256
+ if (error.timestamp) wrappedError.timestamp = error.timestamp;
257
+ if (error.cycle) wrappedError.cycle = error.cycle;
258
+ throw wrappedError;
259
+ }
260
+
261
+ /**
262
+ * Execute function with optional timeout
263
+ * @param {Function} runFn - Function to execute
264
+ * @param {number} timeout - Timeout in milliseconds
265
+ * @returns {Promise<void>}
266
+ */
267
+ async _executeWithTimeout(runFn, timeout) {
268
+ if (timeout && timeout > 0) {
269
+ await Promise.race([
270
+ runFn(),
271
+ new Promise((_, reject) => {
272
+ setTimeout(() => reject(new Error('Execution timeout')), timeout);
273
+ }),
274
+ ]);
275
+ } else {
276
+ await runFn();
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Process execution results and handle checkpointing
282
+ * @param {Map} results - Node execution results
283
+ * @param {Map} errors - Node execution errors
284
+ * @param {Map} fwd - Forward adjacency map
285
+ * @param {Object} options - Processing options
286
+ * @returns {any} - Processed results
287
+ */
288
+ _processResults(results, errors, fwd, options) {
289
+ const {
290
+ gracefulDegradation,
291
+ retryFailedNodes,
292
+ enableCheckpoints,
293
+ checkpointId,
294
+ } = options;
295
+
296
+ // Check for multiple errors first
297
+ if (errors.size >= 2) {
298
+ const agg = new Error('Multiple execution errors');
299
+ agg.errors = [...errors.values()].map((x) => x.cause || x);
300
+ throw agg;
301
+ }
302
+
303
+ // Determine sink nodes and successful sinks
304
+ const sinkIds = this._getSinkIds(fwd);
305
+
306
+ if (sinkIds.length === 0) {
307
+ // Check for single error first before generic "no sink nodes"
308
+ if (errors.size === 1) {
309
+ throw [...errors.values()][0];
310
+ }
311
+ throw new Error('DAG has no sink nodes - no final output available');
312
+ }
313
+
314
+ const successfulSinks = sinkIds.filter((id) => results.has(id));
315
+
316
+ if (successfulSinks.length === 0) {
317
+ // Check for single error first before generic "no sink nodes"
318
+ if (errors.size === 1) {
319
+ throw [...errors.values()][0];
320
+ }
321
+ throw new Error('DAG has no sink nodes - no final output available');
322
+ }
323
+
324
+ // Save checkpoint if enabled
325
+ if (enableCheckpoints && checkpointId) {
326
+ this.saveCheckpoint(checkpointId, { results, errors });
327
+ }
328
+
329
+ // For graceful degradation or retry scenarios, always return object with Map-like methods
330
+ if (gracefulDegradation || retryFailedNodes || successfulSinks.length > 1) {
331
+ // Multiple successful sinks OR graceful degradation - return POJO with sink keys enumerable and non-enumerable helpers
332
+ const sinkEntries = successfulSinks.map((id) => [id, results.get(id)]);
333
+ const obj = Object.fromEntries(sinkEntries);
334
+
335
+ // Attach non-enumerable helpers backed by ALL results
336
+ Object.defineProperty(obj, '__allResults', {
337
+ value: new Map(results),
338
+ enumerable: false,
339
+ });
340
+ Object.defineProperty(obj, 'get', {
341
+ value: (k) => obj.__allResults.get(k),
342
+ enumerable: false,
343
+ });
344
+ Object.defineProperty(obj, 'has', {
345
+ value: (k) => obj.__allResults.has(k),
346
+ enumerable: false,
347
+ });
348
+
349
+ return obj;
350
+ } else if (successfulSinks.length === 1) {
351
+ // Single successful sink without graceful degradation - return its value directly
352
+ return results.get(successfulSinks[0]);
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Executes the DAG with optional seed data and configuration
358
+ * @param {any} seedOrOptions - Seed data or options object
359
+ * @param {Object} _options - Additional options (if first param is seed)
360
+ * @returns {Promise<any>} - Execution results
361
+ */
362
+ async execute(seedOrOptions, _options = {}) {
363
+ const { seed, options } = this._parseExecuteParams(seedOrOptions, _options);
364
+ const {
365
+ concurrency,
366
+ timeout,
367
+ continueOnError,
368
+ gracefulDegradation,
369
+ enableCheckpoints,
370
+ checkpointId,
371
+ requiredNodes,
372
+ returnFormat,
373
+ retryFailedNodes,
374
+ maxRetries,
375
+ } = options;
376
+
377
+ try {
378
+ const executionState = this._initializeExecutionState();
379
+
380
+ const { results, errors, order, fwd, requiredIds } = executionState;
381
+
382
+ // Execute nodes with optional timeout
383
+ await this._executeWithTimeout(
384
+ () =>
385
+ this._runAllNodes(order, {
386
+ concurrency,
387
+ results,
388
+ errors,
389
+ fwd,
390
+ continueOnError,
391
+ requiredIds,
392
+ seed,
393
+ gracefulDegradation,
394
+ requiredNodes,
395
+ retryFailedNodes,
396
+ maxRetries,
397
+ }),
398
+ timeout,
399
+ );
400
+
401
+ return this._processResults(results, errors, fwd, {
402
+ gracefulDegradation,
403
+ retryFailedNodes,
404
+ enableCheckpoints,
405
+ checkpointId,
406
+ });
407
+ } catch (error) {
408
+ this._handleExecutionError(error);
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Helper for running with optional global timeout
414
+ * @param {Function} runFn - Function to run
415
+ * @param {number} timeout - Timeout in milliseconds
416
+ * @returns {Promise} - Promise that resolves/rejects with timeout handling
417
+ */
418
+ async _runWithOptionalGlobalTimeout(runFn, timeout) {
419
+ const run = Promise.resolve().then(runFn);
420
+ if (!timeout || timeout <= 0) return run;
421
+ return Promise.race([
422
+ run,
423
+ new Promise((_, reject) => {
424
+ const t = setTimeout(
425
+ () => reject(new Error('Execution timeout')),
426
+ timeout,
427
+ );
428
+ run.finally(() => clearTimeout(t));
429
+ }),
430
+ ]);
431
+ }
432
+
433
+ /**
434
+ * Build adjacency lists for the DAG
435
+ * @returns {Object} - Object with fwd and rev adjacency maps
436
+ */
437
+ _buildAdjacency() {
438
+ const fwd = new Map(); // node -> Set of children
439
+ const rev = new Map(); // node -> Set of parents
440
+
441
+ // Initialize adjacency lists for all nodes
442
+ for (const node of this.nodes.values()) {
443
+ fwd.set(node.id, new Set());
444
+ rev.set(node.id, new Set());
445
+ }
446
+
447
+ // Populate adjacency lists based on node connections
448
+ for (const node of this.nodes.values()) {
449
+ for (const output of node.outputs) {
450
+ fwd.get(node.id).add(output.id);
451
+ // Only add reverse edge if the output node exists in the DAG
452
+ if (rev.has(output.id)) {
453
+ rev.get(output.id).add(node.id);
454
+ }
455
+ }
456
+ }
457
+
458
+ return { fwd, rev };
459
+ }
460
+
461
+ /**
462
+ * Get sink node IDs (nodes with no outgoing edges)
463
+ * @param {Map} fwd - Forward adjacency map
464
+ * @returns {Array} - Array of sink node IDs
465
+ */
466
+ _getSinkIds(fwd) {
467
+ const sinkIds = [];
468
+ for (const [nodeId, children] of fwd) {
469
+ if (children.size === 0) {
470
+ sinkIds.push(nodeId);
471
+ }
472
+ }
473
+ return sinkIds;
474
+ }
475
+
476
+ /**
477
+ * Get required node IDs (all sinks + all their ancestors)
478
+ * @param {Map} fwd - Forward adjacency map
479
+ * @param {Map} rev - Reverse adjacency map
480
+ * @returns {Set} - Set of required node IDs
481
+ */
482
+ _getRequiredIds(fwd, rev) {
483
+ const sinkIds = this._getSinkIds(fwd);
484
+ const required = new Set();
485
+ const visited = new Set();
486
+
487
+ const addAncestors = (nodeId) => {
488
+ if (visited.has(nodeId)) return;
489
+ visited.add(nodeId);
490
+ required.add(nodeId);
491
+
492
+ const parents = rev.get(nodeId) || new Set();
493
+ for (const parentId of parents) {
494
+ addAncestors(parentId);
495
+ }
496
+ };
497
+
498
+ for (const sinkId of sinkIds) {
499
+ addAncestors(sinkId);
500
+ }
501
+
502
+ return required;
503
+ }
504
+
505
+ /**
506
+ * Build adjacency list for the DAG (legacy method for compatibility)
507
+ * @returns {Map} - Adjacency list mapping node IDs to arrays of dependent node IDs
508
+ */
509
+ buildAdjacencyList() {
510
+ const { fwd } = this._buildAdjacency();
511
+ const adjacency = new Map();
512
+
513
+ for (const [nodeId, children] of fwd) {
514
+ adjacency.set(nodeId, Array.from(children));
515
+ }
516
+
517
+ return adjacency;
518
+ }
519
+
520
+ /**
521
+ * Validates the DAG structure
522
+ * @returns {boolean} - True if valid
523
+ * @throws {Error} - If validation fails
524
+ */
525
+ validate() {
526
+ if (this.nodes.size === 0) {
527
+ throw new Error('DAG is empty - no nodes to execute');
528
+ }
529
+
530
+ // Check for cycles using topological sort
531
+ try {
532
+ this.topoSort();
533
+ } catch (error) {
534
+ if (error.message.includes('Cycle detected')) {
535
+ // Extract cycle information from error message or use error.cycle if available
536
+ let nodes = error.cycle;
537
+ if (!nodes && error.message.includes('involving node:')) {
538
+ // Parse cycle from message if cycle array not available
539
+ const match = error.message.match(/involving node: (.+)/);
540
+ if (match) {
541
+ nodes = match[1].split(' -> ');
542
+ }
543
+ }
544
+ if (!nodes) {
545
+ nodes = ['unknown'];
546
+ }
547
+
548
+ const pretty = nodes.join(' -> ');
549
+ const err = new Error(
550
+ `DAG validation failed: DAG topological sort failed: Cycle detected involving node: ${pretty}`,
551
+ );
552
+ err.cycle = nodes;
553
+ throw err;
554
+ }
555
+ throw new Error(`DAG validation failed: ${error.message}`);
556
+ }
557
+
558
+ // Check for orphaned nodes (nodes with no path to any sink)
559
+ const { fwd } = this._buildAdjacency();
560
+ const sinkIds = this._getSinkIds(fwd);
561
+ if (sinkIds.length === 0) {
562
+ throw new Error('DAG has no sink nodes - no final output available');
563
+ }
564
+
565
+ return true;
566
+ }
567
+
568
+ /**
569
+ * Execute a single node with input data
570
+ * @param {DAGNode} node - Node to execute
571
+ * @param {Object} context - Execution context
572
+ * @returns {Promise<any>} Node execution result
573
+ */
574
+ async executeNode(node, context) {
575
+ const {
576
+ results,
577
+ errors,
578
+ fwd,
579
+ continueOnError,
580
+ requiredIds,
581
+ seed,
582
+ gracefulDegradation,
583
+ requiredNodes,
584
+ } = context;
585
+
586
+ // Handle undefined node.run function
587
+ if (typeof node.run !== 'function') {
588
+ throw new Error(`Node ${node.id} has no run function`);
589
+ }
590
+
591
+ // Determine criticality - default is critical unless explicitly marked optional
592
+ // OR if gracefulDegradation is enabled and node is not in requiredNodes
593
+ // OR if there are multiple sink nodes (individual sink failures should be non-critical)
594
+ const sinkIds = this._getSinkIds(fwd);
595
+ const isMultipleSinks = sinkIds.length > 1;
596
+ const isSinkNode = sinkIds.includes(node.id);
597
+
598
+ const isNonCritical =
599
+ node?.optional === true ||
600
+ node?.isOptional === true ||
601
+ node?.critical === false ||
602
+ (gracefulDegradation &&
603
+ requiredNodes &&
604
+ !requiredNodes.includes(node.id)) ||
605
+ (isMultipleSinks && isSinkNode); // Sink nodes are non-critical when there are multiple sinks
606
+
607
+ // Prepare input based on dependencies
608
+ let input;
609
+ if (node.inputs.length === 0) {
610
+ input = seed;
611
+ } else if (node.inputs.length === 1) {
612
+ input = results.get(node.inputs[0].id);
613
+ } else {
614
+ input = node.inputs.map((inputNode) => {
615
+ // For graceful degradation, return undefined for failed optional dependencies
616
+ if (
617
+ !results.has(inputNode.id) &&
618
+ gracefulDegradation &&
619
+ requiredNodes &&
620
+ !requiredNodes.includes(inputNode.id)
621
+ ) {
622
+ return undefined;
623
+ }
624
+ return results.get(inputNode.id);
625
+ });
626
+ }
627
+
628
+ // Retry logic - use global options if available, otherwise node-specific
629
+ const { retryFailedNodes, maxRetries: globalMaxRetries } = context;
630
+ const nodeMaxRetries = retryFailedNodes
631
+ ? Math.max(0, globalMaxRetries ?? 3)
632
+ : Math.max(0, node?.retry?.retries ?? 0);
633
+ const delayMs = Math.max(0, node?.retry?.delayMs ?? 0);
634
+ let attempt = 0;
635
+ let keepRetrying = true;
636
+
637
+ while (keepRetrying) {
638
+ try {
639
+ // Execute the node
640
+ const output = await node.run(input);
641
+ if (errors.has(node.id)) errors.delete(node.id);
642
+ return output;
643
+ } catch (e) {
644
+ attempt += 1;
645
+ if (attempt <= nodeMaxRetries) {
646
+ if (delayMs) await new Promise((r) => setTimeout(r, delayMs));
647
+ continue; // no error recorded yet
648
+ }
649
+ keepRetrying = false;
650
+ // FINAL failure path
651
+ const deps = [...(fwd.get(node.id) || [])];
652
+ const suffix = deps.length
653
+ ? `. This affects downstream nodes: ${deps.join(', ')}`
654
+ : '';
655
+ const nodeErr = new Error(
656
+ `Node ${node.id} execution failed: ${e.message}${suffix}`,
657
+ );
658
+ nodeErr.nodeId = node.id;
659
+ nodeErr.timestamp = Date.now();
660
+ nodeErr.cause = e;
661
+
662
+ // Handle non-critical failures with warnings and no result nullification
663
+ if (isNonCritical || (continueOnError && !requiredIds.has(node.id))) {
664
+ console.warn(`Non-critical node failure: ${nodeErr.message}`);
665
+ errors.set(node.id, nodeErr);
666
+ // Don't set result for failed optional nodes - return special symbol so they don't appear in results
667
+ return Symbol('failed-optional-node');
668
+ }
669
+ errors.set(node.id, nodeErr);
670
+ throw nodeErr;
671
+ }
672
+ }
673
+ }
674
+
675
+ /**
676
+ * Validate topology with advanced options
677
+ * @param {Object} _options - Validation options
678
+ * @returns {Array} - Array of warnings (empty if no issues)
679
+ */
680
+ validateTopology(_options = {}) {
681
+ const { strict = true } = _options;
682
+ const warnings = [];
683
+
684
+ // Check for empty DAG
685
+ if (this.nodes.size === 0) {
686
+ throw new Error('DAG cannot be empty');
687
+ }
688
+
689
+ // Check for self-loops
690
+ for (const node of this.nodes.values()) {
691
+ if (node.outputs.includes(node)) {
692
+ throw new Error('Self-loop detected');
693
+ }
694
+ }
695
+
696
+ // Check for cycles by attempting topological sort
697
+ try {
698
+ this.topoSort();
699
+ } catch (error) {
700
+ if (error.message.includes('Cycle detected')) {
701
+ // Extract cycle information from error message or use error.cycle if available
702
+ let nodes = error.cycle;
703
+ if (!nodes && error.message.includes('involving node:')) {
704
+ // Parse cycle from message if cycle array not available
705
+ const match = error.message.match(/involving node: (.+)/);
706
+ if (match) {
707
+ nodes = match[1].split(' -> ');
708
+ }
709
+ }
710
+ if (!nodes) {
711
+ nodes = ['unknown'];
712
+ }
713
+
714
+ const err = new Error('Cycle detected in DAG');
715
+ err.cycle = nodes;
716
+ throw err;
717
+ }
718
+ throw error;
719
+ }
720
+
721
+ // Check for isolated nodes
722
+ for (const node of this.nodes.values()) {
723
+ if (node.inputs.length === 0 && node.outputs.length === 0) {
724
+ const message = `Orphaned node detected: ${node.id}`;
725
+ if (strict) {
726
+ throw new Error(message);
727
+ } else {
728
+ warnings.push(message);
729
+ }
730
+ }
731
+ }
732
+
733
+ return warnings;
734
+ }
735
+
736
+ /**
737
+ * Performs topological sort with cycle detection
738
+ * @returns {DAGNode[]} Topologically sorted nodes
739
+ * @throws {Error} If cycles are detected
740
+ */
741
+ topoSort() {
742
+ const order = [];
743
+ const visited = new Set();
744
+ const visiting = new Set(); // Track nodes currently being visited
745
+
746
+ const visit = (node, path = []) => {
747
+ if (visiting.has(node.id)) {
748
+ const cycleStart = path.indexOf(node.id);
749
+ let cyclePath =
750
+ cycleStart >= 0
751
+ ? path.slice(cycleStart).concat([node.id])
752
+ : [node.id];
753
+ // Reverse interior to show forward traversal order (A->B->C->A)
754
+ if (cyclePath.length > 2) {
755
+ const startNode = cyclePath[0];
756
+ const restNodes = cyclePath.slice(1, -1).reverse();
757
+ cyclePath = [startNode, ...restNodes, startNode];
758
+ }
759
+ const cyclePathString = cyclePath.join(' -> ');
760
+ const error = new Error(
761
+ `Cycle detected involving node: ${cyclePathString}`,
762
+ );
763
+ error.cycle = cyclePath; // Array format for test assertions
764
+ throw error;
765
+ }
766
+
767
+ if (visited.has(node.id)) return;
768
+
769
+ visiting.add(node.id);
770
+ const newPath = [...path, node.id];
771
+
772
+ try {
773
+ node.inputs.forEach((input) => visit(input, newPath));
774
+ } catch (error) {
775
+ if (error.message.includes('Cycle detected')) {
776
+ throw error; // Preserve original message and cycle property
777
+ }
778
+ throw error;
779
+ }
780
+
781
+ visiting.delete(node.id);
782
+ visited.add(node.id);
783
+ order.push(node);
784
+ };
785
+
786
+ try {
787
+ for (const node of this.nodes.values()) {
788
+ visit(node);
789
+ }
790
+ } catch (error) {
791
+ if (error.message.includes('Cycle detected')) {
792
+ throw error;
793
+ }
794
+ throw new Error(`DAG topological sort failed: ${error.message}`);
795
+ }
796
+
797
+ return order;
798
+ }
799
+
800
+ /**
801
+ * Resume execution from checkpoint data
802
+ * @param {Object} checkpointData - Checkpoint data to resume from
803
+ * @returns {Promise<Map>} - Results Map from resumed execution
804
+ */
805
+ async resume(checkpointData = {}) {
806
+ const { completed = [], results: priorResults = {} } = checkpointData;
807
+
808
+ // Start with prior results as they were
809
+ const results = new Map(Object.entries(priorResults));
810
+
811
+ // Validate topology first
812
+ this.validateTopology();
813
+
814
+ // Get topological order and adjacency
815
+ const order = this.topoSort();
816
+ const { fwd } = this._buildAdjacency();
817
+
818
+ // Execute remaining nodes
819
+ for (const node of order) {
820
+ if (completed.includes(node.id)) {
821
+ continue; // Skip already completed nodes
822
+ }
823
+
824
+ try {
825
+ // Handle undefined node.run function
826
+ if (typeof node.run !== 'function') {
827
+ throw new Error(`Node ${node.id} has no run function`);
828
+ }
829
+
830
+ // Prepare input based on dependencies
831
+ let input;
832
+ if (node.inputs.length === 0) {
833
+ input = null; // No seed in resume
834
+ } else if (node.inputs.length === 1) {
835
+ input = results.get(node.inputs[0].id);
836
+ } else {
837
+ input = node.inputs.map((inputNode) => results.get(inputNode.id));
838
+ }
839
+
840
+ // Retry logic
841
+ const maxRetries = Math.max(0, node?.retry?.retries ?? 0);
842
+ const delayMs = Math.max(0, node?.retry?.delayMs ?? 0);
843
+ let attempt = 0;
844
+ let keepRetrying = true;
845
+
846
+ while (keepRetrying) {
847
+ try {
848
+ // Execute the node
849
+ const output = await node.run(input);
850
+ results.set(node.id, output);
851
+ keepRetrying = false;
852
+ } catch (e) {
853
+ attempt += 1;
854
+ if (attempt <= maxRetries) {
855
+ if (delayMs) await new Promise((r) => setTimeout(r, delayMs));
856
+ continue; // no error recorded yet
857
+ }
858
+ // FINAL failure path - warn EXACT string and keep going
859
+ // eslint-disable-next-line no-console
860
+ console.warn(`Node ${node.id} failed during resume: ${e.message}`);
861
+ keepRetrying = false; // Don't throw, just continue
862
+ }
863
+ }
864
+ } catch (error) {
865
+ // eslint-disable-next-line no-console
866
+ console.warn(`Node ${node.id} failed during resume: ${error.message}`);
867
+ continue;
868
+ }
869
+ }
870
+
871
+ return results;
872
+ }
873
+
874
+ /**
875
+ * Save execution checkpoint
876
+ * @param {string} checkpointId - Unique checkpoint identifier
877
+ * @param {Object} state - Execution state to save
878
+ */
879
+ saveCheckpoint(checkpointId, state) {
880
+ if (!this.checkpoints) {
881
+ this.checkpoints = new Map();
882
+ }
883
+ this.checkpoints.set(checkpointId, {
884
+ timestamp: Date.now(),
885
+ results: new Map(state.results),
886
+ errors: new Map(state.errors),
887
+ });
888
+ }
889
+
890
+ /**
891
+ * Load execution checkpoint
892
+ * @param {string} checkpointId - Unique checkpoint identifier
893
+ * @returns {Object|null} - Saved execution state or null if not found
894
+ */
895
+ loadCheckpoint(checkpointId) {
896
+ if (!this.checkpoints) {
897
+ return null;
898
+ }
899
+ return this.checkpoints.get(checkpointId) || null;
900
+ }
901
+
902
+ /**
903
+ * List all available checkpoints
904
+ * @returns {Array} - Array of checkpoint IDs with metadata
905
+ */
906
+ listCheckpoints() {
907
+ if (!this.checkpoints) {
908
+ return [];
909
+ }
910
+ return Array.from(this.checkpoints.entries()).map(([id, data]) => ({
911
+ id,
912
+ timestamp: data.timestamp,
913
+ resultCount: data.results.size,
914
+ errorCount: data.errors.size,
915
+ }));
916
+ }
917
+
918
+ /**
919
+ * Clear execution checkpoint
920
+ * @param {string} checkpointId - Unique checkpoint identifier
921
+ */
922
+ clearCheckpoint(checkpointId) {
923
+ if (this.checkpoints) {
924
+ this.checkpoints.delete(checkpointId);
925
+ }
926
+ }
927
+
928
+ /**
929
+ * Get all downstream nodes that depend on the given node
930
+ * @param {DAGNode} node - Node to find downstream dependencies for
931
+ * @returns {DAGNode[]} Array of downstream nodes
932
+ */
933
+ getDownstreamNodes(node) {
934
+ const downstream = new Set();
935
+ const visited = new Set();
936
+
937
+ const traverse = (currentNode) => {
938
+ if (visited.has(currentNode.id)) return;
939
+ visited.add(currentNode.id);
940
+
941
+ for (const output of currentNode.outputs) {
942
+ downstream.add(output);
943
+ traverse(output);
944
+ }
945
+ };
946
+
947
+ traverse(node);
948
+ return Array.from(downstream);
949
+ }
950
+ }
951
+
952
+ // Export classes
953
+ module.exports = { DAG, DAGNode };
954
+ module.exports.default = module.exports;