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.
- package/README.ja.md +60 -1
- package/README.md +60 -1
- package/bin/musubi-dashboard.js +340 -0
- package/bin/musubi-upgrade.js +395 -0
- package/bin/musubi.js +15 -0
- package/package.json +5 -3
- package/src/cli/dashboard-cli.js +536 -0
- package/src/constitutional/checker.js +633 -0
- package/src/constitutional/ci-reporter.js +338 -0
- package/src/constitutional/index.js +22 -0
- package/src/constitutional/phase-minus-one.js +404 -0
- package/src/constitutional/steering-sync.js +473 -0
- package/src/dashboard/index.js +20 -0
- package/src/dashboard/sprint-planner.js +361 -0
- package/src/dashboard/sprint-reporter.js +378 -0
- package/src/dashboard/transition-recorder.js +209 -0
- package/src/dashboard/workflow-dashboard.js +434 -0
- package/src/enterprise/error-recovery.js +524 -0
- package/src/enterprise/experiment-report.js +573 -0
- package/src/enterprise/index.js +57 -4
- package/src/enterprise/rollback-manager.js +584 -0
- package/src/enterprise/tech-article.js +509 -0
- package/src/traceability/extractor.js +294 -0
- package/src/traceability/gap-detector.js +230 -0
- package/src/traceability/index.js +15 -0
- package/src/traceability/matrix-storage.js +368 -0
|
@@ -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
|
+
};
|
package/src/enterprise/index.js
CHANGED
|
@@ -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
|
};
|