musubi-sdd 6.2.0 → 6.2.2

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,573 @@
1
+ /**
2
+ * Experiment Report Generator
3
+ *
4
+ * Automatically generates experiment reports from test results.
5
+ *
6
+ * Requirement: IMP-6.2-006-01
7
+ *
8
+ * @module enterprise/experiment-report
9
+ */
10
+
11
+ const fs = require('fs').promises;
12
+ const path = require('path');
13
+
14
+ /**
15
+ * Report format enum
16
+ */
17
+ const REPORT_FORMAT = {
18
+ MARKDOWN: 'markdown',
19
+ HTML: 'html',
20
+ JSON: 'json'
21
+ };
22
+
23
+ /**
24
+ * Test status enum
25
+ */
26
+ const TEST_STATUS = {
27
+ PASSED: 'passed',
28
+ FAILED: 'failed',
29
+ SKIPPED: 'skipped',
30
+ PENDING: 'pending'
31
+ };
32
+
33
+ /**
34
+ * Experiment Report Generator
35
+ */
36
+ class ExperimentReportGenerator {
37
+ /**
38
+ * Create a new ExperimentReportGenerator
39
+ * @param {Object} config - Configuration options
40
+ */
41
+ constructor(config = {}) {
42
+ this.config = {
43
+ outputDir: config.outputDir || 'reports/experiments',
44
+ format: config.format || REPORT_FORMAT.MARKDOWN,
45
+ includeMetrics: config.includeMetrics !== false,
46
+ includeObservations: config.includeObservations !== false,
47
+ includeCodeSnippets: config.includeCodeSnippets !== false,
48
+ ...config
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Generate experiment report from test results
54
+ * @param {Object} testResults - Jest or compatible test results
55
+ * @param {Object} options - Generation options
56
+ * @returns {Promise<Object>} Generated report info
57
+ */
58
+ async generateFromTestResults(testResults, options = {}) {
59
+ const reportData = this.parseTestResults(testResults);
60
+ const metrics = options.metrics || this.calculateMetrics(testResults);
61
+ const observations = options.observations || [];
62
+
63
+ const report = {
64
+ metadata: {
65
+ title: options.title || 'Experiment Report',
66
+ version: options.version || '1.0.0',
67
+ generatedAt: new Date().toISOString(),
68
+ author: options.author || 'MUSUBI SDD',
69
+ environment: this.getEnvironment()
70
+ },
71
+ summary: reportData.summary,
72
+ testResults: reportData.tests,
73
+ metrics,
74
+ observations,
75
+ conclusions: options.conclusions || []
76
+ };
77
+
78
+ const formatted = this.formatReport(report, options.format || this.config.format);
79
+ const filePath = await this.saveReport(formatted, report.metadata, options.format || this.config.format);
80
+
81
+ return {
82
+ report,
83
+ formatted,
84
+ filePath,
85
+ format: options.format || this.config.format
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Parse test results into report data
91
+ * @param {Object} testResults - Raw test results
92
+ * @returns {Object} Parsed data
93
+ */
94
+ parseTestResults(testResults) {
95
+ const tests = [];
96
+ let passed = 0;
97
+ let failed = 0;
98
+ let skipped = 0;
99
+ let pending = 0;
100
+
101
+ // Handle Jest format
102
+ if (testResults.testResults) {
103
+ for (const suite of testResults.testResults) {
104
+ for (const test of suite.assertionResults || []) {
105
+ const status = this.mapTestStatus(test.status);
106
+ tests.push({
107
+ name: test.title || test.fullName,
108
+ suite: suite.name || path.basename(suite.testFilePath || ''),
109
+ status,
110
+ duration: test.duration || 0,
111
+ failureMessages: test.failureMessages || []
112
+ });
113
+
114
+ if (status === TEST_STATUS.PASSED) passed++;
115
+ else if (status === TEST_STATUS.FAILED) failed++;
116
+ else if (status === TEST_STATUS.SKIPPED) skipped++;
117
+ else if (status === TEST_STATUS.PENDING) pending++;
118
+ }
119
+ }
120
+ }
121
+
122
+ // Handle simple array format
123
+ if (Array.isArray(testResults.tests)) {
124
+ for (const test of testResults.tests) {
125
+ const status = this.mapTestStatus(test.status);
126
+ tests.push({
127
+ name: test.name || test.title,
128
+ suite: test.suite || 'Default',
129
+ status,
130
+ duration: test.duration || 0,
131
+ failureMessages: test.errors || []
132
+ });
133
+
134
+ if (status === TEST_STATUS.PASSED) passed++;
135
+ else if (status === TEST_STATUS.FAILED) failed++;
136
+ else if (status === TEST_STATUS.SKIPPED) skipped++;
137
+ else if (status === TEST_STATUS.PENDING) pending++;
138
+ }
139
+ }
140
+
141
+ // Use provided summary or calculate
142
+ const summary = testResults.summary || {
143
+ total: tests.length,
144
+ passed,
145
+ failed,
146
+ skipped,
147
+ pending,
148
+ passRate: tests.length > 0 ? ((passed / tests.length) * 100).toFixed(2) + '%' : '0%',
149
+ duration: testResults.duration || tests.reduce((sum, t) => sum + t.duration, 0)
150
+ };
151
+
152
+ return { tests, summary };
153
+ }
154
+
155
+ /**
156
+ * Map test status to standard enum
157
+ * @param {string} status - Raw status
158
+ * @returns {string} Mapped status
159
+ */
160
+ mapTestStatus(status) {
161
+ const statusLower = (status || '').toLowerCase();
162
+ if (statusLower === 'passed' || statusLower === 'pass') return TEST_STATUS.PASSED;
163
+ if (statusLower === 'failed' || statusLower === 'fail') return TEST_STATUS.FAILED;
164
+ if (statusLower === 'skipped' || statusLower === 'skip') return TEST_STATUS.SKIPPED;
165
+ if (statusLower === 'pending' || statusLower === 'todo') return TEST_STATUS.PENDING;
166
+ return TEST_STATUS.PENDING;
167
+ }
168
+
169
+ /**
170
+ * Calculate metrics from test results
171
+ * @param {Object} testResults - Test results
172
+ * @returns {Object} Calculated metrics
173
+ */
174
+ calculateMetrics(testResults) {
175
+ const metrics = {
176
+ performance: {},
177
+ coverage: {},
178
+ quality: {}
179
+ };
180
+
181
+ // Performance metrics
182
+ if (testResults.duration) {
183
+ metrics.performance.totalDuration = testResults.duration;
184
+ }
185
+
186
+ if (testResults.testResults) {
187
+ const durations = testResults.testResults
188
+ .flatMap(s => s.assertionResults || [])
189
+ .map(t => t.duration || 0)
190
+ .filter(d => d > 0);
191
+
192
+ if (durations.length > 0) {
193
+ metrics.performance.avgTestDuration = (durations.reduce((a, b) => a + b, 0) / durations.length).toFixed(2);
194
+ metrics.performance.maxTestDuration = Math.max(...durations);
195
+ metrics.performance.minTestDuration = Math.min(...durations);
196
+ }
197
+ }
198
+
199
+ // Coverage metrics (if available)
200
+ if (testResults.coverageMap || testResults.coverage) {
201
+ const coverage = testResults.coverageMap || testResults.coverage;
202
+ metrics.coverage = {
203
+ lines: coverage.lines || coverage.line || 'N/A',
204
+ branches: coverage.branches || coverage.branch || 'N/A',
205
+ functions: coverage.functions || coverage.function || 'N/A',
206
+ statements: coverage.statements || coverage.statement || 'N/A'
207
+ };
208
+ }
209
+
210
+ // Quality metrics
211
+ const summary = this.parseTestResults(testResults).summary;
212
+ metrics.quality.passRate = summary.passRate;
213
+ metrics.quality.failureRate = summary.total > 0
214
+ ? ((summary.failed / summary.total) * 100).toFixed(2) + '%'
215
+ : '0%';
216
+
217
+ return metrics;
218
+ }
219
+
220
+ /**
221
+ * Get environment information
222
+ * @returns {Object} Environment info
223
+ */
224
+ getEnvironment() {
225
+ return {
226
+ node: process.version,
227
+ platform: process.platform,
228
+ arch: process.arch,
229
+ cwd: process.cwd()
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Format report based on format type
235
+ * @param {Object} report - Report data
236
+ * @param {string} format - Output format
237
+ * @returns {string} Formatted report
238
+ */
239
+ formatReport(report, format) {
240
+ switch (format) {
241
+ case REPORT_FORMAT.MARKDOWN:
242
+ return this.formatMarkdown(report);
243
+ case REPORT_FORMAT.HTML:
244
+ return this.formatHTML(report);
245
+ case REPORT_FORMAT.JSON:
246
+ return JSON.stringify(report, null, 2);
247
+ default:
248
+ return this.formatMarkdown(report);
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Format report as Markdown
254
+ * @param {Object} report - Report data
255
+ * @returns {string} Markdown content
256
+ */
257
+ formatMarkdown(report) {
258
+ const lines = [];
259
+ const { metadata, summary, testResults, metrics, observations, conclusions } = report;
260
+
261
+ // Header
262
+ lines.push(`# ${metadata.title}`);
263
+ lines.push('');
264
+ lines.push(`**Version**: ${metadata.version}`);
265
+ lines.push(`**Generated**: ${metadata.generatedAt}`);
266
+ lines.push(`**Author**: ${metadata.author}`);
267
+ lines.push('');
268
+ lines.push('---');
269
+ lines.push('');
270
+
271
+ // Summary
272
+ lines.push('## Summary');
273
+ lines.push('');
274
+ lines.push('| Metric | Value |');
275
+ lines.push('|--------|-------|');
276
+ lines.push(`| Total Tests | ${summary.total} |`);
277
+ lines.push(`| Passed | ${summary.passed} ✅ |`);
278
+ lines.push(`| Failed | ${summary.failed} ❌ |`);
279
+ lines.push(`| Skipped | ${summary.skipped} ⏭️ |`);
280
+ lines.push(`| Pass Rate | ${summary.passRate} |`);
281
+ lines.push(`| Duration | ${this.formatDuration(summary.duration)} |`);
282
+ lines.push('');
283
+
284
+ // Test Results
285
+ if (this.config.includeCodeSnippets && testResults.length > 0) {
286
+ lines.push('## Test Results');
287
+ lines.push('');
288
+
289
+ // Group by suite
290
+ const suites = {};
291
+ for (const test of testResults) {
292
+ if (!suites[test.suite]) suites[test.suite] = [];
293
+ suites[test.suite].push(test);
294
+ }
295
+
296
+ for (const [suiteName, tests] of Object.entries(suites)) {
297
+ lines.push(`### ${suiteName}`);
298
+ lines.push('');
299
+ lines.push('| Test | Status | Duration |');
300
+ lines.push('|------|--------|----------|');
301
+ for (const test of tests) {
302
+ const statusIcon = this.getStatusIcon(test.status);
303
+ lines.push(`| ${test.name} | ${statusIcon} ${test.status} | ${test.duration}ms |`);
304
+ }
305
+ lines.push('');
306
+
307
+ // Show failures
308
+ const failures = tests.filter(t => t.status === TEST_STATUS.FAILED);
309
+ if (failures.length > 0) {
310
+ lines.push('#### Failures');
311
+ lines.push('');
312
+ for (const failure of failures) {
313
+ lines.push(`**${failure.name}**`);
314
+ lines.push('```');
315
+ lines.push(failure.failureMessages.join('\n'));
316
+ lines.push('```');
317
+ lines.push('');
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ // Metrics
324
+ if (this.config.includeMetrics && metrics) {
325
+ lines.push('## Metrics');
326
+ lines.push('');
327
+
328
+ if (metrics.performance && Object.keys(metrics.performance).length > 0) {
329
+ lines.push('### Performance');
330
+ lines.push('');
331
+ lines.push('| Metric | Value |');
332
+ lines.push('|--------|-------|');
333
+ for (const [key, value] of Object.entries(metrics.performance)) {
334
+ lines.push(`| ${this.formatMetricName(key)} | ${value}ms |`);
335
+ }
336
+ lines.push('');
337
+ }
338
+
339
+ if (metrics.coverage && Object.keys(metrics.coverage).length > 0) {
340
+ lines.push('### Coverage');
341
+ lines.push('');
342
+ lines.push('| Type | Coverage |');
343
+ lines.push('|------|----------|');
344
+ for (const [key, value] of Object.entries(metrics.coverage)) {
345
+ lines.push(`| ${this.formatMetricName(key)} | ${value} |`);
346
+ }
347
+ lines.push('');
348
+ }
349
+
350
+ if (metrics.quality && Object.keys(metrics.quality).length > 0) {
351
+ lines.push('### Quality');
352
+ lines.push('');
353
+ lines.push('| Metric | Value |');
354
+ lines.push('|--------|-------|');
355
+ for (const [key, value] of Object.entries(metrics.quality)) {
356
+ lines.push(`| ${this.formatMetricName(key)} | ${value} |`);
357
+ }
358
+ lines.push('');
359
+ }
360
+ }
361
+
362
+ // Observations
363
+ if (this.config.includeObservations && observations.length > 0) {
364
+ lines.push('## Observations');
365
+ lines.push('');
366
+ for (const obs of observations) {
367
+ lines.push(`- ${obs}`);
368
+ }
369
+ lines.push('');
370
+ }
371
+
372
+ // Conclusions
373
+ if (conclusions.length > 0) {
374
+ lines.push('## Conclusions');
375
+ lines.push('');
376
+ for (const conclusion of conclusions) {
377
+ lines.push(`- ${conclusion}`);
378
+ }
379
+ lines.push('');
380
+ }
381
+
382
+ // Footer
383
+ lines.push('---');
384
+ lines.push('');
385
+ lines.push(`*Generated by MUSUBI SDD Experiment Report Generator*`);
386
+
387
+ return lines.join('\n');
388
+ }
389
+
390
+ /**
391
+ * Format report as HTML
392
+ * @param {Object} report - Report data
393
+ * @returns {string} HTML content
394
+ */
395
+ formatHTML(report) {
396
+ const { metadata, summary, testResults, observations, conclusions } = report;
397
+
398
+ return `<!DOCTYPE html>
399
+ <html lang="en">
400
+ <head>
401
+ <meta charset="UTF-8">
402
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
403
+ <title>${metadata.title}</title>
404
+ <style>
405
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; }
406
+ h1 { color: #333; }
407
+ table { border-collapse: collapse; width: 100%; margin: 20px 0; }
408
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
409
+ th { background-color: #4CAF50; color: white; }
410
+ tr:nth-child(even) { background-color: #f2f2f2; }
411
+ .passed { color: green; }
412
+ .failed { color: red; }
413
+ .skipped { color: orange; }
414
+ .meta { color: #666; font-size: 0.9em; }
415
+ </style>
416
+ </head>
417
+ <body>
418
+ <h1>${metadata.title}</h1>
419
+ <p class="meta">
420
+ <strong>Version:</strong> ${metadata.version} |
421
+ <strong>Generated:</strong> ${metadata.generatedAt} |
422
+ <strong>Author:</strong> ${metadata.author}
423
+ </p>
424
+
425
+ <h2>Summary</h2>
426
+ <table>
427
+ <tr><th>Metric</th><th>Value</th></tr>
428
+ <tr><td>Total Tests</td><td>${summary.total}</td></tr>
429
+ <tr><td>Passed</td><td class="passed">${summary.passed}</td></tr>
430
+ <tr><td>Failed</td><td class="failed">${summary.failed}</td></tr>
431
+ <tr><td>Skipped</td><td class="skipped">${summary.skipped}</td></tr>
432
+ <tr><td>Pass Rate</td><td>${summary.passRate}</td></tr>
433
+ <tr><td>Duration</td><td>${this.formatDuration(summary.duration)}</td></tr>
434
+ </table>
435
+
436
+ ${testResults.length > 0 ? `
437
+ <h2>Test Results</h2>
438
+ <table>
439
+ <tr><th>Suite</th><th>Test</th><th>Status</th><th>Duration</th></tr>
440
+ ${testResults.map(t => `
441
+ <tr>
442
+ <td>${t.suite}</td>
443
+ <td>${t.name}</td>
444
+ <td class="${t.status}">${t.status}</td>
445
+ <td>${t.duration}ms</td>
446
+ </tr>
447
+ `).join('')}
448
+ </table>
449
+ ` : ''}
450
+
451
+ ${observations.length > 0 ? `
452
+ <h2>Observations</h2>
453
+ <ul>
454
+ ${observations.map(o => `<li>${o}</li>`).join('')}
455
+ </ul>
456
+ ` : ''}
457
+
458
+ ${conclusions.length > 0 ? `
459
+ <h2>Conclusions</h2>
460
+ <ul>
461
+ ${conclusions.map(c => `<li>${c}</li>`).join('')}
462
+ </ul>
463
+ ` : ''}
464
+
465
+ <hr>
466
+ <p class="meta"><em>Generated by MUSUBI SDD Experiment Report Generator</em></p>
467
+ </body>
468
+ </html>`;
469
+ }
470
+
471
+ /**
472
+ * Save report to file
473
+ * @param {string} content - Formatted content
474
+ * @param {Object} metadata - Report metadata
475
+ * @param {string} format - Output format
476
+ * @returns {Promise<string>} File path
477
+ */
478
+ async saveReport(content, metadata, format) {
479
+ await this.ensureOutputDir();
480
+
481
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
482
+ const extension = format === REPORT_FORMAT.HTML ? 'html' :
483
+ format === REPORT_FORMAT.JSON ? 'json' : 'md';
484
+ const fileName = `experiment-${timestamp}.${extension}`;
485
+ const filePath = path.join(this.config.outputDir, fileName);
486
+
487
+ await fs.writeFile(filePath, content, 'utf-8');
488
+ return filePath;
489
+ }
490
+
491
+ /**
492
+ * Ensure output directory exists
493
+ * @returns {Promise<void>}
494
+ */
495
+ async ensureOutputDir() {
496
+ await fs.mkdir(this.config.outputDir, { recursive: true });
497
+ }
498
+
499
+ /**
500
+ * Get status icon
501
+ * @param {string} status - Test status
502
+ * @returns {string} Icon
503
+ */
504
+ getStatusIcon(status) {
505
+ switch (status) {
506
+ case TEST_STATUS.PASSED: return '✅';
507
+ case TEST_STATUS.FAILED: return '❌';
508
+ case TEST_STATUS.SKIPPED: return '⏭️';
509
+ case TEST_STATUS.PENDING: return '⏳';
510
+ default: return '❓';
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Format duration
516
+ * @param {number} ms - Duration in milliseconds
517
+ * @returns {string} Formatted duration
518
+ */
519
+ formatDuration(ms) {
520
+ if (!ms || ms < 0) return '0ms';
521
+ if (ms < 1000) return `${ms}ms`;
522
+ if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
523
+ return `${(ms / 60000).toFixed(2)}m`;
524
+ }
525
+
526
+ /**
527
+ * Format metric name
528
+ * @param {string} name - camelCase name
529
+ * @returns {string} Formatted name
530
+ */
531
+ formatMetricName(name) {
532
+ return name
533
+ .replace(/([A-Z])/g, ' $1')
534
+ .replace(/^./, str => str.toUpperCase())
535
+ .trim();
536
+ }
537
+
538
+ /**
539
+ * Add observation to report
540
+ * @param {Object} report - Report object
541
+ * @param {string} observation - Observation text
542
+ */
543
+ addObservation(report, observation) {
544
+ if (!report.observations) report.observations = [];
545
+ report.observations.push(observation);
546
+ }
547
+
548
+ /**
549
+ * Add conclusion to report
550
+ * @param {Object} report - Report object
551
+ * @param {string} conclusion - Conclusion text
552
+ */
553
+ addConclusion(report, conclusion) {
554
+ if (!report.conclusions) report.conclusions = [];
555
+ report.conclusions.push(conclusion);
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Create a new ExperimentReportGenerator instance
561
+ * @param {Object} config - Configuration options
562
+ * @returns {ExperimentReportGenerator}
563
+ */
564
+ function createExperimentReportGenerator(config = {}) {
565
+ return new ExperimentReportGenerator(config);
566
+ }
567
+
568
+ module.exports = {
569
+ ExperimentReportGenerator,
570
+ createExperimentReportGenerator,
571
+ REPORT_FORMAT,
572
+ TEST_STATUS
573
+ };
@@ -2,6 +2,7 @@
2
2
  * Enterprise Module - MUSUBI SDD
3
3
  *
4
4
  * Phase 6: Enterprise-grade features for multi-tenant deployments
5
+ * Phase 4 (v6.2): Document generation, error handling, rollback
5
6
  *
6
7
  * @module enterprise
7
8
  */
@@ -21,20 +22,72 @@ const {
21
22
  defaultTenantManager,
22
23
  } = require('./multi-tenant');
23
24
 
25
+ const {
26
+ ExperimentReportGenerator,
27
+ createExperimentReportGenerator,
28
+ REPORT_FORMAT,
29
+ TEST_STATUS
30
+ } = require('./experiment-report');
31
+
32
+ const {
33
+ TechArticleGenerator,
34
+ createTechArticleGenerator,
35
+ PLATFORM,
36
+ ARTICLE_TYPE
37
+ } = require('./tech-article');
38
+
39
+ const {
40
+ ErrorRecoveryHandler,
41
+ createErrorRecoveryHandler,
42
+ ERROR_CATEGORY,
43
+ RECOVERY_ACTION
44
+ } = require('./error-recovery');
45
+
46
+ const {
47
+ RollbackManager,
48
+ createRollbackManager,
49
+ ROLLBACK_LEVEL,
50
+ ROLLBACK_STATUS,
51
+ WORKFLOW_STAGE
52
+ } = require('./rollback-manager');
53
+
24
54
  module.exports = {
25
- // Constants
55
+ // Multi-tenant Constants
26
56
  TenantRole,
27
57
  Permission,
28
58
  ROLE_PERMISSIONS,
29
59
 
30
- // Classes
60
+ // Multi-tenant Classes
31
61
  TenantConfig,
32
62
  TenantUser,
33
63
  TenantContext,
34
64
  UsageTracker,
35
65
  AuditLogger,
36
66
  TenantManager,
37
-
38
- // Default instance
39
67
  defaultTenantManager,
68
+
69
+ // Experiment Report (IMP-6.2-006-01)
70
+ ExperimentReportGenerator,
71
+ createExperimentReportGenerator,
72
+ REPORT_FORMAT,
73
+ TEST_STATUS,
74
+
75
+ // Tech Article (IMP-6.2-006-02)
76
+ TechArticleGenerator,
77
+ createTechArticleGenerator,
78
+ PLATFORM,
79
+ ARTICLE_TYPE,
80
+
81
+ // Error Recovery (IMP-6.2-008-01)
82
+ ErrorRecoveryHandler,
83
+ createErrorRecoveryHandler,
84
+ ERROR_CATEGORY,
85
+ RECOVERY_ACTION,
86
+
87
+ // Rollback Manager (IMP-6.2-008-02)
88
+ RollbackManager,
89
+ createRollbackManager,
90
+ ROLLBACK_LEVEL,
91
+ ROLLBACK_STATUS,
92
+ WORKFLOW_STAGE
40
93
  };