@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.
- package/README.md +323 -722
- package/contracts/contract-schema.json +189 -0
- package/contracts/reranker-contract.json +69 -0
- package/dist/index.cjs +19 -0
- package/dist/index.mjs +15 -1
- package/package.json +4 -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/performance/streaming-safeguards.js +10 -1
- package/src/core/plugin-registry.js +275 -0
- 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 +184 -15
- 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/reranker/llm-reranker.js +30 -4
- 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/rate-limiter.js +244 -0
- package/src/utils/secure-logger-config.js +352 -0
- package/src/utils/secure-logger.js +576 -0
- package/src/utils/secure-logging.js +338 -0
- 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;
|