@paulduvall/claude-dev-toolkit 0.0.1-alpha.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.
@@ -0,0 +1,563 @@
1
+ /**
2
+ * Error Recovery System
3
+ *
4
+ * Comprehensive error recovery and retry mechanism for resilient operation handling.
5
+ * Provides automated error recovery strategies and context-aware retry logic.
6
+ *
7
+ * Features:
8
+ * - Intelligent retry strategies with backoff
9
+ * - Context-aware recovery actions
10
+ * - Fallback mechanism support
11
+ * - Recovery metrics and monitoring
12
+ * - Integration with error handling workflow
13
+ */
14
+
15
+ const ErrorHandlerUtils = require('./error-handler-utils');
16
+ const ErrorFactory = require('./error-factory');
17
+
18
+ class ErrorRecoverySystem {
19
+ constructor() {
20
+ this.errorHandler = new ErrorHandlerUtils();
21
+ this.errorFactory = new ErrorFactory();
22
+
23
+ this.config = {
24
+ defaultRetryConfig: {
25
+ maxAttempts: 3,
26
+ baseDelay: 1000,
27
+ maxDelay: 30000,
28
+ backoffMultiplier: 2,
29
+ jitter: true
30
+ },
31
+ recoveryStrategies: this._initializeRecoveryStrategies(),
32
+ recoverableErrorCodes: new Set([
33
+ 'EACCES', 'EPERM', 'ENOENT', 'TIMEOUT', 'ECONNREFUSED',
34
+ 'NOT_FOUND', 'VERSION_MISMATCH', 'VALIDATION_ERROR'
35
+ ])
36
+ };
37
+
38
+ this.metrics = {
39
+ attempts: new Map(),
40
+ successes: new Map(),
41
+ failures: new Map(),
42
+ recoveries: new Map()
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Initialize recovery strategies
48
+ * @returns {Object} Recovery strategies by error category
49
+ * @private
50
+ */
51
+ _initializeRecoveryStrategies() {
52
+ return {
53
+ permission: {
54
+ strategies: [
55
+ 'check_permissions',
56
+ 'attempt_elevation',
57
+ 'try_alternative_location',
58
+ 'request_manual_intervention'
59
+ ],
60
+ autoRecoverable: true,
61
+ estimatedTime: 30000, // 30 seconds
62
+ priority: 'high'
63
+ },
64
+
65
+ dependency: {
66
+ strategies: [
67
+ 'check_package_manager',
68
+ 'update_package_lists',
69
+ 'install_missing_dependency',
70
+ 'try_alternative_package_manager',
71
+ 'manual_installation_guide'
72
+ ],
73
+ autoRecoverable: true,
74
+ estimatedTime: 120000, // 2 minutes
75
+ priority: 'high'
76
+ },
77
+
78
+ network: {
79
+ strategies: [
80
+ 'check_connectivity',
81
+ 'retry_with_backoff',
82
+ 'try_alternative_endpoint',
83
+ 'use_cached_data',
84
+ 'offline_mode'
85
+ ],
86
+ autoRecoverable: true,
87
+ estimatedTime: 60000, // 1 minute
88
+ priority: 'medium'
89
+ },
90
+
91
+ validation: {
92
+ strategies: [
93
+ 'analyze_validation_failure',
94
+ 'apply_automatic_fixes',
95
+ 'use_default_values',
96
+ 'request_user_correction',
97
+ 'skip_validation'
98
+ ],
99
+ autoRecoverable: true,
100
+ estimatedTime: 15000, // 15 seconds
101
+ priority: 'medium'
102
+ },
103
+
104
+ system: {
105
+ strategies: [
106
+ 'check_system_resources',
107
+ 'cleanup_temporary_files',
108
+ 'restart_services',
109
+ 'escalate_to_administrator'
110
+ ],
111
+ autoRecoverable: false,
112
+ estimatedTime: 300000, // 5 minutes
113
+ priority: 'critical'
114
+ }
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Wrap function with comprehensive error recovery
120
+ * @param {Function} fn - Function to wrap
121
+ * @param {Object} options - Recovery options
122
+ * @returns {Function} Wrapped function with recovery
123
+ */
124
+ wrapWithRecovery(fn, options = {}) {
125
+ const {
126
+ retryConfig = this.config.defaultRetryConfig,
127
+ context = {},
128
+ fallbacks = [],
129
+ enableAutoRecovery = true,
130
+ recordMetrics = true
131
+ } = options;
132
+
133
+ return async (...args) => {
134
+ const operationId = this._generateOperationId();
135
+ const startTime = Date.now();
136
+
137
+ if (recordMetrics) {
138
+ this._recordAttempt(operationId, context);
139
+ }
140
+
141
+ let lastError = null;
142
+ let recoveryAttempted = false;
143
+
144
+ for (let attempt = 1; attempt <= retryConfig.maxAttempts; attempt++) {
145
+ try {
146
+ const result = await fn(...args);
147
+
148
+ if (recordMetrics) {
149
+ this._recordSuccess(operationId, attempt, Date.now() - startTime);
150
+ }
151
+
152
+ return result;
153
+
154
+ } catch (error) {
155
+ lastError = error;
156
+ const enhancedError = this.errorHandler.createEnhancedError(error, {
157
+ ...context,
158
+ attempt,
159
+ operationId
160
+ });
161
+
162
+ // Log the error
163
+ this.errorHandler.logError(enhancedError, {
164
+ attempt,
165
+ maxAttempts: retryConfig.maxAttempts,
166
+ operationId
167
+ });
168
+
169
+ // Check if error is recoverable
170
+ if (!this._isRecoverable(enhancedError)) {
171
+ break;
172
+ }
173
+
174
+ // Attempt automatic recovery if enabled and not already attempted
175
+ if (enableAutoRecovery && !recoveryAttempted && this._shouldAttemptRecovery(enhancedError)) {
176
+ const recoveryResult = await this.attemptRecovery(enhancedError, context);
177
+ recoveryAttempted = true;
178
+
179
+ if (recoveryResult.success) {
180
+ // Retry immediately after successful recovery
181
+ continue;
182
+ }
183
+ }
184
+
185
+ // Don't retry if this is the last attempt
186
+ if (attempt === retryConfig.maxAttempts) {
187
+ break;
188
+ }
189
+
190
+ // Calculate delay with backoff and jitter
191
+ const delay = this._calculateDelay(attempt, retryConfig);
192
+ await this._sleep(delay);
193
+ }
194
+ }
195
+
196
+ // Try fallback functions
197
+ for (const fallback of fallbacks) {
198
+ try {
199
+ const fallbackResult = await fallback(lastError, ...args);
200
+
201
+ if (recordMetrics) {
202
+ this._recordSuccess(operationId, 'fallback', Date.now() - startTime);
203
+ }
204
+
205
+ return fallbackResult;
206
+
207
+ } catch (fallbackError) {
208
+ this.errorHandler.logError(
209
+ this.errorHandler.createEnhancedError(fallbackError, {
210
+ ...context,
211
+ fallback: true,
212
+ operationId
213
+ })
214
+ );
215
+ }
216
+ }
217
+
218
+ // Record final failure
219
+ if (recordMetrics) {
220
+ this._recordFailure(operationId, lastError, Date.now() - startTime);
221
+ }
222
+
223
+ throw lastError;
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Attempt automatic error recovery
229
+ * @param {Object} enhancedError - Enhanced error object
230
+ * @param {Object} context - Recovery context
231
+ * @returns {Promise<Object>} Recovery result
232
+ */
233
+ async attemptRecovery(enhancedError, context = {}) {
234
+ const strategy = this._getRecoveryStrategy(enhancedError);
235
+ if (!strategy) {
236
+ return { success: false, reason: 'No recovery strategy available' };
237
+ }
238
+
239
+ const recoveryId = this._generateRecoveryId();
240
+ const startTime = Date.now();
241
+
242
+ try {
243
+ const result = await this._executeRecoveryStrategy(enhancedError, strategy, {
244
+ ...context,
245
+ recoveryId
246
+ });
247
+
248
+ this._recordRecovery(recoveryId, enhancedError, result, Date.now() - startTime);
249
+
250
+ return result;
251
+
252
+ } catch (recoveryError) {
253
+ const failureResult = {
254
+ success: false,
255
+ error: recoveryError,
256
+ reason: 'Recovery strategy failed'
257
+ };
258
+
259
+ this._recordRecovery(recoveryId, enhancedError, failureResult, Date.now() - startTime);
260
+
261
+ return failureResult;
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Execute specific recovery strategy
267
+ * @param {Object} enhancedError - Enhanced error object
268
+ * @param {Object} strategy - Recovery strategy
269
+ * @param {Object} context - Recovery context
270
+ * @returns {Promise<Object>} Strategy execution result
271
+ * @private
272
+ */
273
+ async _executeRecoveryStrategy(enhancedError, strategy, context) {
274
+ const result = {
275
+ success: false,
276
+ actions: [],
277
+ duration: 0,
278
+ strategy: strategy
279
+ };
280
+
281
+ const startTime = Date.now();
282
+
283
+ for (const strategyStep of strategy.strategies) {
284
+ const stepResult = await this._executeRecoveryStep(strategyStep, enhancedError, context);
285
+
286
+ result.actions.push({
287
+ step: strategyStep,
288
+ result: stepResult,
289
+ timestamp: new Date().toISOString()
290
+ });
291
+
292
+ // If step succeeds, we can potentially stop
293
+ if (stepResult.success && stepResult.recoveryComplete) {
294
+ result.success = true;
295
+ break;
296
+ }
297
+
298
+ // If step indicates fatal failure, stop recovery
299
+ if (stepResult.fatal) {
300
+ result.reason = stepResult.reason || 'Fatal error during recovery';
301
+ break;
302
+ }
303
+ }
304
+
305
+ result.duration = Date.now() - startTime;
306
+
307
+ return result;
308
+ }
309
+
310
+ /**
311
+ * Execute individual recovery step
312
+ * @param {string} step - Recovery step name
313
+ * @param {Object} enhancedError - Enhanced error object
314
+ * @param {Object} context - Recovery context
315
+ * @returns {Promise<Object>} Step execution result
316
+ * @private
317
+ */
318
+ async _executeRecoveryStep(step, enhancedError, context) {
319
+ const stepResult = {
320
+ success: false,
321
+ recoveryComplete: false,
322
+ fatal: false,
323
+ details: {},
324
+ reason: null
325
+ };
326
+
327
+ try {
328
+ switch (step) {
329
+ case 'check_permissions':
330
+ stepResult.success = await this._checkPermissions(enhancedError.path);
331
+ stepResult.details.permissions = 'checked';
332
+ break;
333
+
334
+ case 'attempt_elevation':
335
+ stepResult.success = await this._attemptElevation(context);
336
+ stepResult.details.elevation = 'attempted';
337
+ break;
338
+
339
+ case 'check_connectivity':
340
+ stepResult.success = await this._checkNetworkConnectivity();
341
+ stepResult.details.connectivity = 'checked';
342
+ break;
343
+
344
+ case 'retry_with_backoff':
345
+ // This is handled by the main retry logic
346
+ stepResult.success = true;
347
+ stepResult.details.backoff = 'applied';
348
+ break;
349
+
350
+ case 'check_package_manager':
351
+ stepResult.success = await this._checkPackageManager();
352
+ stepResult.details.packageManager = 'checked';
353
+ break;
354
+
355
+ case 'analyze_validation_failure':
356
+ stepResult.success = await this._analyzeValidationFailure(enhancedError);
357
+ stepResult.details.analysis = 'completed';
358
+ break;
359
+
360
+ case 'use_default_values':
361
+ stepResult.success = true;
362
+ stepResult.recoveryComplete = true;
363
+ stepResult.details.defaults = 'applied';
364
+ break;
365
+
366
+ default:
367
+ stepResult.success = false;
368
+ stepResult.reason = `Unknown recovery step: ${step}`;
369
+ }
370
+
371
+ } catch (stepError) {
372
+ stepResult.success = false;
373
+ stepResult.fatal = this._isStepFatal(stepError);
374
+ stepResult.reason = stepError.message;
375
+ }
376
+
377
+ return stepResult;
378
+ }
379
+
380
+ /**
381
+ * Check if error is recoverable
382
+ * @param {Object} enhancedError - Enhanced error object
383
+ * @returns {boolean} True if recoverable
384
+ * @private
385
+ */
386
+ _isRecoverable(enhancedError) {
387
+ return this.config.recoverableErrorCodes.has(enhancedError.code) ||
388
+ enhancedError.recoverable === true;
389
+ }
390
+
391
+ /**
392
+ * Check if automatic recovery should be attempted
393
+ * @param {Object} enhancedError - Enhanced error object
394
+ * @returns {boolean} True if recovery should be attempted
395
+ * @private
396
+ */
397
+ _shouldAttemptRecovery(enhancedError) {
398
+ const strategy = this._getRecoveryStrategy(enhancedError);
399
+ return strategy && strategy.autoRecoverable;
400
+ }
401
+
402
+ /**
403
+ * Get recovery strategy for error
404
+ * @param {Object} enhancedError - Enhanced error object
405
+ * @returns {Object|null} Recovery strategy or null
406
+ * @private
407
+ */
408
+ _getRecoveryStrategy(enhancedError) {
409
+ return this.config.recoveryStrategies[enhancedError.category] || null;
410
+ }
411
+
412
+ /**
413
+ * Calculate retry delay with backoff and jitter
414
+ * @param {number} attempt - Current attempt number
415
+ * @param {Object} retryConfig - Retry configuration
416
+ * @returns {number} Delay in milliseconds
417
+ * @private
418
+ */
419
+ _calculateDelay(attempt, retryConfig) {
420
+ let delay = retryConfig.baseDelay * Math.pow(retryConfig.backoffMultiplier, attempt - 1);
421
+
422
+ // Apply maximum delay limit
423
+ delay = Math.min(delay, retryConfig.maxDelay);
424
+
425
+ // Add jitter if enabled
426
+ if (retryConfig.jitter) {
427
+ delay = delay * (0.5 + Math.random() * 0.5);
428
+ }
429
+
430
+ return Math.floor(delay);
431
+ }
432
+
433
+ /**
434
+ * Sleep for specified duration
435
+ * @param {number} ms - Milliseconds to sleep
436
+ * @returns {Promise} Sleep promise
437
+ * @private
438
+ */
439
+ _sleep(ms) {
440
+ return new Promise(resolve => setTimeout(resolve, ms));
441
+ }
442
+
443
+ /**
444
+ * Simple recovery step implementations (would be more sophisticated in practice)
445
+ */
446
+ async _checkPermissions(path) {
447
+ // Simplified permission check
448
+ return true;
449
+ }
450
+
451
+ async _attemptElevation(context) {
452
+ // Simplified elevation attempt
453
+ return false; // Cannot actually elevate in this context
454
+ }
455
+
456
+ async _checkNetworkConnectivity() {
457
+ // Simplified network check
458
+ return true;
459
+ }
460
+
461
+ async _checkPackageManager() {
462
+ // Simplified package manager check
463
+ return true;
464
+ }
465
+
466
+ async _analyzeValidationFailure(enhancedError) {
467
+ // Simplified validation analysis
468
+ return true;
469
+ }
470
+
471
+ /**
472
+ * Check if step error is fatal
473
+ * @param {Error} error - Step error
474
+ * @returns {boolean} True if fatal
475
+ * @private
476
+ */
477
+ _isStepFatal(error) {
478
+ const fatalCodes = ['SYSTEM_FAILURE', 'CORRUPTION'];
479
+ return fatalCodes.includes(error.code);
480
+ }
481
+
482
+ /**
483
+ * Metrics and monitoring methods
484
+ */
485
+ _generateOperationId() {
486
+ return `op_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
487
+ }
488
+
489
+ _generateRecoveryId() {
490
+ return `rec_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
491
+ }
492
+
493
+ _recordAttempt(operationId, context) {
494
+ this.metrics.attempts.set(operationId, {
495
+ timestamp: new Date().toISOString(),
496
+ context
497
+ });
498
+ }
499
+
500
+ _recordSuccess(operationId, attempt, duration) {
501
+ this.metrics.successes.set(operationId, {
502
+ attempt,
503
+ duration,
504
+ timestamp: new Date().toISOString()
505
+ });
506
+ }
507
+
508
+ _recordFailure(operationId, error, duration) {
509
+ this.metrics.failures.set(operationId, {
510
+ error: error.code || error.message,
511
+ duration,
512
+ timestamp: new Date().toISOString()
513
+ });
514
+ }
515
+
516
+ _recordRecovery(recoveryId, error, result, duration) {
517
+ this.metrics.recoveries.set(recoveryId, {
518
+ errorCode: error.code,
519
+ success: result.success,
520
+ duration,
521
+ timestamp: new Date().toISOString()
522
+ });
523
+ }
524
+
525
+ /**
526
+ * Get recovery system metrics
527
+ * @returns {Object} System metrics
528
+ */
529
+ getMetrics() {
530
+ return {
531
+ attempts: this.metrics.attempts.size,
532
+ successes: this.metrics.successes.size,
533
+ failures: this.metrics.failures.size,
534
+ recoveries: this.metrics.recoveries.size,
535
+ successRate: this.metrics.successes.size / Math.max(this.metrics.attempts.size, 1),
536
+ recoveryRate: Array.from(this.metrics.recoveries.values())
537
+ .filter(r => r.success).length / Math.max(this.metrics.recoveries.size, 1)
538
+ };
539
+ }
540
+
541
+ /**
542
+ * Clear metrics (useful for testing)
543
+ */
544
+ clearMetrics() {
545
+ this.metrics.attempts.clear();
546
+ this.metrics.successes.clear();
547
+ this.metrics.failures.clear();
548
+ this.metrics.recoveries.clear();
549
+ }
550
+
551
+ /**
552
+ * Get singleton instance
553
+ * @returns {ErrorRecoverySystem} Recovery system instance
554
+ */
555
+ static getInstance() {
556
+ if (!ErrorRecoverySystem._instance) {
557
+ ErrorRecoverySystem._instance = new ErrorRecoverySystem();
558
+ }
559
+ return ErrorRecoverySystem._instance;
560
+ }
561
+ }
562
+
563
+ module.exports = ErrorRecoverySystem;