musubi-sdd 6.1.2 → 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,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 };