@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.
- package/README.md +244 -11
- 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
|
@@ -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;
|