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,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MatrixStorage Implementation
|
|
3
|
+
*
|
|
4
|
+
* YAML-based persistence for traceability matrices.
|
|
5
|
+
*
|
|
6
|
+
* Requirement: IMP-6.2-004-03
|
|
7
|
+
* Design: ADR-6.2-002
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs').promises;
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const yaml = require('yaml');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default configuration
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_CONFIG = {
|
|
18
|
+
storageDir: 'storage/traceability'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* MatrixStorage
|
|
23
|
+
*
|
|
24
|
+
* Persists traceability matrices as YAML files.
|
|
25
|
+
*/
|
|
26
|
+
class MatrixStorage {
|
|
27
|
+
/**
|
|
28
|
+
* @param {Object} config - Configuration options
|
|
29
|
+
*/
|
|
30
|
+
constructor(config = {}) {
|
|
31
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Save traceability matrix
|
|
36
|
+
* @param {string} featureId - Feature ID
|
|
37
|
+
* @param {Object} matrix - Traceability matrix
|
|
38
|
+
* @returns {Promise<string>} Saved file path
|
|
39
|
+
*/
|
|
40
|
+
async save(featureId, matrix) {
|
|
41
|
+
await this.ensureStorageDir();
|
|
42
|
+
|
|
43
|
+
const timestamp = new Date().toISOString().slice(0, 10);
|
|
44
|
+
const filename = `${featureId}-${timestamp}.yaml`;
|
|
45
|
+
const filePath = path.join(this.config.storageDir, filename);
|
|
46
|
+
|
|
47
|
+
const yamlContent = yaml.stringify(matrix, {
|
|
48
|
+
indent: 2,
|
|
49
|
+
lineWidth: 0
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await fs.writeFile(filePath, yamlContent, 'utf-8');
|
|
53
|
+
|
|
54
|
+
return filePath;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load traceability matrix by filename
|
|
59
|
+
* @param {string} filename - Filename to load
|
|
60
|
+
* @returns {Promise<Object|null>} Traceability matrix
|
|
61
|
+
*/
|
|
62
|
+
async load(filename) {
|
|
63
|
+
try {
|
|
64
|
+
let filePath;
|
|
65
|
+
|
|
66
|
+
if (filename.endsWith('.yaml') || filename.endsWith('.yml')) {
|
|
67
|
+
filePath = path.join(this.config.storageDir, filename);
|
|
68
|
+
} else {
|
|
69
|
+
// Try to find matching file
|
|
70
|
+
const files = await this.list(filename);
|
|
71
|
+
if (files.length === 0) return null;
|
|
72
|
+
filePath = path.join(this.config.storageDir, files[0]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await fs.access(filePath);
|
|
76
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
77
|
+
|
|
78
|
+
return yaml.parse(content);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Load most recent matrix for feature
|
|
86
|
+
* @param {string} featureId - Feature ID
|
|
87
|
+
* @returns {Promise<Object|null>} Traceability matrix
|
|
88
|
+
*/
|
|
89
|
+
async loadLatest(featureId) {
|
|
90
|
+
const files = await this.list(featureId);
|
|
91
|
+
|
|
92
|
+
if (files.length === 0) return null;
|
|
93
|
+
|
|
94
|
+
// Sort by date (newest first)
|
|
95
|
+
files.sort().reverse();
|
|
96
|
+
|
|
97
|
+
const latestFile = files[0];
|
|
98
|
+
const filePath = path.join(this.config.storageDir, latestFile);
|
|
99
|
+
|
|
100
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
101
|
+
return yaml.parse(content);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* List saved matrices
|
|
106
|
+
* @param {string} [prefix] - Optional prefix filter
|
|
107
|
+
* @returns {Promise<Array>} List of filenames
|
|
108
|
+
*/
|
|
109
|
+
async list(prefix) {
|
|
110
|
+
try {
|
|
111
|
+
const files = await fs.readdir(this.config.storageDir);
|
|
112
|
+
const yamlFiles = files.filter(f =>
|
|
113
|
+
f.endsWith('.yaml') || f.endsWith('.yml')
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (prefix) {
|
|
117
|
+
return yamlFiles.filter(f => f.startsWith(prefix));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return yamlFiles;
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Delete matrix file
|
|
128
|
+
* @param {string} filename - Filename to delete
|
|
129
|
+
*/
|
|
130
|
+
async delete(filename) {
|
|
131
|
+
const filePath = path.join(this.config.storageDir, filename);
|
|
132
|
+
await fs.access(filePath);
|
|
133
|
+
await fs.unlink(filePath);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Merge two matrices
|
|
138
|
+
* @param {Object} matrix1 - First matrix
|
|
139
|
+
* @param {Object} matrix2 - Second matrix
|
|
140
|
+
* @returns {Object} Merged matrix
|
|
141
|
+
*/
|
|
142
|
+
merge(matrix1, matrix2) {
|
|
143
|
+
const requirements = {};
|
|
144
|
+
|
|
145
|
+
// Get all requirement IDs
|
|
146
|
+
const allReqIds = new Set([
|
|
147
|
+
...Object.keys(matrix1.requirements),
|
|
148
|
+
...Object.keys(matrix2.requirements)
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
for (const reqId of allReqIds) {
|
|
152
|
+
const link1 = matrix1.requirements[reqId];
|
|
153
|
+
const link2 = matrix2.requirements[reqId];
|
|
154
|
+
|
|
155
|
+
if (link1 && link2) {
|
|
156
|
+
// Merge links
|
|
157
|
+
requirements[reqId] = {
|
|
158
|
+
requirementId: reqId,
|
|
159
|
+
design: this.mergeLinks(link1.design, link2.design),
|
|
160
|
+
code: this.mergeLinks(link1.code, link2.code),
|
|
161
|
+
tests: this.mergeLinks(link1.tests, link2.tests),
|
|
162
|
+
commits: this.mergeLinks(link1.commits, link2.commits)
|
|
163
|
+
};
|
|
164
|
+
} else {
|
|
165
|
+
requirements[reqId] = link1 || link2;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
version: matrix2.version,
|
|
171
|
+
generatedAt: new Date().toISOString(),
|
|
172
|
+
requirements,
|
|
173
|
+
summary: this.calculateSummary(requirements)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Merge link arrays
|
|
179
|
+
* @param {Array} links1 - First links array
|
|
180
|
+
* @param {Array} links2 - Second links array
|
|
181
|
+
* @returns {Array} Merged links
|
|
182
|
+
*/
|
|
183
|
+
mergeLinks(links1, links2) {
|
|
184
|
+
const merged = [...links1];
|
|
185
|
+
|
|
186
|
+
for (const link of links2) {
|
|
187
|
+
const exists = merged.some(l => {
|
|
188
|
+
if (l.path && link.path) {
|
|
189
|
+
return l.path === link.path;
|
|
190
|
+
}
|
|
191
|
+
if (l.hash && link.hash) {
|
|
192
|
+
return l.hash === link.hash;
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!exists) {
|
|
198
|
+
merged.push(link);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return merged;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Calculate summary statistics
|
|
207
|
+
* @param {Object} requirements - Requirements map
|
|
208
|
+
* @returns {Object} Summary
|
|
209
|
+
*/
|
|
210
|
+
calculateSummary(requirements) {
|
|
211
|
+
const links = Object.values(requirements);
|
|
212
|
+
const total = links.length;
|
|
213
|
+
|
|
214
|
+
if (total === 0) {
|
|
215
|
+
return {
|
|
216
|
+
totalRequirements: 0,
|
|
217
|
+
linkedRequirements: 0,
|
|
218
|
+
withDesign: 0,
|
|
219
|
+
withCode: 0,
|
|
220
|
+
withTests: 0,
|
|
221
|
+
gaps: 0,
|
|
222
|
+
coveragePercentage: 0
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let withDesign = 0;
|
|
227
|
+
let withCode = 0;
|
|
228
|
+
let withTests = 0;
|
|
229
|
+
let linked = 0;
|
|
230
|
+
let gapCount = 0;
|
|
231
|
+
|
|
232
|
+
for (const link of links) {
|
|
233
|
+
const hasDesign = link.design.length > 0;
|
|
234
|
+
const hasCode = link.code.length > 0;
|
|
235
|
+
const hasTests = link.tests.length > 0;
|
|
236
|
+
|
|
237
|
+
if (hasDesign) withDesign++;
|
|
238
|
+
if (hasCode) withCode++;
|
|
239
|
+
if (hasTests) withTests++;
|
|
240
|
+
|
|
241
|
+
if (hasDesign || hasCode || hasTests) {
|
|
242
|
+
linked++;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Count gaps
|
|
246
|
+
if (!hasDesign) gapCount++;
|
|
247
|
+
if (!hasCode) gapCount++;
|
|
248
|
+
if (!hasTests) gapCount++;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Calculate coverage as percentage of requirements with full coverage
|
|
252
|
+
const fullyLinked = links.filter(l =>
|
|
253
|
+
l.design.length > 0 && l.code.length > 0 && l.tests.length > 0
|
|
254
|
+
).length;
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
totalRequirements: total,
|
|
258
|
+
linkedRequirements: linked,
|
|
259
|
+
withDesign,
|
|
260
|
+
withCode,
|
|
261
|
+
withTests,
|
|
262
|
+
gaps: gapCount,
|
|
263
|
+
coveragePercentage: Math.round((fullyLinked / total) * 100)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Export matrix as JSON
|
|
269
|
+
* @param {Object} matrix - Matrix to export
|
|
270
|
+
* @returns {string} JSON string
|
|
271
|
+
*/
|
|
272
|
+
exportAsJson(matrix) {
|
|
273
|
+
return JSON.stringify(matrix, null, 2);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Export matrix as Markdown
|
|
278
|
+
* @param {Object} matrix - Matrix to export
|
|
279
|
+
* @returns {string} Markdown string
|
|
280
|
+
*/
|
|
281
|
+
exportAsMarkdown(matrix) {
|
|
282
|
+
const lines = [];
|
|
283
|
+
|
|
284
|
+
lines.push('# Traceability Matrix');
|
|
285
|
+
lines.push('');
|
|
286
|
+
lines.push(`Generated: ${matrix.generatedAt}`);
|
|
287
|
+
lines.push(`Version: ${matrix.version}`);
|
|
288
|
+
lines.push('');
|
|
289
|
+
|
|
290
|
+
// Summary
|
|
291
|
+
lines.push('## Summary');
|
|
292
|
+
lines.push('');
|
|
293
|
+
lines.push('| Metric | Value |');
|
|
294
|
+
lines.push('|--------|-------|');
|
|
295
|
+
lines.push(`| Total Requirements | ${matrix.summary.totalRequirements} |`);
|
|
296
|
+
lines.push(`| With Design | ${matrix.summary.withDesign} |`);
|
|
297
|
+
lines.push(`| With Code | ${matrix.summary.withCode} |`);
|
|
298
|
+
lines.push(`| With Tests | ${matrix.summary.withTests} |`);
|
|
299
|
+
lines.push(`| Gaps | ${matrix.summary.gaps} |`);
|
|
300
|
+
lines.push(`| Coverage | ${matrix.summary.coveragePercentage}% |`);
|
|
301
|
+
lines.push('');
|
|
302
|
+
|
|
303
|
+
// Requirements table
|
|
304
|
+
lines.push('## Requirements');
|
|
305
|
+
lines.push('');
|
|
306
|
+
lines.push('| Requirement | Design | Code | Tests | Commits |');
|
|
307
|
+
lines.push('|-------------|--------|------|-------|---------|');
|
|
308
|
+
|
|
309
|
+
for (const [reqId, link] of Object.entries(matrix.requirements)) {
|
|
310
|
+
const design = link.design.length > 0 ? '✅' : '❌';
|
|
311
|
+
const code = link.code.length > 0 ? '✅' : '❌';
|
|
312
|
+
const tests = link.tests.length > 0 ? '✅' : '❌';
|
|
313
|
+
const commits = link.commits.length > 0 ? '✅' : '-';
|
|
314
|
+
|
|
315
|
+
lines.push(`| ${reqId} | ${design} | ${code} | ${tests} | ${commits} |`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
lines.push('');
|
|
319
|
+
|
|
320
|
+
// Details
|
|
321
|
+
lines.push('## Details');
|
|
322
|
+
lines.push('');
|
|
323
|
+
|
|
324
|
+
for (const [reqId, link] of Object.entries(matrix.requirements)) {
|
|
325
|
+
lines.push(`### ${reqId}`);
|
|
326
|
+
lines.push('');
|
|
327
|
+
|
|
328
|
+
if (link.design.length > 0) {
|
|
329
|
+
lines.push('**Design:**');
|
|
330
|
+
for (const d of link.design) {
|
|
331
|
+
lines.push(`- ${d.path}`);
|
|
332
|
+
}
|
|
333
|
+
lines.push('');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (link.code.length > 0) {
|
|
337
|
+
lines.push('**Code:**');
|
|
338
|
+
for (const c of link.code) {
|
|
339
|
+
lines.push(`- ${c.path}${c.line ? `:${c.line}` : ''}`);
|
|
340
|
+
}
|
|
341
|
+
lines.push('');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (link.tests.length > 0) {
|
|
345
|
+
lines.push('**Tests:**');
|
|
346
|
+
for (const t of link.tests) {
|
|
347
|
+
lines.push(`- ${t.path}${t.line ? `:${t.line}` : ''}`);
|
|
348
|
+
}
|
|
349
|
+
lines.push('');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return lines.join('\n');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Ensure storage directory exists
|
|
358
|
+
*/
|
|
359
|
+
async ensureStorageDir() {
|
|
360
|
+
try {
|
|
361
|
+
await fs.access(this.config.storageDir);
|
|
362
|
+
} catch {
|
|
363
|
+
await fs.mkdir(this.config.storageDir, { recursive: true });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
module.exports = { MatrixStorage };
|