musubi-sdd 6.2.0 → 6.2.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,524 @@
1
+ /**
2
+ * Error Recovery Handler
3
+ *
4
+ * Provides recovery guidance for workflow failures.
5
+ *
6
+ * Requirement: IMP-6.2-008-01
7
+ *
8
+ * @module enterprise/error-recovery
9
+ */
10
+
11
+ const fs = require('fs').promises;
12
+ const path = require('path');
13
+
14
+ /**
15
+ * Error category enum
16
+ */
17
+ const ERROR_CATEGORY = {
18
+ TEST_FAILURE: 'test-failure',
19
+ VALIDATION_ERROR: 'validation-error',
20
+ BUILD_ERROR: 'build-error',
21
+ LINT_ERROR: 'lint-error',
22
+ TYPE_ERROR: 'type-error',
23
+ DEPENDENCY_ERROR: 'dependency-error',
24
+ CONFIGURATION_ERROR: 'configuration-error',
25
+ RUNTIME_ERROR: 'runtime-error',
26
+ UNKNOWN: 'unknown'
27
+ };
28
+
29
+ /**
30
+ * Recovery action enum
31
+ */
32
+ const RECOVERY_ACTION = {
33
+ FIX_CODE: 'fix-code',
34
+ UPDATE_TEST: 'update-test',
35
+ INSTALL_DEPS: 'install-deps',
36
+ UPDATE_CONFIG: 'update-config',
37
+ ROLLBACK: 'rollback',
38
+ MANUAL_REVIEW: 'manual-review',
39
+ RETRY: 'retry'
40
+ };
41
+
42
+ /**
43
+ * Error Recovery Handler
44
+ */
45
+ class ErrorRecoveryHandler {
46
+ /**
47
+ * Create a new ErrorRecoveryHandler
48
+ * @param {Object} config - Configuration options
49
+ */
50
+ constructor(config = {}) {
51
+ this.config = {
52
+ storageDir: config.storageDir || 'storage/errors',
53
+ maxHistorySize: config.maxHistorySize || 100,
54
+ enableAutoAnalysis: config.enableAutoAnalysis !== false,
55
+ ...config
56
+ };
57
+
58
+ this.errorHistory = [];
59
+ this.recoveryPatterns = this.loadRecoveryPatterns();
60
+ }
61
+
62
+ /**
63
+ * Load recovery patterns
64
+ * @returns {Object} Recovery patterns
65
+ */
66
+ loadRecoveryPatterns() {
67
+ return {
68
+ [ERROR_CATEGORY.TEST_FAILURE]: {
69
+ patterns: [
70
+ { match: /expect.*toEqual/i, cause: 'Assertion mismatch', action: RECOVERY_ACTION.FIX_CODE },
71
+ { match: /undefined is not/i, cause: 'Null reference', action: RECOVERY_ACTION.FIX_CODE },
72
+ { match: /timeout/i, cause: 'Test timeout', action: RECOVERY_ACTION.UPDATE_TEST },
73
+ { match: /cannot find module/i, cause: 'Missing import', action: RECOVERY_ACTION.INSTALL_DEPS }
74
+ ],
75
+ defaultAction: RECOVERY_ACTION.MANUAL_REVIEW
76
+ },
77
+ [ERROR_CATEGORY.VALIDATION_ERROR]: {
78
+ patterns: [
79
+ { match: /ears.*format/i, cause: 'EARS format violation', action: RECOVERY_ACTION.FIX_CODE },
80
+ { match: /traceability/i, cause: 'Missing traceability', action: RECOVERY_ACTION.FIX_CODE },
81
+ { match: /constitutional/i, cause: 'Constitutional violation', action: RECOVERY_ACTION.MANUAL_REVIEW }
82
+ ],
83
+ defaultAction: RECOVERY_ACTION.FIX_CODE
84
+ },
85
+ [ERROR_CATEGORY.BUILD_ERROR]: {
86
+ patterns: [
87
+ { match: /syntax.*error/i, cause: 'Syntax error', action: RECOVERY_ACTION.FIX_CODE },
88
+ { match: /cannot resolve/i, cause: 'Module resolution failed', action: RECOVERY_ACTION.INSTALL_DEPS },
89
+ { match: /out of memory/i, cause: 'Memory limit exceeded', action: RECOVERY_ACTION.UPDATE_CONFIG }
90
+ ],
91
+ defaultAction: RECOVERY_ACTION.FIX_CODE
92
+ },
93
+ [ERROR_CATEGORY.LINT_ERROR]: {
94
+ patterns: [
95
+ { match: /parsing error/i, cause: 'Parse error', action: RECOVERY_ACTION.FIX_CODE },
96
+ { match: /no-unused/i, cause: 'Unused code', action: RECOVERY_ACTION.FIX_CODE },
97
+ { match: /prefer-const/i, cause: 'Style violation', action: RECOVERY_ACTION.FIX_CODE }
98
+ ],
99
+ defaultAction: RECOVERY_ACTION.FIX_CODE
100
+ },
101
+ [ERROR_CATEGORY.TYPE_ERROR]: {
102
+ patterns: [
103
+ { match: /type.*not assignable/i, cause: 'Type mismatch', action: RECOVERY_ACTION.FIX_CODE },
104
+ { match: /property.*does not exist/i, cause: 'Missing property', action: RECOVERY_ACTION.FIX_CODE },
105
+ { match: /cannot find name/i, cause: 'Undefined identifier', action: RECOVERY_ACTION.FIX_CODE }
106
+ ],
107
+ defaultAction: RECOVERY_ACTION.FIX_CODE
108
+ },
109
+ [ERROR_CATEGORY.DEPENDENCY_ERROR]: {
110
+ patterns: [
111
+ { match: /peer dep/i, cause: 'Peer dependency conflict', action: RECOVERY_ACTION.INSTALL_DEPS },
112
+ { match: /not found in npm registry/i, cause: 'Package not found', action: RECOVERY_ACTION.UPDATE_CONFIG },
113
+ { match: /version.*incompatible/i, cause: 'Version conflict', action: RECOVERY_ACTION.INSTALL_DEPS }
114
+ ],
115
+ defaultAction: RECOVERY_ACTION.INSTALL_DEPS
116
+ },
117
+ [ERROR_CATEGORY.CONFIGURATION_ERROR]: {
118
+ patterns: [
119
+ { match: /invalid.*config/i, cause: 'Invalid configuration', action: RECOVERY_ACTION.UPDATE_CONFIG },
120
+ { match: /missing.*required/i, cause: 'Missing required field', action: RECOVERY_ACTION.UPDATE_CONFIG }
121
+ ],
122
+ defaultAction: RECOVERY_ACTION.UPDATE_CONFIG
123
+ },
124
+ [ERROR_CATEGORY.RUNTIME_ERROR]: {
125
+ patterns: [
126
+ { match: /enoent/i, cause: 'File not found', action: RECOVERY_ACTION.FIX_CODE },
127
+ { match: /eacces/i, cause: 'Permission denied', action: RECOVERY_ACTION.UPDATE_CONFIG },
128
+ { match: /econnrefused/i, cause: 'Connection refused', action: RECOVERY_ACTION.RETRY }
129
+ ],
130
+ defaultAction: RECOVERY_ACTION.MANUAL_REVIEW
131
+ }
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Analyze error and provide recovery guidance
137
+ * @param {Error|Object} error - Error object
138
+ * @param {Object} context - Error context
139
+ * @returns {Object} Analysis result with recovery guidance
140
+ */
141
+ analyze(error, context = {}) {
142
+ const errorInfo = this.normalizeError(error);
143
+ const category = this.categorizeError(errorInfo, context);
144
+ const rootCause = this.identifyRootCause(errorInfo, category);
145
+ const remediation = this.generateRemediation(category, rootCause, context);
146
+
147
+ const analysis = {
148
+ id: this.generateId(),
149
+ timestamp: new Date().toISOString(),
150
+ error: errorInfo,
151
+ category,
152
+ rootCause,
153
+ remediation,
154
+ context,
155
+ confidence: this.calculateConfidence(rootCause)
156
+ };
157
+
158
+ // Record in history
159
+ this.recordError(analysis);
160
+
161
+ return analysis;
162
+ }
163
+
164
+ /**
165
+ * Normalize error object
166
+ * @param {Error|Object} error - Error input
167
+ * @returns {Object} Normalized error
168
+ */
169
+ normalizeError(error) {
170
+ if (error instanceof Error) {
171
+ return {
172
+ message: error.message,
173
+ name: error.name,
174
+ stack: error.stack,
175
+ code: error.code
176
+ };
177
+ }
178
+ return {
179
+ message: error.message || String(error),
180
+ name: error.name || 'Error',
181
+ stack: error.stack || '',
182
+ code: error.code || ''
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Categorize error
188
+ * @param {Object} errorInfo - Error info
189
+ * @param {Object} context - Error context
190
+ * @returns {string} Error category
191
+ */
192
+ categorizeError(errorInfo, context) {
193
+ const message = errorInfo.message.toLowerCase();
194
+ const name = errorInfo.name.toLowerCase();
195
+
196
+ // Context-based categorization
197
+ if (context.stage === 'test') return ERROR_CATEGORY.TEST_FAILURE;
198
+ if (context.stage === 'validation') return ERROR_CATEGORY.VALIDATION_ERROR;
199
+ if (context.stage === 'build') return ERROR_CATEGORY.BUILD_ERROR;
200
+ if (context.stage === 'lint') return ERROR_CATEGORY.LINT_ERROR;
201
+
202
+ // Message-based categorization
203
+ if (message.includes('assert') || name.includes('assert')) return ERROR_CATEGORY.TEST_FAILURE;
204
+ if (message.includes('type') || name.includes('type')) return ERROR_CATEGORY.TYPE_ERROR;
205
+ if (message.includes('lint') || message.includes('eslint')) return ERROR_CATEGORY.LINT_ERROR;
206
+ if (message.includes('dependency') || message.includes('npm') || message.includes('package')) {
207
+ return ERROR_CATEGORY.DEPENDENCY_ERROR;
208
+ }
209
+ if (message.includes('config')) return ERROR_CATEGORY.CONFIGURATION_ERROR;
210
+ if (message.includes('build') || message.includes('compile')) return ERROR_CATEGORY.BUILD_ERROR;
211
+ if (errorInfo.code?.startsWith('E')) return ERROR_CATEGORY.RUNTIME_ERROR;
212
+
213
+ return ERROR_CATEGORY.UNKNOWN;
214
+ }
215
+
216
+ /**
217
+ * Identify root cause
218
+ * @param {Object} errorInfo - Error info
219
+ * @param {string} category - Error category
220
+ * @returns {Object} Root cause analysis
221
+ */
222
+ identifyRootCause(errorInfo, category) {
223
+ const patterns = this.recoveryPatterns[category];
224
+
225
+ if (!patterns) {
226
+ return {
227
+ cause: 'Unknown error',
228
+ action: RECOVERY_ACTION.MANUAL_REVIEW,
229
+ matched: false
230
+ };
231
+ }
232
+
233
+ const message = errorInfo.message;
234
+
235
+ for (const pattern of patterns.patterns) {
236
+ if (pattern.match.test(message)) {
237
+ return {
238
+ cause: pattern.cause,
239
+ action: pattern.action,
240
+ matched: true,
241
+ pattern: pattern.match.toString()
242
+ };
243
+ }
244
+ }
245
+
246
+ return {
247
+ cause: 'Unrecognized error pattern',
248
+ action: patterns.defaultAction,
249
+ matched: false
250
+ };
251
+ }
252
+
253
+ /**
254
+ * Generate remediation steps
255
+ * @param {string} category - Error category
256
+ * @param {Object} rootCause - Root cause info
257
+ * @param {Object} context - Error context
258
+ * @returns {Object} Remediation guidance
259
+ */
260
+ generateRemediation(category, rootCause, context) {
261
+ const steps = [];
262
+ const commands = [];
263
+
264
+ switch (rootCause.action) {
265
+ case RECOVERY_ACTION.FIX_CODE:
266
+ steps.push('1. Locate the error in the source file');
267
+ steps.push(`2. Fix the issue: ${rootCause.cause}`);
268
+ steps.push('3. Run tests to verify the fix');
269
+ if (context.file) {
270
+ steps.push(`4. Target file: ${context.file}`);
271
+ }
272
+ commands.push('npm test');
273
+ break;
274
+
275
+ case RECOVERY_ACTION.UPDATE_TEST:
276
+ steps.push('1. Review the failing test case');
277
+ steps.push('2. Update test expectations if behavior changed');
278
+ steps.push('3. Consider increasing timeout if needed');
279
+ commands.push('npm test -- --verbose');
280
+ break;
281
+
282
+ case RECOVERY_ACTION.INSTALL_DEPS:
283
+ steps.push('1. Check package.json for missing dependencies');
284
+ steps.push('2. Run npm install to update packages');
285
+ steps.push('3. Clear npm cache if issues persist');
286
+ commands.push('npm install');
287
+ commands.push('npm cache clean --force');
288
+ break;
289
+
290
+ case RECOVERY_ACTION.UPDATE_CONFIG:
291
+ steps.push('1. Review configuration file');
292
+ steps.push('2. Validate JSON/YAML syntax');
293
+ steps.push('3. Check for missing required fields');
294
+ break;
295
+
296
+ case RECOVERY_ACTION.ROLLBACK:
297
+ steps.push('1. Identify the last known good state');
298
+ steps.push('2. Use rollback manager to revert changes');
299
+ steps.push('3. Verify system stability after rollback');
300
+ commands.push('git log --oneline -10');
301
+ break;
302
+
303
+ case RECOVERY_ACTION.RETRY:
304
+ steps.push('1. Wait a moment and retry the operation');
305
+ steps.push('2. Check network connectivity');
306
+ steps.push('3. Verify external service availability');
307
+ break;
308
+
309
+ case RECOVERY_ACTION.MANUAL_REVIEW:
310
+ default:
311
+ steps.push('1. Review the error message and stack trace');
312
+ steps.push('2. Search for similar issues in documentation');
313
+ steps.push('3. Consult with team if needed');
314
+ break;
315
+ }
316
+
317
+ return {
318
+ action: rootCause.action,
319
+ steps,
320
+ commands,
321
+ estimatedTime: this.estimateRecoveryTime(rootCause.action),
322
+ priority: this.determinePriority(category, context)
323
+ };
324
+ }
325
+
326
+ /**
327
+ * Estimate recovery time
328
+ * @param {string} action - Recovery action
329
+ * @returns {string} Time estimate
330
+ */
331
+ estimateRecoveryTime(action) {
332
+ const estimates = {
333
+ [RECOVERY_ACTION.FIX_CODE]: '15-30 minutes',
334
+ [RECOVERY_ACTION.UPDATE_TEST]: '10-20 minutes',
335
+ [RECOVERY_ACTION.INSTALL_DEPS]: '5-10 minutes',
336
+ [RECOVERY_ACTION.UPDATE_CONFIG]: '5-15 minutes',
337
+ [RECOVERY_ACTION.ROLLBACK]: '10-30 minutes',
338
+ [RECOVERY_ACTION.RETRY]: '1-5 minutes',
339
+ [RECOVERY_ACTION.MANUAL_REVIEW]: '30-60 minutes'
340
+ };
341
+ return estimates[action] || 'Unknown';
342
+ }
343
+
344
+ /**
345
+ * Determine priority
346
+ * @param {string} category - Error category
347
+ * @param {Object} context - Context
348
+ * @returns {string} Priority level
349
+ */
350
+ determinePriority(category, context) {
351
+ if (context.blocking) return 'critical';
352
+
353
+ const highPriority = [ERROR_CATEGORY.BUILD_ERROR, ERROR_CATEGORY.TEST_FAILURE];
354
+ const mediumPriority = [ERROR_CATEGORY.TYPE_ERROR, ERROR_CATEGORY.LINT_ERROR];
355
+
356
+ if (highPriority.includes(category)) return 'high';
357
+ if (mediumPriority.includes(category)) return 'medium';
358
+ return 'low';
359
+ }
360
+
361
+ /**
362
+ * Calculate confidence score
363
+ * @param {Object} rootCause - Root cause info
364
+ * @returns {number} Confidence (0-1)
365
+ */
366
+ calculateConfidence(rootCause) {
367
+ if (rootCause.matched) return 0.9;
368
+ return 0.5;
369
+ }
370
+
371
+ /**
372
+ * Record error in history
373
+ * @param {Object} analysis - Error analysis
374
+ */
375
+ recordError(analysis) {
376
+ this.errorHistory.unshift(analysis);
377
+
378
+ // Trim history
379
+ if (this.errorHistory.length > this.config.maxHistorySize) {
380
+ this.errorHistory = this.errorHistory.slice(0, this.config.maxHistorySize);
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Get error history
386
+ * @param {Object} filter - Filter options
387
+ * @returns {Array} Filtered history
388
+ */
389
+ getHistory(filter = {}) {
390
+ let history = [...this.errorHistory];
391
+
392
+ if (filter.category) {
393
+ history = history.filter(e => e.category === filter.category);
394
+ }
395
+ if (filter.since) {
396
+ const since = new Date(filter.since);
397
+ history = history.filter(e => new Date(e.timestamp) >= since);
398
+ }
399
+ if (filter.limit) {
400
+ history = history.slice(0, filter.limit);
401
+ }
402
+
403
+ return history;
404
+ }
405
+
406
+ /**
407
+ * Save error analysis to file
408
+ * @param {Object} analysis - Error analysis
409
+ * @returns {Promise<string>} File path
410
+ */
411
+ async saveAnalysis(analysis) {
412
+ await this.ensureStorageDir();
413
+
414
+ const fileName = `error-${analysis.id}.json`;
415
+ const filePath = path.join(this.config.storageDir, fileName);
416
+
417
+ await fs.writeFile(filePath, JSON.stringify(analysis, null, 2), 'utf-8');
418
+ return filePath;
419
+ }
420
+
421
+ /**
422
+ * Load error analysis from file
423
+ * @param {string} id - Error ID
424
+ * @returns {Promise<Object|null>} Analysis or null
425
+ */
426
+ async loadAnalysis(id) {
427
+ const fileName = `error-${id}.json`;
428
+ const filePath = path.join(this.config.storageDir, fileName);
429
+
430
+ try {
431
+ const content = await fs.readFile(filePath, 'utf-8');
432
+ return JSON.parse(content);
433
+ } catch {
434
+ return null;
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Ensure storage directory exists
440
+ * @returns {Promise<void>}
441
+ */
442
+ async ensureStorageDir() {
443
+ await fs.mkdir(this.config.storageDir, { recursive: true });
444
+ }
445
+
446
+ /**
447
+ * Generate unique ID
448
+ * @returns {string} Unique ID
449
+ */
450
+ generateId() {
451
+ return `err-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
452
+ }
453
+
454
+ /**
455
+ * Generate recovery report
456
+ * @param {Object} analysis - Error analysis
457
+ * @returns {string} Markdown report
458
+ */
459
+ generateReport(analysis) {
460
+ const lines = [];
461
+
462
+ lines.push('# Error Recovery Report');
463
+ lines.push('');
464
+ lines.push(`**Error ID**: ${analysis.id}`);
465
+ lines.push(`**Timestamp**: ${analysis.timestamp}`);
466
+ lines.push(`**Category**: ${analysis.category}`);
467
+ lines.push(`**Confidence**: ${(analysis.confidence * 100).toFixed(0)}%`);
468
+ lines.push('');
469
+
470
+ lines.push('## Error Details');
471
+ lines.push('');
472
+ lines.push(`**Name**: ${analysis.error.name}`);
473
+ lines.push(`**Message**: ${analysis.error.message}`);
474
+ lines.push('');
475
+
476
+ lines.push('## Root Cause Analysis');
477
+ lines.push('');
478
+ lines.push(`**Identified Cause**: ${analysis.rootCause.cause}`);
479
+ lines.push(`**Pattern Matched**: ${analysis.rootCause.matched ? 'Yes' : 'No'}`);
480
+ lines.push('');
481
+
482
+ lines.push('## Remediation');
483
+ lines.push('');
484
+ lines.push(`**Recommended Action**: ${analysis.remediation.action}`);
485
+ lines.push(`**Priority**: ${analysis.remediation.priority}`);
486
+ lines.push(`**Estimated Time**: ${analysis.remediation.estimatedTime}`);
487
+ lines.push('');
488
+
489
+ lines.push('### Steps');
490
+ lines.push('');
491
+ for (const step of analysis.remediation.steps) {
492
+ lines.push(step);
493
+ }
494
+ lines.push('');
495
+
496
+ if (analysis.remediation.commands.length > 0) {
497
+ lines.push('### Commands');
498
+ lines.push('');
499
+ lines.push('```bash');
500
+ for (const cmd of analysis.remediation.commands) {
501
+ lines.push(cmd);
502
+ }
503
+ lines.push('```');
504
+ }
505
+
506
+ return lines.join('\n');
507
+ }
508
+ }
509
+
510
+ /**
511
+ * Create a new ErrorRecoveryHandler instance
512
+ * @param {Object} config - Configuration options
513
+ * @returns {ErrorRecoveryHandler}
514
+ */
515
+ function createErrorRecoveryHandler(config = {}) {
516
+ return new ErrorRecoveryHandler(config);
517
+ }
518
+
519
+ module.exports = {
520
+ ErrorRecoveryHandler,
521
+ createErrorRecoveryHandler,
522
+ ERROR_CATEGORY,
523
+ RECOVERY_ACTION
524
+ };