@devilsdev/rag-pipeline-utils 2.2.0 → 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 (46) hide show
  1. package/README.md +323 -722
  2. package/contracts/contract-schema.json +189 -0
  3. package/contracts/reranker-contract.json +69 -0
  4. package/dist/index.cjs +19 -0
  5. package/dist/index.mjs +15 -1
  6. package/package.json +4 -2
  7. package/src/cli/commands/wizard.js +627 -0
  8. package/src/cli/input-sanitization-middleware.js +356 -0
  9. package/src/cli/templates/development.template.json +64 -0
  10. package/src/cli/templates/minimal.template.json +33 -0
  11. package/src/cli/templates/production.template.json +104 -0
  12. package/src/cli/templates/testing.template.json +45 -0
  13. package/src/config/connection-pool-config.json +259 -0
  14. package/src/config/memory-optimization.js +414 -0
  15. package/src/config/redaction-patterns.json +150 -0
  16. package/src/config/sanitization-rules.json +323 -0
  17. package/src/core/performance/streaming-safeguards.js +10 -1
  18. package/src/core/plugin-registry.js +275 -0
  19. package/src/dag/core/error-context.js +391 -0
  20. package/src/dag/core/execution-scheduler.js +350 -0
  21. package/src/dag/core/topology-validation.js +415 -0
  22. package/src/dag/dag-engine.js +26 -424
  23. package/src/dev/README.md +126 -0
  24. package/src/dev/dev-server.js +356 -0
  25. package/src/dev/examples/hot-reload-usage.js +355 -0
  26. package/src/dev/hot-reload.js +807 -0
  27. package/src/enterprise/sso-integration.js +184 -15
  28. package/src/index.js +19 -0
  29. package/src/observability/event-logger.js +62 -2
  30. package/src/plugins/optimized-openai-embedder.js +169 -0
  31. package/src/reranker/llm-reranker.js +30 -4
  32. package/src/security/jwt-config.js +431 -0
  33. package/src/security/jwt-validator.js +701 -0
  34. package/src/utils/api-client-pool-integration.js +436 -0
  35. package/src/utils/batch-processor.js +710 -0
  36. package/src/utils/connection-pool.js +605 -0
  37. package/src/utils/contract-validator.js +460 -0
  38. package/src/utils/error-formatter.js +603 -0
  39. package/src/utils/examples/error-formatter-usage.js +351 -0
  40. package/src/utils/input-sanitizer.js +967 -0
  41. package/src/utils/memory-monitor.js +800 -0
  42. package/src/utils/rate-limiter.js +244 -0
  43. package/src/utils/secure-logger-config.js +352 -0
  44. package/src/utils/secure-logger.js +576 -0
  45. package/src/utils/secure-logging.js +338 -0
  46. package/src/utils/structured-logger.js +32 -5
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const Ajv = require('ajv');
5
6
  const {
6
7
  createPluginVerifier,
7
8
  } = require('../security/plugin-signature-verifier.js');
@@ -18,6 +19,40 @@ class PluginRegistry {
18
19
  'evaluator',
19
20
  ]);
20
21
  this._contracts = new Map();
22
+ this._contractWarningsShown = new Set(); // Track warnings to show only once per type
23
+ this._disableContractWarnings = options.disableContractWarnings || false;
24
+ this._validateContractSchema =
25
+ options.validateContractSchema !== undefined
26
+ ? options.validateContractSchema
27
+ : true; // Enable schema validation by default
28
+
29
+ // Initialize Ajv for contract schema validation
30
+ this._ajv = new Ajv({
31
+ allErrors: true,
32
+ verbose: true,
33
+ strict: false,
34
+ });
35
+
36
+ // Load contract schema
37
+ this._contractSchema = this._loadContractSchema();
38
+
39
+ // Compile schema validator once
40
+ this._contractSchemaValidator = null;
41
+ if (this._contractSchema) {
42
+ try {
43
+ this._contractSchemaValidator = this._ajv.compile(this._contractSchema);
44
+ } catch (error) {
45
+ if (
46
+ process.env.NODE_ENV !== 'production' &&
47
+ !this._disableContractWarnings
48
+ ) {
49
+ console.warn(
50
+ `[PLUGIN_REGISTRY] Warning: Failed to compile contract schema: ${error.message}`,
51
+ ); // eslint-disable-line no-console
52
+ }
53
+ this._contractSchema = null;
54
+ }
55
+ }
21
56
 
22
57
  // Environment-aware signature verification defaults
23
58
  const isDevelopment = process.env.NODE_ENV === 'development';
@@ -35,6 +70,190 @@ class PluginRegistry {
35
70
  this._loadContracts();
36
71
  }
37
72
 
73
+ /**
74
+ * Load contract schema for validation
75
+ *
76
+ * @private
77
+ * @returns {Object|null} Contract schema or null if not found
78
+ *
79
+ * @since 2.2.4
80
+ */
81
+ _loadContractSchema() {
82
+ try {
83
+ const schemaPath = path.join(
84
+ __dirname,
85
+ '../../contracts/contract-schema.json',
86
+ );
87
+ if (fs.existsSync(schemaPath)) {
88
+ const schemaContent = fs.readFileSync(schemaPath, 'utf8');
89
+ return JSON.parse(schemaContent);
90
+ }
91
+ } catch (error) {
92
+ if (
93
+ process.env.NODE_ENV !== 'production' &&
94
+ !this._disableContractWarnings
95
+ ) {
96
+ console.warn(
97
+ `[PLUGIN_REGISTRY] Warning: Failed to load contract schema: ${error.message}`,
98
+ ); // eslint-disable-line no-console
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Validate contract against schema
106
+ *
107
+ * @private
108
+ * @param {Object} contract - Contract to validate
109
+ * @param {string} type - Contract type
110
+ * @returns {Object} Validation result { valid: boolean, errors: Array }
111
+ *
112
+ * @since 2.2.4
113
+ */
114
+ _validateContractAgainstSchema(contract, type) {
115
+ if (!this._validateContractSchema || !this._contractSchemaValidator) {
116
+ return { valid: true, errors: [] };
117
+ }
118
+
119
+ try {
120
+ const valid = this._contractSchemaValidator(contract);
121
+
122
+ if (!valid) {
123
+ return {
124
+ valid: false,
125
+ errors: this._contractSchemaValidator.errors || [],
126
+ };
127
+ }
128
+
129
+ return { valid: true, errors: [] };
130
+ } catch (error) {
131
+ return {
132
+ valid: false,
133
+ errors: [
134
+ {
135
+ message: `Schema validation error: ${error.message}`,
136
+ },
137
+ ],
138
+ };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Format schema validation errors for display
144
+ *
145
+ * @private
146
+ * @param {Array} errors - Ajv validation errors
147
+ * @returns {string} Formatted error message
148
+ *
149
+ * @since 2.2.4
150
+ */
151
+ _formatSchemaErrors(errors) {
152
+ if (!errors || errors.length === 0) {
153
+ return 'Unknown schema validation error';
154
+ }
155
+
156
+ const messages = errors.map((err) => {
157
+ const path = err.instancePath || err.dataPath || '/';
158
+ const message = err.message || 'validation failed';
159
+
160
+ if (err.params) {
161
+ const params = Object.entries(err.params)
162
+ .map(([key, value]) => `${key}=${JSON.stringify(value)}`)
163
+ .join(', ');
164
+ return ` - ${path}: ${message} (${params})`;
165
+ }
166
+
167
+ return ` - ${path}: ${message}`;
168
+ });
169
+
170
+ return messages.join('\n');
171
+ }
172
+
173
+ /**
174
+ * Check if a contract exists for the given plugin type
175
+ *
176
+ * @param {string} type - Plugin type (loader, embedder, etc.)
177
+ * @returns {boolean} True if contract exists, false otherwise
178
+ *
179
+ * @example
180
+ * const hasContract = registry.checkContractExists('loader');
181
+ * if (!hasContract) {
182
+ * console.warn('No contract for loader plugins');
183
+ * }
184
+ *
185
+ * @since 2.2.4
186
+ */
187
+ checkContractExists(type) {
188
+ return this._contracts.has(type);
189
+ }
190
+
191
+ /**
192
+ * Log one-time warning for missing contract
193
+ *
194
+ * Logs a helpful warning message about the benefits of contracts
195
+ * and how to add them. Only logs once per contract type and only
196
+ * in non-production environments.
197
+ *
198
+ * @private
199
+ * @param {string} type - Plugin type
200
+ * @param {string} context - Context where warning is generated (e.g., 'load', 'register')
201
+ *
202
+ * @since 2.2.4
203
+ */
204
+ _warnMissingContract(type, context = 'load') {
205
+ // Skip if warnings disabled or in production
206
+ if (
207
+ this._disableContractWarnings ||
208
+ process.env.NODE_ENV === 'production'
209
+ ) {
210
+ return;
211
+ }
212
+
213
+ // Only warn once per type
214
+ const warningKey = `${type}:${context}`;
215
+ if (this._contractWarningsShown.has(warningKey)) {
216
+ return;
217
+ }
218
+
219
+ this._contractWarningsShown.add(warningKey);
220
+
221
+ console.warn(
222
+ `
223
+ ╔════════════════════════════════════════════════════════════════════════════╗
224
+ ║ [PLUGIN_REGISTRY] Missing Contract for '${type}' plugins ║
225
+ ╠════════════════════════════════════════════════════════════════════════════╣
226
+ ║ A contract specification was not found for the '${type}' plugin type. ║
227
+ ║ ║
228
+ ║ Benefits of using contracts: ║
229
+ ║ • Ensures plugin compatibility across versions ║
230
+ ║ • Validates required methods and properties ║
231
+ ║ • Provides clear interface documentation ║
232
+ ║ • Catches errors early in development ║
233
+ ║ • Enables automated testing and validation ║
234
+ ║ ║
235
+ ║ To add a contract: ║
236
+ ║ 1. Create contracts/${type}-contract.json ║
237
+ ║ 2. Define required methods and their signatures ║
238
+ ║ 3. Run contract validation tests ║
239
+ ║ ║
240
+ ║ Example contract structure: ║
241
+ ║ { ║
242
+ ║ "type": "${type}", ║
243
+ ║ "version": "1.0.0", ║
244
+ ║ "required": ["methodName"], ║
245
+ ║ "properties": { /* method definitions */ } ║
246
+ ║ } ║
247
+ ║ ║
248
+ ║ See: docs/error-context-guide.md and __tests__/contracts/README.md ║
249
+ ║ ║
250
+ ║ To disable these warnings: ║
251
+ ║ new PluginRegistry({ disableContractWarnings: true }) ║
252
+ ╚════════════════════════════════════════════════════════════════════════════╝
253
+ `.trim(),
254
+ ); // eslint-disable-line no-console
255
+ }
256
+
38
257
  _loadContracts() {
39
258
  const contractsDir = path.join(__dirname, '../../contracts');
40
259
 
@@ -44,6 +263,7 @@ class PluginRegistry {
44
263
  embedder: 'embedder-contract.json',
45
264
  retriever: 'retriever-contract.json',
46
265
  llm: 'llm-contract.json',
266
+ reranker: 'reranker-contract.json',
47
267
  };
48
268
 
49
269
  for (const [type, filename] of Object.entries(contractFiles)) {
@@ -52,20 +272,75 @@ class PluginRegistry {
52
272
  if (fs.existsSync(contractPath)) {
53
273
  const contractContent = fs.readFileSync(contractPath, 'utf8');
54
274
  const contract = JSON.parse(contractContent);
275
+
276
+ // Validate contract against schema
277
+ const validation = this._validateContractAgainstSchema(
278
+ contract,
279
+ type,
280
+ );
281
+
282
+ if (!validation.valid) {
283
+ const errorMessage = this._formatSchemaErrors(validation.errors);
284
+ const error = new Error(
285
+ `Contract schema validation failed for '${type}':\n${errorMessage}`,
286
+ );
287
+ error.validationErrors = validation.errors;
288
+ error.contractType = type;
289
+
290
+ if (process.env.NODE_ENV === 'production') {
291
+ // In production, log error but continue (fail open)
292
+ console.error(`[PLUGIN_REGISTRY] ERROR: ${error.message}`); // eslint-disable-line no-console
293
+ } else {
294
+ // In development, throw to fail fast
295
+ throw error;
296
+ }
297
+ }
298
+
55
299
  this._contracts.set(type, contract);
300
+ } else {
301
+ // Use new warning method for missing contracts
302
+ this._warnMissingContract(type, 'load');
56
303
  }
57
304
  } catch (error) {
305
+ // Re-throw validation errors in development mode
306
+ if (error.validationErrors && process.env.NODE_ENV !== 'production') {
307
+ throw error;
308
+ }
309
+
58
310
  // Contract loading is optional - don't fail if missing
311
+ if (
312
+ process.env.NODE_ENV !== 'production' &&
313
+ !this._disableContractWarnings
314
+ ) {
315
+ console.warn(
316
+ `[PLUGIN_REGISTRY] Warning: Failed to load contract for '${type}': ${error.message}`,
317
+ ); // eslint-disable-line no-console
318
+ }
59
319
  }
60
320
  }
61
321
  } catch (error) {
322
+ // Re-throw validation errors in development mode
323
+ if (error.validationErrors && process.env.NODE_ENV !== 'production') {
324
+ throw error;
325
+ }
326
+
62
327
  // Contracts directory may not exist in all environments
328
+ if (
329
+ process.env.NODE_ENV !== 'production' &&
330
+ !this._disableContractWarnings
331
+ ) {
332
+ console.warn(
333
+ `[PLUGIN_REGISTRY] Warning: Contracts directory not accessible: ${error.message}`,
334
+ ); // eslint-disable-line no-console
335
+ }
63
336
  }
64
337
  }
65
338
 
66
339
  _validatePluginContract(category, impl) {
67
340
  const contract = this._contracts.get(category);
68
341
  if (!contract) {
342
+ // Use new warning method when registering a plugin without a contract
343
+ this._warnMissingContract(category, 'register');
69
344
  return; // No contract to validate against
70
345
  }
71
346
 
@@ -0,0 +1,391 @@
1
+ /**
2
+ * @fileoverview Error Context Management Module
3
+ *
4
+ * Provides centralized error handling, context management, and error reporting
5
+ * for DAG execution. Handles error creation, aggregation, preservation of error
6
+ * properties, and serialization for debugging and monitoring.
7
+ *
8
+ * @module dag/core/error-context
9
+ * @author Ali Kahwaji
10
+ * @since 2.2.4
11
+ * @version 1.0.0
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ /**
17
+ * ErrorContext class for managing DAG execution errors
18
+ *
19
+ * Provides comprehensive error management with:
20
+ * - Rich error context creation with node metadata
21
+ * - Error aggregation for multiple failures
22
+ * - Property preservation across error transformations
23
+ * - Error serialization for logging and monitoring
24
+ * - Cycle path tracking and reporting
25
+ * - Downstream impact analysis
26
+ *
27
+ * @class
28
+ * @since 2.2.4
29
+ *
30
+ * @example
31
+ * const errorContext = new ErrorContext();
32
+ *
33
+ * // Create error with context
34
+ * const nodeError = errorContext.createErrorContext('nodeA', new Error('Failed'), {
35
+ * downstream: ['nodeB', 'nodeC'],
36
+ * timestamp: Date.now()
37
+ * });
38
+ *
39
+ * // Aggregate multiple errors
40
+ * const errorsMap = new Map([['nodeA', error1], ['nodeB', error2]]);
41
+ * const aggregated = errorContext.aggregateErrors(errorsMap);
42
+ *
43
+ * // Serialize for logging
44
+ * console.log(errorContext.serializeError(nodeError));
45
+ */
46
+ class ErrorContext {
47
+ /**
48
+ * Create an ErrorContext instance
49
+ *
50
+ * @constructor
51
+ */
52
+ constructor() {
53
+ // Reserved for future state management if needed
54
+ }
55
+
56
+ /**
57
+ * Create error context with node information and downstream impact
58
+ *
59
+ * Creates a new Error object enriched with:
60
+ * - Node ID for tracing execution path
61
+ * - Timestamp for temporal analysis
62
+ * - Original error as cause (for stack trace preservation)
63
+ * - Downstream node impact message
64
+ *
65
+ * @param {string} nodeId - ID of the node that failed
66
+ * @param {Error} error - Original error that occurred
67
+ * @param {Object} [options={}] - Additional context options
68
+ * @param {string[]} [options.downstream=[]] - Array of downstream node IDs affected
69
+ * @param {number} [options.timestamp=Date.now()] - Error timestamp
70
+ * @returns {Error} Enhanced error object with context
71
+ *
72
+ * @example
73
+ * const error = errorContext.createErrorContext(
74
+ * 'processData',
75
+ * new Error('Invalid input'),
76
+ * {
77
+ * downstream: ['saveResults', 'sendNotification'],
78
+ * timestamp: Date.now()
79
+ * }
80
+ * );
81
+ * // Error message: "Node processData execution failed: Invalid input.
82
+ * // This affects downstream nodes: saveResults, sendNotification"
83
+ * // error.nodeId === 'processData'
84
+ * // error.cause === original Error object
85
+ *
86
+ * @since 2.2.4
87
+ */
88
+ createErrorContext(nodeId, error, options = {}) {
89
+ const { downstream = [], timestamp = Date.now() } = options;
90
+
91
+ const suffix = downstream.length
92
+ ? `. This affects downstream nodes: ${downstream.join(', ')}`
93
+ : '';
94
+
95
+ const nodeErr = new Error(
96
+ `Node ${nodeId} execution failed: ${error.message}${suffix}`,
97
+ );
98
+ nodeErr.nodeId = nodeId;
99
+ nodeErr.timestamp = timestamp;
100
+ nodeErr.cause = error;
101
+
102
+ return nodeErr;
103
+ }
104
+
105
+ /**
106
+ * Aggregate multiple errors into a single error object
107
+ *
108
+ * Creates a consolidated error when multiple nodes fail during execution.
109
+ * The aggregated error maintains references to all original errors for
110
+ * comprehensive debugging.
111
+ *
112
+ * @param {Map<string, Error>} errorsMap - Map of node ID to error object
113
+ * @returns {Error|null} Aggregated error, single error, or null if no errors
114
+ *
115
+ * @example
116
+ * const errors = new Map([
117
+ * ['nodeA', new Error('A failed')],
118
+ * ['nodeB', new Error('B failed')]
119
+ * ]);
120
+ * const aggregated = errorContext.aggregateErrors(errors);
121
+ * // aggregated.message === 'Multiple execution errors'
122
+ * // aggregated.errors === [Error('A failed'), Error('B failed')]
123
+ *
124
+ * @since 2.2.4
125
+ */
126
+ aggregateErrors(errorsMap) {
127
+ if (errorsMap.size >= 2) {
128
+ const agg = new Error('Multiple execution errors');
129
+ agg.errors = [...errorsMap.values()].map((x) => x.cause || x);
130
+ return agg;
131
+ }
132
+ if (errorsMap.size === 1) {
133
+ return [...errorsMap.values()][0];
134
+ }
135
+ return null;
136
+ }
137
+
138
+ /**
139
+ * Preserve node context when wrapping or transforming errors
140
+ *
141
+ * Ensures all error metadata (nodeId, timestamp, cause, cycle, errors)
142
+ * is preserved when creating wrapped errors. This is critical for
143
+ * maintaining debugging information through error propagation.
144
+ *
145
+ * @param {Error} error - Original error to preserve
146
+ * @param {Object} [context={}] - Additional context to add
147
+ * @returns {Error} New error with preserved properties
148
+ *
149
+ * @example
150
+ * const originalError = new Error('Original');
151
+ * originalError.nodeId = 'nodeA';
152
+ * originalError.timestamp = Date.now();
153
+ *
154
+ * const wrapped = errorContext.preserveNodeContext(originalError, {
155
+ * retryCount: 3
156
+ * });
157
+ * // wrapped.nodeId === 'nodeA'
158
+ * // wrapped.timestamp === original timestamp
159
+ * // wrapped.retryCount === 3
160
+ *
161
+ * @since 2.2.4
162
+ */
163
+ preserveNodeContext(error, context = {}) {
164
+ const wrapped = new Error(error.message);
165
+
166
+ // Preserve standard error properties
167
+ if (error.nodeId !== undefined) wrapped.nodeId = error.nodeId;
168
+ if (error.timestamp !== undefined) wrapped.timestamp = error.timestamp;
169
+ if (error.cause !== undefined) wrapped.cause = error.cause;
170
+ if (error.cycle !== undefined) wrapped.cycle = error.cycle;
171
+ if (error.errors !== undefined) wrapped.errors = error.errors;
172
+
173
+ // Add any additional context
174
+ for (const [key, value] of Object.entries(context)) {
175
+ if (!(key in wrapped)) {
176
+ wrapped[key] = value;
177
+ }
178
+ }
179
+
180
+ return wrapped;
181
+ }
182
+
183
+ /**
184
+ * Format error report for debugging and monitoring
185
+ *
186
+ * Creates a structured representation of an error suitable for logging,
187
+ * monitoring systems, or debugging tools. Includes all metadata and
188
+ * nested error information.
189
+ *
190
+ * @param {Error} error - Error to format
191
+ * @returns {Object} Structured error report
192
+ *
193
+ * @example
194
+ * const report = errorContext.formatErrorReport(nodeError);
195
+ * // {
196
+ * // message: "Node A execution failed: ...",
197
+ * // nodeId: "A",
198
+ * // timestamp: 1234567890,
199
+ * // cause: { message: "...", stack: "..." },
200
+ * // cycle: null,
201
+ * // errors: null
202
+ * // }
203
+ *
204
+ * @since 2.2.4
205
+ */
206
+ formatErrorReport(error) {
207
+ const report = {
208
+ message: error.message,
209
+ nodeId: error.nodeId || null,
210
+ timestamp: error.timestamp || null,
211
+ cause: error.cause
212
+ ? {
213
+ message: error.cause.message,
214
+ stack: error.cause.stack,
215
+ }
216
+ : null,
217
+ cycle: error.cycle || null,
218
+ errors: error.errors
219
+ ? error.errors.map((e) => ({
220
+ message: e.message,
221
+ stack: e.stack,
222
+ }))
223
+ : null,
224
+ };
225
+
226
+ return report;
227
+ }
228
+
229
+ /**
230
+ * Serialize error for logging and monitoring systems
231
+ *
232
+ * Converts error to JSON string with formatting for readability.
233
+ * Safe for logging systems that require string representation.
234
+ *
235
+ * @param {Error} error - Error to serialize
236
+ * @param {number} [indent=2] - JSON indentation spaces
237
+ * @returns {string} JSON string representation of error
238
+ *
239
+ * @example
240
+ * const json = errorContext.serializeError(nodeError);
241
+ * console.log(json);
242
+ * // {
243
+ * // "message": "Node A execution failed: ...",
244
+ * // "nodeId": "A",
245
+ * // "timestamp": 1234567890,
246
+ * // ...
247
+ * // }
248
+ *
249
+ * @since 2.2.4
250
+ */
251
+ serializeError(error, indent = 2) {
252
+ return JSON.stringify(this.formatErrorReport(error), null, indent);
253
+ }
254
+
255
+ /**
256
+ * Wrap execution errors with appropriate context
257
+ *
258
+ * Implements error wrapping policy:
259
+ * - Node errors (with nodeId or starting with "Node ") are rethrown unchanged
260
+ * - Validation/cycle errors are wrapped once with "DAG execution failed"
261
+ * - Timeout and "no sink nodes" errors are not wrapped
262
+ * - All other errors are wrapped with "DAG execution failed"
263
+ *
264
+ * All error properties are preserved during wrapping.
265
+ *
266
+ * @param {Error} error - Error to wrap
267
+ * @returns {Error} Appropriately wrapped error
268
+ *
269
+ * @example
270
+ * const nodeError = new Error('Node A failed');
271
+ * nodeError.nodeId = 'A';
272
+ * const wrapped = errorContext.wrapExecutionError(nodeError);
273
+ * // Returns same error unchanged (node error policy)
274
+ *
275
+ * @example
276
+ * const validationError = new Error('DAG validation failed: cycle detected');
277
+ * validationError.cycle = ['A', 'B', 'A'];
278
+ * const wrapped = errorContext.wrapExecutionError(validationError);
279
+ * // Returns: Error('DAG execution failed: DAG validation failed: cycle detected')
280
+ * // with cycle property preserved
281
+ *
282
+ * @since 2.2.4
283
+ */
284
+ wrapExecutionError(error) {
285
+ // Node errors - rethrow unchanged
286
+ if (error.message.startsWith('Node ') || error.nodeId) {
287
+ return error;
288
+ }
289
+
290
+ // Validation or aggregate errors - wrap once
291
+ if (
292
+ error.cycle ||
293
+ /^DAG validation failed/.test(error.message) ||
294
+ error.errors
295
+ ) {
296
+ const wrappedError = new Error(`DAG execution failed: ${error.message}`);
297
+ if (error.cycle) wrappedError.cycle = error.cycle;
298
+ if (error.errors) wrappedError.errors = error.errors;
299
+ if (error.nodeId) wrappedError.nodeId = error.nodeId;
300
+ if (error.timestamp) wrappedError.timestamp = error.timestamp;
301
+ return wrappedError;
302
+ }
303
+
304
+ // Timeout and "no sink nodes" errors - don't wrap
305
+ if (
306
+ error.message === 'Execution timeout' ||
307
+ error.message === 'DAG has no sink nodes - no final output available'
308
+ ) {
309
+ return error;
310
+ }
311
+
312
+ // All other errors - wrap with context preservation
313
+ const wrappedError = new Error(`DAG execution failed: ${error.message}`);
314
+ if (error.errors) wrappedError.errors = error.errors;
315
+ if (error.nodeId) wrappedError.nodeId = error.nodeId;
316
+ if (error.timestamp) wrappedError.timestamp = error.timestamp;
317
+ if (error.cycle) wrappedError.cycle = error.cycle;
318
+ return wrappedError;
319
+ }
320
+
321
+ /**
322
+ * Create cycle error with path information
323
+ *
324
+ * Creates a specialized error for cycle detection with the cycle path
325
+ * embedded for debugging and error reporting.
326
+ *
327
+ * @param {string[]} cyclePath - Array of node IDs forming the cycle
328
+ * @returns {Error} Cycle error with path information
329
+ *
330
+ * @example
331
+ * const cycleError = errorContext.createCycleError(['A', 'B', 'C', 'A']);
332
+ * // cycleError.message === 'Cycle detected involving node: A -> B -> C -> A'
333
+ * // cycleError.cycle === ['A', 'B', 'C', 'A']
334
+ *
335
+ * @since 2.2.4
336
+ */
337
+ createCycleError(cyclePath) {
338
+ const error = new Error(
339
+ `Cycle detected involving node: ${cyclePath.join(' -> ')}`,
340
+ );
341
+ error.cycle = cyclePath;
342
+ return error;
343
+ }
344
+
345
+ /**
346
+ * Check if error should halt execution
347
+ *
348
+ * Determines if an error is critical enough to stop DAG execution
349
+ * based on error type and execution options.
350
+ *
351
+ * @param {Error} error - Error to evaluate
352
+ * @param {Object} options - Execution options
353
+ * @param {boolean} [options.continueOnError=false] - Continue on non-critical errors
354
+ * @param {boolean} [options.isNonCritical=false] - Whether node is non-critical
355
+ * @returns {boolean} True if execution should halt
356
+ *
357
+ * @example
358
+ * const shouldHalt = errorContext.shouldHaltExecution(error, {
359
+ * continueOnError: true,
360
+ * isNonCritical: true
361
+ * });
362
+ * // Returns false - execution continues
363
+ *
364
+ * @since 2.2.4
365
+ */
366
+ shouldHaltExecution(error, options = {}) {
367
+ const { continueOnError = false, isNonCritical = false } = options;
368
+
369
+ // Always halt on validation/cycle errors
370
+ if (error.cycle || /^DAG validation failed/.test(error.message)) {
371
+ return true;
372
+ }
373
+
374
+ // Halt on critical node errors
375
+ if (error.nodeId && !isNonCritical && !continueOnError) {
376
+ return true;
377
+ }
378
+
379
+ // Halt on timeout
380
+ if (error.message === 'Execution timeout') {
381
+ return true;
382
+ }
383
+
384
+ return false;
385
+ }
386
+ }
387
+
388
+ module.exports = ErrorContext;
389
+
390
+ // Default export for convenience
391
+ module.exports.default = ErrorContext;