@devilsdev/rag-pipeline-utils 2.2.1 → 2.3.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.
- package/README.md +260 -19
- package/contracts/contract-schema.json +189 -0
- package/dist/index.cjs +19 -0
- package/dist/index.mjs +15 -1
- package/package.json +3 -2
- package/src/cli/commands/wizard.js +627 -0
- package/src/cli/input-sanitization-middleware.js +356 -0
- package/src/cli/templates/development.template.json +64 -0
- package/src/cli/templates/minimal.template.json +33 -0
- package/src/cli/templates/production.template.json +104 -0
- package/src/cli/templates/testing.template.json +45 -0
- package/src/config/connection-pool-config.json +259 -0
- package/src/config/memory-optimization.js +414 -0
- package/src/config/redaction-patterns.json +150 -0
- package/src/config/sanitization-rules.json +323 -0
- package/src/core/plugin-registry.js +266 -20
- package/src/dag/core/error-context.js +391 -0
- package/src/dag/core/execution-scheduler.js +350 -0
- package/src/dag/core/topology-validation.js +415 -0
- package/src/dag/dag-engine.js +26 -424
- package/src/dev/README.md +126 -0
- package/src/dev/dev-server.js +356 -0
- package/src/dev/examples/hot-reload-usage.js +355 -0
- package/src/dev/hot-reload.js +807 -0
- package/src/enterprise/sso-integration.js +67 -25
- package/src/index.js +19 -0
- package/src/observability/event-logger.js +62 -2
- package/src/plugins/optimized-openai-embedder.js +169 -0
- package/src/security/jwt-config.js +431 -0
- package/src/security/jwt-validator.js +701 -0
- package/src/utils/api-client-pool-integration.js +436 -0
- package/src/utils/batch-processor.js +710 -0
- package/src/utils/connection-pool.js +605 -0
- package/src/utils/contract-validator.js +460 -0
- package/src/utils/error-formatter.js +603 -0
- package/src/utils/examples/error-formatter-usage.js +351 -0
- package/src/utils/input-sanitizer.js +967 -0
- package/src/utils/memory-monitor.js +800 -0
- package/src/utils/secure-logger-config.js +352 -0
- package/src/utils/secure-logger.js +576 -0
package/src/dag/dag-engine.js
CHANGED
|
@@ -6,10 +6,23 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const DAGNode = require('./dag-node.js');
|
|
9
|
+
const {
|
|
10
|
+
buildAdjacency,
|
|
11
|
+
getSinkIds,
|
|
12
|
+
topologicalSort,
|
|
13
|
+
validateDAG,
|
|
14
|
+
validateTopology,
|
|
15
|
+
} = require('./core/topology-validation.js');
|
|
16
|
+
const ExecutionScheduler = require('./core/execution-scheduler.js');
|
|
17
|
+
const ErrorContext = require('./core/error-context.js');
|
|
9
18
|
|
|
10
19
|
class DAG {
|
|
11
20
|
constructor() {
|
|
12
21
|
this.nodes = new Map();
|
|
22
|
+
this.scheduler = new ExecutionScheduler({
|
|
23
|
+
getSinkIds: (fwd) => getSinkIds(fwd),
|
|
24
|
+
});
|
|
25
|
+
this.errorContext = new ErrorContext();
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
addNode(id, _fn, options = {}) {
|
|
@@ -87,89 +100,7 @@ class DAG {
|
|
|
87
100
|
* @returns {Promise<void>}
|
|
88
101
|
*/
|
|
89
102
|
async _runAllNodes(order, context) {
|
|
90
|
-
|
|
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
|
-
}
|
|
103
|
+
return this.scheduler.scheduleExecution(order, context);
|
|
173
104
|
}
|
|
174
105
|
|
|
175
106
|
/**
|
|
@@ -179,23 +110,7 @@ class DAG {
|
|
|
179
110
|
* @returns {boolean}
|
|
180
111
|
*/
|
|
181
112
|
_canExecuteNode(node, context) {
|
|
182
|
-
|
|
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
|
-
});
|
|
113
|
+
return this.scheduler.canExecuteNode(node, context);
|
|
199
114
|
}
|
|
200
115
|
|
|
201
116
|
/**
|
|
@@ -221,41 +136,7 @@ class DAG {
|
|
|
221
136
|
* @throws {Error} - Appropriately wrapped error
|
|
222
137
|
*/
|
|
223
138
|
_handleExecutionError(error) {
|
|
224
|
-
|
|
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;
|
|
139
|
+
throw this.errorContext.wrapExecutionError(error);
|
|
259
140
|
}
|
|
260
141
|
|
|
261
142
|
/**
|
|
@@ -265,16 +146,7 @@ class DAG {
|
|
|
265
146
|
* @returns {Promise<void>}
|
|
266
147
|
*/
|
|
267
148
|
async _executeWithTimeout(runFn, timeout) {
|
|
268
|
-
|
|
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
|
-
}
|
|
149
|
+
return this.scheduler.executeWithTimeout(runFn, timeout);
|
|
278
150
|
}
|
|
279
151
|
|
|
280
152
|
/**
|
|
@@ -294,10 +166,9 @@ class DAG {
|
|
|
294
166
|
} = options;
|
|
295
167
|
|
|
296
168
|
// Check for multiple errors first
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
throw agg;
|
|
169
|
+
const aggregatedError = this.errorContext.aggregateErrors(errors);
|
|
170
|
+
if (aggregatedError && errors.size >= 2) {
|
|
171
|
+
throw aggregatedError;
|
|
301
172
|
}
|
|
302
173
|
|
|
303
174
|
// Determine sink nodes and successful sinks
|
|
@@ -435,27 +306,7 @@ class DAG {
|
|
|
435
306
|
* @returns {Object} - Object with fwd and rev adjacency maps
|
|
436
307
|
*/
|
|
437
308
|
_buildAdjacency() {
|
|
438
|
-
|
|
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 };
|
|
309
|
+
return buildAdjacency(this.nodes);
|
|
459
310
|
}
|
|
460
311
|
|
|
461
312
|
/**
|
|
@@ -464,13 +315,7 @@ class DAG {
|
|
|
464
315
|
* @returns {Array} - Array of sink node IDs
|
|
465
316
|
*/
|
|
466
317
|
_getSinkIds(fwd) {
|
|
467
|
-
|
|
468
|
-
for (const [nodeId, children] of fwd) {
|
|
469
|
-
if (children.size === 0) {
|
|
470
|
-
sinkIds.push(nodeId);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
return sinkIds;
|
|
318
|
+
return getSinkIds(fwd);
|
|
474
319
|
}
|
|
475
320
|
|
|
476
321
|
/**
|
|
@@ -523,46 +368,7 @@ class DAG {
|
|
|
523
368
|
* @throws {Error} - If validation fails
|
|
524
369
|
*/
|
|
525
370
|
validate() {
|
|
526
|
-
|
|
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;
|
|
371
|
+
return validateDAG(this.nodes);
|
|
566
372
|
}
|
|
567
373
|
|
|
568
374
|
/**
|
|
@@ -572,104 +378,7 @@ class DAG {
|
|
|
572
378
|
* @returns {Promise<any>} Node execution result
|
|
573
379
|
*/
|
|
574
380
|
async executeNode(node, context) {
|
|
575
|
-
|
|
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
|
-
}
|
|
381
|
+
return this.scheduler.executeNode(node, context);
|
|
673
382
|
}
|
|
674
383
|
|
|
675
384
|
/**
|
|
@@ -678,59 +387,7 @@ class DAG {
|
|
|
678
387
|
* @returns {Array} - Array of warnings (empty if no issues)
|
|
679
388
|
*/
|
|
680
389
|
validateTopology(_options = {}) {
|
|
681
|
-
|
|
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;
|
|
390
|
+
return validateTopology(this.nodes, _options);
|
|
734
391
|
}
|
|
735
392
|
|
|
736
393
|
/**
|
|
@@ -739,62 +396,7 @@ class DAG {
|
|
|
739
396
|
* @throws {Error} If cycles are detected
|
|
740
397
|
*/
|
|
741
398
|
topoSort() {
|
|
742
|
-
|
|
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;
|
|
399
|
+
return topologicalSort(this.nodes);
|
|
798
400
|
}
|
|
799
401
|
|
|
800
402
|
/**
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Development Tools
|
|
2
|
+
|
|
3
|
+
This directory contains development tools for the RAG Pipeline Utils library.
|
|
4
|
+
|
|
5
|
+
## Available Tools
|
|
6
|
+
|
|
7
|
+
### Hot Reload Manager (`hot-reload.js`)
|
|
8
|
+
|
|
9
|
+
Provides automatic reloading of plugins and configuration files during development.
|
|
10
|
+
|
|
11
|
+
**Features:**
|
|
12
|
+
|
|
13
|
+
- File watching with chokidar
|
|
14
|
+
- Plugin hot-reloading with module cache clearing
|
|
15
|
+
- Configuration hot-reloading with validation
|
|
16
|
+
- State preservation and restoration
|
|
17
|
+
- Lifecycle hooks (beforeReload, afterReload, onError, etc.)
|
|
18
|
+
- Statistics tracking and history
|
|
19
|
+
- Error handling with notifications
|
|
20
|
+
|
|
21
|
+
**Usage:**
|
|
22
|
+
|
|
23
|
+
```javascript
|
|
24
|
+
const { createHotReloadManager } = require("./hot-reload");
|
|
25
|
+
|
|
26
|
+
const hotReload = createHotReloadManager({
|
|
27
|
+
watchPaths: ["./src/plugins"],
|
|
28
|
+
configPath: ".ragrc.json",
|
|
29
|
+
verbose: true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await hotReload.start();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Development Server (`dev-server.js`)
|
|
36
|
+
|
|
37
|
+
Integrated development server with hot reload support.
|
|
38
|
+
|
|
39
|
+
**Features:**
|
|
40
|
+
|
|
41
|
+
- Hot reload integration
|
|
42
|
+
- Real-time notifications
|
|
43
|
+
- Statistics dashboard
|
|
44
|
+
- Event-driven architecture
|
|
45
|
+
- Graceful shutdown
|
|
46
|
+
- Pipeline integration
|
|
47
|
+
|
|
48
|
+
**Usage:**
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
const { createDevServer } = require("./dev-server");
|
|
52
|
+
|
|
53
|
+
const devServer = createDevServer({
|
|
54
|
+
port: 3000,
|
|
55
|
+
watchPaths: ["./src/plugins"],
|
|
56
|
+
autoReload: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await devServer.start();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Examples
|
|
63
|
+
|
|
64
|
+
See `examples/hot-reload-usage.js` for comprehensive examples including:
|
|
65
|
+
|
|
66
|
+
1. Basic hot reload setup
|
|
67
|
+
2. Integration with plugin registry
|
|
68
|
+
3. Development server usage
|
|
69
|
+
4. Pipeline integration
|
|
70
|
+
5. Custom state preservation
|
|
71
|
+
6. Manual reload triggering
|
|
72
|
+
7. Configuration validation
|
|
73
|
+
8. Complete development workflows
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
Full documentation is available at `docs/hot-reload.md`.
|
|
78
|
+
|
|
79
|
+
## Testing
|
|
80
|
+
|
|
81
|
+
Tests are located in `__tests__/dev/hot-reload.test.js`.
|
|
82
|
+
|
|
83
|
+
Run tests:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm test -- __tests__/dev/hot-reload.test.js
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## API Reference
|
|
90
|
+
|
|
91
|
+
### Hot Reload Manager
|
|
92
|
+
|
|
93
|
+
- `start()` - Start watching for changes
|
|
94
|
+
- `stop()` - Stop watching
|
|
95
|
+
- `triggerReload(filePath, type)` - Manually trigger reload
|
|
96
|
+
- `registerHook(hookName, callback)` - Register lifecycle hook
|
|
97
|
+
- `getStats()` - Get reload statistics
|
|
98
|
+
- `getHistory()` - Get reload history
|
|
99
|
+
|
|
100
|
+
### Development Server
|
|
101
|
+
|
|
102
|
+
- `start()` - Start dev server
|
|
103
|
+
- `stop()` - Stop dev server
|
|
104
|
+
- `getStatus()` - Get server status
|
|
105
|
+
- `getStatistics()` - Get formatted statistics
|
|
106
|
+
- `triggerReload(filePath, type)` - Manually trigger reload
|
|
107
|
+
- `registerHook(hookName, callback)` - Register lifecycle hook
|
|
108
|
+
|
|
109
|
+
## Events
|
|
110
|
+
|
|
111
|
+
Both tools emit various events:
|
|
112
|
+
|
|
113
|
+
- `started` / `stopped`
|
|
114
|
+
- `plugin:reloaded` / `plugin:reload-failed`
|
|
115
|
+
- `config:reloaded` / `config:reload-failed`
|
|
116
|
+
- `plugin:added` / `plugin:removed`
|
|
117
|
+
- `error`
|
|
118
|
+
- `notification` (DevServer only)
|
|
119
|
+
|
|
120
|
+
## Dependencies
|
|
121
|
+
|
|
122
|
+
- `chokidar` - File system watching
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
GPL-3.0
|