musubi-sdd 0.8.4 → 0.8.5
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 +9 -0
- package/README.md +9 -0
- package/bin/musubi-trace.js +367 -0
- package/package.json +3 -2
- package/src/analyzers/traceability.js +524 -0
package/README.ja.md
CHANGED
|
@@ -129,6 +129,15 @@ musubi-tasks list --priority P0 # 重要タスクのみ表示
|
|
|
129
129
|
musubi-tasks update 001 "In Progress" # タスクステータス更新
|
|
130
130
|
musubi-tasks validate # タスク完全性を検証
|
|
131
131
|
musubi-tasks graph # 依存関係グラフ表示
|
|
132
|
+
|
|
133
|
+
# エンドツーエンドトレーサビリティ(v0.8.5)
|
|
134
|
+
musubi-trace matrix # トレーサビリティマトリクス生成
|
|
135
|
+
musubi-trace matrix --format markdown > trace.md # Markdownにエクスポート
|
|
136
|
+
musubi-trace coverage # カバレッジ統計計算
|
|
137
|
+
musubi-trace coverage --min-coverage 100 # 100%カバレッジ要求
|
|
138
|
+
musubi-trace gaps # 孤立した要件/コード検出
|
|
139
|
+
musubi-trace requirement REQ-AUTH-001 # 特定要件をトレース
|
|
140
|
+
musubi-trace validate # 100%トレーサビリティ検証(第5条)
|
|
132
141
|
```
|
|
133
142
|
|
|
134
143
|
### プロジェクトタイプ
|
package/README.md
CHANGED
|
@@ -133,6 +133,15 @@ musubi-tasks list --priority P0 # List critical tasks
|
|
|
133
133
|
musubi-tasks update 001 "In Progress" # Update task status
|
|
134
134
|
musubi-tasks validate # Validate task completeness
|
|
135
135
|
musubi-tasks graph # Show dependency graph
|
|
136
|
+
|
|
137
|
+
# End-to-end traceability (v0.8.5)
|
|
138
|
+
musubi-trace matrix # Generate traceability matrix
|
|
139
|
+
musubi-trace matrix --format markdown > trace.md # Export to markdown
|
|
140
|
+
musubi-trace coverage # Calculate coverage statistics
|
|
141
|
+
musubi-trace coverage --min-coverage 100 # Require 100% coverage
|
|
142
|
+
musubi-trace gaps # Detect orphaned requirements/code
|
|
143
|
+
musubi-trace requirement REQ-AUTH-001 # Trace specific requirement
|
|
144
|
+
musubi-trace validate # Validate 100% traceability (Article V)
|
|
136
145
|
```
|
|
137
146
|
|
|
138
147
|
### Project Types
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MUSUBI Traceability System CLI
|
|
5
|
+
*
|
|
6
|
+
* Provides end-to-end traceability from requirements to code to tests
|
|
7
|
+
* Ensures 100% coverage and detects gaps
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* musubi-trace matrix # Generate full traceability matrix
|
|
11
|
+
* musubi-trace coverage # Calculate requirement coverage
|
|
12
|
+
* musubi-trace gaps # Detect orphaned requirements/code
|
|
13
|
+
* musubi-trace requirement <id> # Trace specific requirement
|
|
14
|
+
* musubi-trace validate # Validate 100% coverage
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { Command } = require('commander');
|
|
18
|
+
const chalk = require('chalk');
|
|
19
|
+
const TraceabilityAnalyzer = require('../src/analyzers/traceability');
|
|
20
|
+
|
|
21
|
+
const program = new Command();
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.name('musubi-trace')
|
|
25
|
+
.description('MUSUBI Traceability System - End-to-end requirement traceability')
|
|
26
|
+
.version('0.8.5');
|
|
27
|
+
|
|
28
|
+
// Generate traceability matrix
|
|
29
|
+
program
|
|
30
|
+
.command('matrix')
|
|
31
|
+
.description('Generate full traceability matrix')
|
|
32
|
+
.option('-f, --format <type>', 'Output format (table|markdown|json|html)', 'table')
|
|
33
|
+
.option('-o, --output <path>', 'Output file path')
|
|
34
|
+
.option('--requirements <path>', 'Requirements directory', 'docs/requirements')
|
|
35
|
+
.option('--design <path>', 'Design directory', 'docs/design')
|
|
36
|
+
.option('--tasks <path>', 'Tasks directory', 'docs/tasks')
|
|
37
|
+
.option('--code <path>', 'Source code directory', 'src')
|
|
38
|
+
.option('--tests <path>', 'Tests directory', 'tests')
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
try {
|
|
41
|
+
console.log(chalk.bold('\n📊 Generating Traceability Matrix\n'));
|
|
42
|
+
|
|
43
|
+
const analyzer = new TraceabilityAnalyzer(process.cwd());
|
|
44
|
+
const matrix = await analyzer.generateMatrix(options);
|
|
45
|
+
|
|
46
|
+
if (options.output) {
|
|
47
|
+
const fs = require('fs-extra');
|
|
48
|
+
await fs.writeFile(options.output, analyzer.formatMatrix(matrix, options.format), 'utf-8');
|
|
49
|
+
console.log(chalk.green(`✓ Matrix saved to ${options.output}`));
|
|
50
|
+
} else {
|
|
51
|
+
console.log(analyzer.formatMatrix(matrix, options.format));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(chalk.bold('Summary:'));
|
|
56
|
+
console.log(chalk.dim(` Requirements: ${matrix.summary.totalRequirements}`));
|
|
57
|
+
console.log(chalk.dim(` With Design: ${matrix.summary.withDesign} (${matrix.summary.designCoverage}%)`));
|
|
58
|
+
console.log(chalk.dim(` With Tasks: ${matrix.summary.withTasks} (${matrix.summary.tasksCoverage}%)`));
|
|
59
|
+
console.log(chalk.dim(` With Code: ${matrix.summary.withCode} (${matrix.summary.codeCoverage}%)`));
|
|
60
|
+
console.log(chalk.dim(` With Tests: ${matrix.summary.withTests} (${matrix.summary.testsCoverage}%)`));
|
|
61
|
+
console.log();
|
|
62
|
+
|
|
63
|
+
process.exit(0);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Calculate coverage
|
|
71
|
+
program
|
|
72
|
+
.command('coverage')
|
|
73
|
+
.description('Calculate requirement coverage statistics')
|
|
74
|
+
.option('--requirements <path>', 'Requirements directory', 'docs/requirements')
|
|
75
|
+
.option('--design <path>', 'Design directory', 'docs/design')
|
|
76
|
+
.option('--tasks <path>', 'Tasks directory', 'docs/tasks')
|
|
77
|
+
.option('--code <path>', 'Source code directory', 'src')
|
|
78
|
+
.option('--tests <path>', 'Tests directory', 'tests')
|
|
79
|
+
.option('--min-coverage <percent>', 'Minimum required coverage', '100')
|
|
80
|
+
.action(async (options) => {
|
|
81
|
+
try {
|
|
82
|
+
console.log(chalk.bold('\n📈 Calculating Coverage\n'));
|
|
83
|
+
|
|
84
|
+
const analyzer = new TraceabilityAnalyzer(process.cwd());
|
|
85
|
+
const coverage = await analyzer.calculateCoverage(options);
|
|
86
|
+
|
|
87
|
+
const minCoverage = parseInt(options.minCoverage);
|
|
88
|
+
|
|
89
|
+
console.log(chalk.bold('Coverage Report:'));
|
|
90
|
+
console.log();
|
|
91
|
+
|
|
92
|
+
const stages = [
|
|
93
|
+
{ name: 'Requirements → Design', value: coverage.designCoverage, color: coverage.designCoverage >= minCoverage ? chalk.green : chalk.red },
|
|
94
|
+
{ name: 'Requirements → Tasks', value: coverage.tasksCoverage, color: coverage.tasksCoverage >= minCoverage ? chalk.green : chalk.red },
|
|
95
|
+
{ name: 'Requirements → Code', value: coverage.codeCoverage, color: coverage.codeCoverage >= minCoverage ? chalk.green : chalk.red },
|
|
96
|
+
{ name: 'Requirements → Tests', value: coverage.testsCoverage, color: coverage.testsCoverage >= minCoverage ? chalk.green : chalk.red }
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
stages.forEach(stage => {
|
|
100
|
+
const bar = '█'.repeat(Math.floor(stage.value / 2));
|
|
101
|
+
console.log(stage.color(` ${stage.name.padEnd(25)} ${bar} ${stage.value}%`));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
console.log();
|
|
105
|
+
console.log(chalk.bold('Overall Coverage:'));
|
|
106
|
+
const overallColor = coverage.overall >= minCoverage ? chalk.green : chalk.red;
|
|
107
|
+
console.log(overallColor(` ${coverage.overall}% (min: ${minCoverage}%)`));
|
|
108
|
+
console.log();
|
|
109
|
+
|
|
110
|
+
if (coverage.overall >= minCoverage) {
|
|
111
|
+
console.log(chalk.green('✓ Coverage meets requirements\n'));
|
|
112
|
+
process.exit(0);
|
|
113
|
+
} else {
|
|
114
|
+
console.log(chalk.red('✗ Coverage below minimum threshold\n'));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Detect gaps
|
|
124
|
+
program
|
|
125
|
+
.command('gaps')
|
|
126
|
+
.description('Detect orphaned requirements, design, tasks, and untested code')
|
|
127
|
+
.option('--requirements <path>', 'Requirements directory', 'docs/requirements')
|
|
128
|
+
.option('--design <path>', 'Design directory', 'docs/design')
|
|
129
|
+
.option('--tasks <path>', 'Tasks directory', 'docs/tasks')
|
|
130
|
+
.option('--code <path>', 'Source code directory', 'src')
|
|
131
|
+
.option('--tests <path>', 'Tests directory', 'tests')
|
|
132
|
+
.option('-v, --verbose', 'Show detailed gap information')
|
|
133
|
+
.action(async (options) => {
|
|
134
|
+
try {
|
|
135
|
+
console.log(chalk.bold('\n🔍 Detecting Gaps\n'));
|
|
136
|
+
|
|
137
|
+
const analyzer = new TraceabilityAnalyzer(process.cwd());
|
|
138
|
+
const gaps = await analyzer.detectGaps(options);
|
|
139
|
+
|
|
140
|
+
let hasGaps = false;
|
|
141
|
+
|
|
142
|
+
if (gaps.orphanedRequirements.length > 0) {
|
|
143
|
+
hasGaps = true;
|
|
144
|
+
console.log(chalk.red.bold('Orphaned Requirements (no design/tasks):'));
|
|
145
|
+
gaps.orphanedRequirements.forEach(req => {
|
|
146
|
+
console.log(chalk.red(` • ${req.id}: ${req.title}`));
|
|
147
|
+
if (options.verbose && req.file) {
|
|
148
|
+
console.log(chalk.dim(` ${req.file}`));
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
console.log();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (gaps.orphanedDesign.length > 0) {
|
|
155
|
+
hasGaps = true;
|
|
156
|
+
console.log(chalk.yellow.bold('Orphaned Design (no requirements):'));
|
|
157
|
+
gaps.orphanedDesign.forEach(design => {
|
|
158
|
+
console.log(chalk.yellow(` • ${design.id}: ${design.title}`));
|
|
159
|
+
if (options.verbose && design.file) {
|
|
160
|
+
console.log(chalk.dim(` ${design.file}`));
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
console.log();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (gaps.orphanedTasks.length > 0) {
|
|
167
|
+
hasGaps = true;
|
|
168
|
+
console.log(chalk.yellow.bold('Orphaned Tasks (no requirements):'));
|
|
169
|
+
gaps.orphanedTasks.forEach(task => {
|
|
170
|
+
console.log(chalk.yellow(` • ${task.id}: ${task.title}`));
|
|
171
|
+
if (options.verbose && task.file) {
|
|
172
|
+
console.log(chalk.dim(` ${task.file}`));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
console.log();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (gaps.untestedCode.length > 0) {
|
|
179
|
+
hasGaps = true;
|
|
180
|
+
console.log(chalk.red.bold('Untested Code (no test coverage):'));
|
|
181
|
+
gaps.untestedCode.forEach(code => {
|
|
182
|
+
console.log(chalk.red(` • ${code.file}:${code.function || code.class}`));
|
|
183
|
+
if (options.verbose && code.lines) {
|
|
184
|
+
console.log(chalk.dim(` Lines: ${code.lines}`));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
console.log();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (gaps.missingTests.length > 0) {
|
|
191
|
+
hasGaps = true;
|
|
192
|
+
console.log(chalk.red.bold('Missing Tests (requirements not tested):'));
|
|
193
|
+
gaps.missingTests.forEach(req => {
|
|
194
|
+
console.log(chalk.red(` • ${req.id}: ${req.title}`));
|
|
195
|
+
});
|
|
196
|
+
console.log();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!hasGaps) {
|
|
200
|
+
console.log(chalk.green('✓ No gaps detected - 100% traceability!\n'));
|
|
201
|
+
process.exit(0);
|
|
202
|
+
} else {
|
|
203
|
+
console.log(chalk.bold('Gap Summary:'));
|
|
204
|
+
console.log(chalk.dim(` Orphaned Requirements: ${gaps.orphanedRequirements.length}`));
|
|
205
|
+
console.log(chalk.dim(` Orphaned Design: ${gaps.orphanedDesign.length}`));
|
|
206
|
+
console.log(chalk.dim(` Orphaned Tasks: ${gaps.orphanedTasks.length}`));
|
|
207
|
+
console.log(chalk.dim(` Untested Code: ${gaps.untestedCode.length}`));
|
|
208
|
+
console.log(chalk.dim(` Missing Tests: ${gaps.missingTests.length}`));
|
|
209
|
+
console.log();
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Trace specific requirement
|
|
219
|
+
program
|
|
220
|
+
.command('requirement <id>')
|
|
221
|
+
.description('Trace specific requirement through design, tasks, code, and tests')
|
|
222
|
+
.option('--requirements <path>', 'Requirements directory', 'docs/requirements')
|
|
223
|
+
.option('--design <path>', 'Design directory', 'docs/design')
|
|
224
|
+
.option('--tasks <path>', 'Tasks directory', 'docs/tasks')
|
|
225
|
+
.option('--code <path>', 'Source code directory', 'src')
|
|
226
|
+
.option('--tests <path>', 'Tests directory', 'tests')
|
|
227
|
+
.action(async (id, options) => {
|
|
228
|
+
try {
|
|
229
|
+
console.log(chalk.bold(`\n🔗 Tracing Requirement: ${id}\n`));
|
|
230
|
+
|
|
231
|
+
const analyzer = new TraceabilityAnalyzer(process.cwd());
|
|
232
|
+
const trace = await analyzer.traceRequirement(id, options);
|
|
233
|
+
|
|
234
|
+
if (!trace.requirement) {
|
|
235
|
+
console.log(chalk.red(`✗ Requirement ${id} not found\n`));
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
console.log(chalk.bold('Requirement:'));
|
|
240
|
+
console.log(chalk.cyan(` ${trace.requirement.id}: ${trace.requirement.title}`));
|
|
241
|
+
console.log(chalk.dim(` ${trace.requirement.file}`));
|
|
242
|
+
console.log();
|
|
243
|
+
|
|
244
|
+
if (trace.design.length > 0) {
|
|
245
|
+
console.log(chalk.bold('Design:'));
|
|
246
|
+
trace.design.forEach(d => {
|
|
247
|
+
console.log(chalk.green(` ✓ ${d.id}: ${d.title}`));
|
|
248
|
+
console.log(chalk.dim(` ${d.file}`));
|
|
249
|
+
});
|
|
250
|
+
console.log();
|
|
251
|
+
} else {
|
|
252
|
+
console.log(chalk.yellow('⚠ No design found\n'));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (trace.tasks.length > 0) {
|
|
256
|
+
console.log(chalk.bold('Tasks:'));
|
|
257
|
+
trace.tasks.forEach(t => {
|
|
258
|
+
console.log(chalk.green(` ✓ ${t.id}: ${t.title} (${t.status})`));
|
|
259
|
+
console.log(chalk.dim(` ${t.file}`));
|
|
260
|
+
});
|
|
261
|
+
console.log();
|
|
262
|
+
} else {
|
|
263
|
+
console.log(chalk.yellow('⚠ No tasks found\n'));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (trace.code.length > 0) {
|
|
267
|
+
console.log(chalk.bold('Code:'));
|
|
268
|
+
trace.code.forEach(c => {
|
|
269
|
+
console.log(chalk.green(` ✓ ${c.file}:${c.function || c.class}`));
|
|
270
|
+
console.log(chalk.dim(` Lines: ${c.lines}`));
|
|
271
|
+
});
|
|
272
|
+
console.log();
|
|
273
|
+
} else {
|
|
274
|
+
console.log(chalk.yellow('⚠ No code implementation found\n'));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (trace.tests.length > 0) {
|
|
278
|
+
console.log(chalk.bold('Tests:'));
|
|
279
|
+
trace.tests.forEach(t => {
|
|
280
|
+
console.log(chalk.green(` ✓ ${t.file}:${t.test}`));
|
|
281
|
+
console.log(chalk.dim(` Status: ${t.status || 'passing'}`));
|
|
282
|
+
});
|
|
283
|
+
console.log();
|
|
284
|
+
} else {
|
|
285
|
+
console.log(chalk.red('✗ No tests found\n'));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const coverage = {
|
|
289
|
+
design: trace.design.length > 0,
|
|
290
|
+
tasks: trace.tasks.length > 0,
|
|
291
|
+
code: trace.code.length > 0,
|
|
292
|
+
tests: trace.tests.length > 0
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const coveragePercent = Object.values(coverage).filter(Boolean).length * 25;
|
|
296
|
+
const coverageColor = coveragePercent === 100 ? chalk.green : coveragePercent >= 75 ? chalk.yellow : chalk.red;
|
|
297
|
+
|
|
298
|
+
console.log(chalk.bold('Coverage:'));
|
|
299
|
+
console.log(coverageColor(` ${coveragePercent}% (${Object.values(coverage).filter(Boolean).length}/4 stages)`));
|
|
300
|
+
console.log();
|
|
301
|
+
|
|
302
|
+
process.exit(coveragePercent === 100 ? 0 : 1);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Validate 100% coverage
|
|
310
|
+
program
|
|
311
|
+
.command('validate')
|
|
312
|
+
.description('Validate 100% traceability coverage (Constitutional Article V)')
|
|
313
|
+
.option('--requirements <path>', 'Requirements directory', 'docs/requirements')
|
|
314
|
+
.option('--design <path>', 'Design directory', 'docs/design')
|
|
315
|
+
.option('--tasks <path>', 'Tasks directory', 'docs/tasks')
|
|
316
|
+
.option('--code <path>', 'Source code directory', 'src')
|
|
317
|
+
.option('--tests <path>', 'Tests directory', 'tests')
|
|
318
|
+
.option('--strict', 'Fail on any gaps (default: true)', true)
|
|
319
|
+
.action(async (options) => {
|
|
320
|
+
try {
|
|
321
|
+
console.log(chalk.bold('\n🔍 Validating Traceability (Article V)\n'));
|
|
322
|
+
|
|
323
|
+
const analyzer = new TraceabilityAnalyzer(process.cwd());
|
|
324
|
+
const validation = await analyzer.validate(options);
|
|
325
|
+
|
|
326
|
+
console.log(chalk.bold('Article V: Complete Traceability'));
|
|
327
|
+
console.log();
|
|
328
|
+
|
|
329
|
+
if (validation.passed) {
|
|
330
|
+
console.log(chalk.green('✓ 100% traceability achieved'));
|
|
331
|
+
console.log(chalk.dim(` Requirements: ${validation.coverage.totalRequirements}`));
|
|
332
|
+
console.log(chalk.dim(` Design Coverage: ${validation.coverage.designCoverage}%`));
|
|
333
|
+
console.log(chalk.dim(` Tasks Coverage: ${validation.coverage.tasksCoverage}%`));
|
|
334
|
+
console.log(chalk.dim(` Code Coverage: ${validation.coverage.codeCoverage}%`));
|
|
335
|
+
console.log(chalk.dim(` Test Coverage: ${validation.coverage.testsCoverage}%`));
|
|
336
|
+
console.log();
|
|
337
|
+
process.exit(0);
|
|
338
|
+
} else {
|
|
339
|
+
console.log(chalk.red('✗ Traceability gaps detected'));
|
|
340
|
+
console.log();
|
|
341
|
+
|
|
342
|
+
if (validation.gaps.orphanedRequirements.length > 0) {
|
|
343
|
+
console.log(chalk.red(` Orphaned Requirements: ${validation.gaps.orphanedRequirements.length}`));
|
|
344
|
+
}
|
|
345
|
+
if (validation.gaps.untestedCode.length > 0) {
|
|
346
|
+
console.log(chalk.red(` Untested Code: ${validation.gaps.untestedCode.length}`));
|
|
347
|
+
}
|
|
348
|
+
if (validation.gaps.missingTests.length > 0) {
|
|
349
|
+
console.log(chalk.red(` Missing Tests: ${validation.gaps.missingTests.length}`));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
console.log();
|
|
353
|
+
console.log(chalk.dim('Run `musubi-trace gaps` for detailed gap analysis'));
|
|
354
|
+
console.log();
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
program.parse(process.argv);
|
|
364
|
+
|
|
365
|
+
if (!process.argv.slice(2).length) {
|
|
366
|
+
program.outputHelp();
|
|
367
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musubi-sdd",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "Ultimate Specification Driven Development Tool with 25 Agents for 7 AI Coding Platforms (Claude Code, GitHub Copilot, Cursor, Gemini CLI, Windsurf, Codex, Qwen Code)",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"musubi-validate": "bin/musubi-validate.js",
|
|
15
15
|
"musubi-requirements": "bin/musubi-requirements.js",
|
|
16
16
|
"musubi-design": "bin/musubi-design.js",
|
|
17
|
-
"musubi-tasks": "bin/musubi-tasks.js"
|
|
17
|
+
"musubi-tasks": "bin/musubi-tasks.js",
|
|
18
|
+
"musubi-trace": "bin/musubi-trace.js"
|
|
18
19
|
},
|
|
19
20
|
"scripts": {
|
|
20
21
|
"test": "jest",
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MUSUBI Traceability Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Provides end-to-end traceability from requirements to code to tests
|
|
5
|
+
* Implements Constitutional Article V: Complete Traceability
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const glob = require('glob');
|
|
11
|
+
|
|
12
|
+
class TraceabilityAnalyzer {
|
|
13
|
+
constructor(workspaceRoot) {
|
|
14
|
+
this.workspaceRoot = workspaceRoot;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate full traceability matrix
|
|
19
|
+
*/
|
|
20
|
+
async generateMatrix(options = {}) {
|
|
21
|
+
const requirements = await this.findRequirements(options.requirements || 'docs/requirements');
|
|
22
|
+
const design = await this.findDesign(options.design || 'docs/design');
|
|
23
|
+
const tasks = await this.findTasks(options.tasks || 'docs/tasks');
|
|
24
|
+
const code = await this.findCode(options.code || 'src');
|
|
25
|
+
const tests = await this.findTests(options.tests || 'tests');
|
|
26
|
+
|
|
27
|
+
const matrix = [];
|
|
28
|
+
|
|
29
|
+
for (const req of requirements) {
|
|
30
|
+
const relatedDesign = design.filter(d => this.linksToRequirement(d, req.id));
|
|
31
|
+
const relatedTasks = tasks.filter(t => this.linksToRequirement(t, req.id));
|
|
32
|
+
const relatedCode = code.filter(c => this.linksToRequirement(c, req.id));
|
|
33
|
+
const relatedTests = tests.filter(t => this.linksToRequirement(t, req.id));
|
|
34
|
+
|
|
35
|
+
matrix.push({
|
|
36
|
+
requirement: req,
|
|
37
|
+
design: relatedDesign,
|
|
38
|
+
tasks: relatedTasks,
|
|
39
|
+
code: relatedCode,
|
|
40
|
+
tests: relatedTests,
|
|
41
|
+
coverage: {
|
|
42
|
+
design: relatedDesign.length > 0,
|
|
43
|
+
tasks: relatedTasks.length > 0,
|
|
44
|
+
code: relatedCode.length > 0,
|
|
45
|
+
tests: relatedTests.length > 0
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const summary = this.calculateMatrixSummary(matrix);
|
|
51
|
+
|
|
52
|
+
return { matrix, summary };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Calculate coverage statistics
|
|
57
|
+
*/
|
|
58
|
+
async calculateCoverage(options = {}) {
|
|
59
|
+
const { matrix, summary } = await this.generateMatrix(options);
|
|
60
|
+
|
|
61
|
+
const overall = Math.round(
|
|
62
|
+
(summary.designCoverage + summary.tasksCoverage + summary.codeCoverage + summary.testsCoverage) / 4
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
totalRequirements: summary.totalRequirements,
|
|
67
|
+
withDesign: summary.withDesign,
|
|
68
|
+
withTasks: summary.withTasks,
|
|
69
|
+
withCode: summary.withCode,
|
|
70
|
+
withTests: summary.withTests,
|
|
71
|
+
designCoverage: summary.designCoverage,
|
|
72
|
+
tasksCoverage: summary.tasksCoverage,
|
|
73
|
+
codeCoverage: summary.codeCoverage,
|
|
74
|
+
testsCoverage: summary.testsCoverage,
|
|
75
|
+
overall
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Detect gaps in traceability
|
|
81
|
+
*/
|
|
82
|
+
async detectGaps(options = {}) {
|
|
83
|
+
const requirements = await this.findRequirements(options.requirements || 'docs/requirements');
|
|
84
|
+
const design = await this.findDesign(options.design || 'docs/design');
|
|
85
|
+
const tasks = await this.findTasks(options.tasks || 'docs/tasks');
|
|
86
|
+
const code = await this.findCode(options.code || 'src');
|
|
87
|
+
const tests = await this.findTests(options.tests || 'tests');
|
|
88
|
+
|
|
89
|
+
const orphanedRequirements = [];
|
|
90
|
+
const orphanedDesign = [];
|
|
91
|
+
const orphanedTasks = [];
|
|
92
|
+
const untestedCode = [];
|
|
93
|
+
const missingTests = [];
|
|
94
|
+
|
|
95
|
+
// Find orphaned requirements (no design or tasks)
|
|
96
|
+
for (const req of requirements) {
|
|
97
|
+
const hasDesign = design.some(d => this.linksToRequirement(d, req.id));
|
|
98
|
+
const hasTasks = tasks.some(t => this.linksToRequirement(t, req.id));
|
|
99
|
+
|
|
100
|
+
if (!hasDesign && !hasTasks) {
|
|
101
|
+
orphanedRequirements.push(req);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check for missing tests
|
|
105
|
+
const hasTests = tests.some(t => this.linksToRequirement(t, req.id));
|
|
106
|
+
if (!hasTests) {
|
|
107
|
+
missingTests.push(req);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Find orphaned design (no requirements)
|
|
112
|
+
for (const d of design) {
|
|
113
|
+
const hasRequirement = requirements.some(r => this.linksToRequirement(d, r.id));
|
|
114
|
+
if (!hasRequirement) {
|
|
115
|
+
orphanedDesign.push(d);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Find orphaned tasks (no requirements)
|
|
120
|
+
for (const t of tasks) {
|
|
121
|
+
const hasRequirement = requirements.some(r => this.linksToRequirement(t, r.id));
|
|
122
|
+
if (!hasRequirement) {
|
|
123
|
+
orphanedTasks.push(t);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Find untested code
|
|
128
|
+
for (const c of code) {
|
|
129
|
+
const hasTest = tests.some(t => this.testCoversCode(t, c));
|
|
130
|
+
if (!hasTest) {
|
|
131
|
+
untestedCode.push(c);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
orphanedRequirements,
|
|
137
|
+
orphanedDesign,
|
|
138
|
+
orphanedTasks,
|
|
139
|
+
untestedCode,
|
|
140
|
+
missingTests
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Trace specific requirement
|
|
146
|
+
*/
|
|
147
|
+
async traceRequirement(requirementId, options = {}) {
|
|
148
|
+
const requirements = await this.findRequirements(options.requirements || 'docs/requirements');
|
|
149
|
+
const design = await this.findDesign(options.design || 'docs/design');
|
|
150
|
+
const tasks = await this.findTasks(options.tasks || 'docs/tasks');
|
|
151
|
+
const code = await this.findCode(options.code || 'src');
|
|
152
|
+
const tests = await this.findTests(options.tests || 'tests');
|
|
153
|
+
|
|
154
|
+
const requirement = requirements.find(r => r.id === requirementId);
|
|
155
|
+
if (!requirement) {
|
|
156
|
+
return { requirement: null, design: [], tasks: [], code: [], tests: [] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const relatedDesign = design.filter(d => this.linksToRequirement(d, requirementId));
|
|
160
|
+
const relatedTasks = tasks.filter(t => this.linksToRequirement(t, requirementId));
|
|
161
|
+
const relatedCode = code.filter(c => this.linksToRequirement(c, requirementId));
|
|
162
|
+
const relatedTests = tests.filter(t => this.linksToRequirement(t, requirementId));
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
requirement,
|
|
166
|
+
design: relatedDesign,
|
|
167
|
+
tasks: relatedTasks,
|
|
168
|
+
code: relatedCode,
|
|
169
|
+
tests: relatedTests
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Validate 100% traceability coverage
|
|
175
|
+
*/
|
|
176
|
+
async validate(options = {}) {
|
|
177
|
+
const coverage = await this.calculateCoverage(options);
|
|
178
|
+
const gaps = await this.detectGaps(options);
|
|
179
|
+
|
|
180
|
+
const passed = coverage.overall === 100 &&
|
|
181
|
+
gaps.orphanedRequirements.length === 0 &&
|
|
182
|
+
gaps.untestedCode.length === 0 &&
|
|
183
|
+
gaps.missingTests.length === 0;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
passed,
|
|
187
|
+
coverage,
|
|
188
|
+
gaps
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Format matrix for output
|
|
194
|
+
*/
|
|
195
|
+
formatMatrix(matrixData, format = 'table') {
|
|
196
|
+
const { matrix, summary } = matrixData;
|
|
197
|
+
|
|
198
|
+
if (format === 'json') {
|
|
199
|
+
return JSON.stringify(matrixData, null, 2);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (format === 'html') {
|
|
203
|
+
return this.formatMatrixHTML(matrix, summary);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (format === 'markdown') {
|
|
207
|
+
return this.formatMatrixMarkdown(matrix, summary);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Default: table format
|
|
211
|
+
return this.formatMatrixTable(matrix, summary);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Format matrix as table
|
|
216
|
+
*/
|
|
217
|
+
formatMatrixTable(matrix, _summary) {
|
|
218
|
+
const chalk = require('chalk');
|
|
219
|
+
let output = '';
|
|
220
|
+
|
|
221
|
+
output += chalk.bold('Requirement Traceability Matrix\n\n');
|
|
222
|
+
output += chalk.dim('REQ ID'.padEnd(20) + 'Design'.padEnd(10) + 'Tasks'.padEnd(10) + 'Code'.padEnd(10) + 'Tests'.padEnd(10)) + '\n';
|
|
223
|
+
output += chalk.dim('-'.repeat(60)) + '\n';
|
|
224
|
+
|
|
225
|
+
matrix.forEach(row => {
|
|
226
|
+
const designIcon = row.coverage.design ? chalk.green('✓') : chalk.red('✗');
|
|
227
|
+
const tasksIcon = row.coverage.tasks ? chalk.green('✓') : chalk.red('✗');
|
|
228
|
+
const codeIcon = row.coverage.code ? chalk.green('✓') : chalk.red('✗');
|
|
229
|
+
const testsIcon = row.coverage.tests ? chalk.green('✓') : chalk.red('✗');
|
|
230
|
+
|
|
231
|
+
output += `${row.requirement.id.padEnd(20)}${designIcon.padEnd(10)}${tasksIcon.padEnd(10)}${codeIcon.padEnd(10)}${testsIcon.padEnd(10)}\n`;
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return output;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Format matrix as markdown
|
|
239
|
+
*/
|
|
240
|
+
formatMatrixMarkdown(matrix, summary) {
|
|
241
|
+
let output = '# Requirement Traceability Matrix\n\n';
|
|
242
|
+
output += `Generated: ${new Date().toISOString()}\n\n`;
|
|
243
|
+
output += '## Summary\n\n';
|
|
244
|
+
output += `- Total Requirements: ${summary.totalRequirements}\n`;
|
|
245
|
+
output += `- Design Coverage: ${summary.designCoverage}%\n`;
|
|
246
|
+
output += `- Tasks Coverage: ${summary.tasksCoverage}%\n`;
|
|
247
|
+
output += `- Code Coverage: ${summary.codeCoverage}%\n`;
|
|
248
|
+
output += `- Test Coverage: ${summary.testsCoverage}%\n\n`;
|
|
249
|
+
output += '## Matrix\n\n';
|
|
250
|
+
output += '| Requirement ID | Title | Design | Tasks | Code | Tests |\n';
|
|
251
|
+
output += '|---------------|-------|--------|-------|------|-------|\n';
|
|
252
|
+
|
|
253
|
+
matrix.forEach(row => {
|
|
254
|
+
const designIcon = row.coverage.design ? '✓' : '✗';
|
|
255
|
+
const tasksIcon = row.coverage.tasks ? '✓' : '✗';
|
|
256
|
+
const codeIcon = row.coverage.code ? '✓' : '✗';
|
|
257
|
+
const testsIcon = row.coverage.tests ? '✓' : '✗';
|
|
258
|
+
|
|
259
|
+
output += `| ${row.requirement.id} | ${row.requirement.title} | ${designIcon} | ${tasksIcon} | ${codeIcon} | ${testsIcon} |\n`;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return output;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Format matrix as HTML
|
|
267
|
+
*/
|
|
268
|
+
formatMatrixHTML(matrix, summary) {
|
|
269
|
+
let html = '<!DOCTYPE html>\n<html>\n<head>\n';
|
|
270
|
+
html += '<title>Requirement Traceability Matrix</title>\n';
|
|
271
|
+
html += '<style>\n';
|
|
272
|
+
html += 'body { font-family: Arial, sans-serif; margin: 20px; }\n';
|
|
273
|
+
html += 'table { border-collapse: collapse; width: 100%; }\n';
|
|
274
|
+
html += 'th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }\n';
|
|
275
|
+
html += 'th { background-color: #4CAF50; color: white; }\n';
|
|
276
|
+
html += '.pass { color: green; }\n';
|
|
277
|
+
html += '.fail { color: red; }\n';
|
|
278
|
+
html += '</style>\n</head>\n<body>\n';
|
|
279
|
+
html += '<h1>Requirement Traceability Matrix</h1>\n';
|
|
280
|
+
html += `<p>Generated: ${new Date().toISOString()}</p>\n`;
|
|
281
|
+
html += '<h2>Summary</h2>\n<ul>\n';
|
|
282
|
+
html += `<li>Total Requirements: ${summary.totalRequirements}</li>\n`;
|
|
283
|
+
html += `<li>Design Coverage: ${summary.designCoverage}%</li>\n`;
|
|
284
|
+
html += `<li>Tasks Coverage: ${summary.tasksCoverage}%</li>\n`;
|
|
285
|
+
html += `<li>Code Coverage: ${summary.codeCoverage}%</li>\n`;
|
|
286
|
+
html += `<li>Test Coverage: ${summary.testsCoverage}%</li>\n`;
|
|
287
|
+
html += '</ul>\n<h2>Matrix</h2>\n';
|
|
288
|
+
html += '<table>\n<tr><th>Requirement ID</th><th>Title</th><th>Design</th><th>Tasks</th><th>Code</th><th>Tests</th></tr>\n';
|
|
289
|
+
|
|
290
|
+
matrix.forEach(row => {
|
|
291
|
+
const designClass = row.coverage.design ? 'pass' : 'fail';
|
|
292
|
+
const tasksClass = row.coverage.tasks ? 'pass' : 'fail';
|
|
293
|
+
const codeClass = row.coverage.code ? 'pass' : 'fail';
|
|
294
|
+
const testsClass = row.coverage.tests ? 'pass' : 'fail';
|
|
295
|
+
|
|
296
|
+
html += `<tr><td>${row.requirement.id}</td><td>${row.requirement.title}</td>`;
|
|
297
|
+
html += `<td class="${designClass}">${row.coverage.design ? '✓' : '✗'}</td>`;
|
|
298
|
+
html += `<td class="${tasksClass}">${row.coverage.tasks ? '✓' : '✗'}</td>`;
|
|
299
|
+
html += `<td class="${codeClass}">${row.coverage.code ? '✓' : '✗'}</td>`;
|
|
300
|
+
html += `<td class="${testsClass}">${row.coverage.tests ? '✓' : '✗'}</td></tr>\n`;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
html += '</table>\n</body>\n</html>';
|
|
304
|
+
return html;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Calculate matrix summary statistics
|
|
309
|
+
*/
|
|
310
|
+
calculateMatrixSummary(matrix) {
|
|
311
|
+
const totalRequirements = matrix.length;
|
|
312
|
+
const withDesign = matrix.filter(r => r.coverage.design).length;
|
|
313
|
+
const withTasks = matrix.filter(r => r.coverage.tasks).length;
|
|
314
|
+
const withCode = matrix.filter(r => r.coverage.code).length;
|
|
315
|
+
const withTests = matrix.filter(r => r.coverage.tests).length;
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
totalRequirements,
|
|
319
|
+
withDesign,
|
|
320
|
+
withTasks,
|
|
321
|
+
withCode,
|
|
322
|
+
withTests,
|
|
323
|
+
designCoverage: totalRequirements > 0 ? Math.round((withDesign / totalRequirements) * 100) : 0,
|
|
324
|
+
tasksCoverage: totalRequirements > 0 ? Math.round((withTasks / totalRequirements) * 100) : 0,
|
|
325
|
+
codeCoverage: totalRequirements > 0 ? Math.round((withCode / totalRequirements) * 100) : 0,
|
|
326
|
+
testsCoverage: totalRequirements > 0 ? Math.round((withTests / totalRequirements) * 100) : 0
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Find all requirements
|
|
332
|
+
*/
|
|
333
|
+
async findRequirements(reqDir) {
|
|
334
|
+
const reqPath = path.join(this.workspaceRoot, reqDir);
|
|
335
|
+
if (!await fs.pathExists(reqPath)) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const files = glob.sync('**/*.md', { cwd: reqPath, absolute: true });
|
|
340
|
+
const requirements = [];
|
|
341
|
+
|
|
342
|
+
for (const file of files) {
|
|
343
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
344
|
+
const reqMatches = content.matchAll(/### (REQ-[A-Z0-9]+-\d{3}): (.+)/g);
|
|
345
|
+
|
|
346
|
+
for (const match of reqMatches) {
|
|
347
|
+
requirements.push({
|
|
348
|
+
id: match[1],
|
|
349
|
+
title: match[2],
|
|
350
|
+
file: path.relative(this.workspaceRoot, file),
|
|
351
|
+
content
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return requirements;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Find all design documents
|
|
361
|
+
*/
|
|
362
|
+
async findDesign(designDir) {
|
|
363
|
+
const designPath = path.join(this.workspaceRoot, designDir);
|
|
364
|
+
if (!await fs.pathExists(designPath)) {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const files = glob.sync('**/*.md', { cwd: designPath, absolute: true });
|
|
369
|
+
const designs = [];
|
|
370
|
+
|
|
371
|
+
for (const file of files) {
|
|
372
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
373
|
+
|
|
374
|
+
// Extract C4 diagrams and ADRs
|
|
375
|
+
const c4Matches = content.matchAll(/### (Level \d: .+)/g);
|
|
376
|
+
const adrMatches = content.matchAll(/### (ADR-\d{3}): (.+)/g);
|
|
377
|
+
|
|
378
|
+
for (const match of c4Matches) {
|
|
379
|
+
designs.push({
|
|
380
|
+
id: match[1],
|
|
381
|
+
title: match[1],
|
|
382
|
+
type: 'C4',
|
|
383
|
+
file: path.relative(this.workspaceRoot, file),
|
|
384
|
+
content
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for (const match of adrMatches) {
|
|
389
|
+
designs.push({
|
|
390
|
+
id: match[1],
|
|
391
|
+
title: match[2],
|
|
392
|
+
type: 'ADR',
|
|
393
|
+
file: path.relative(this.workspaceRoot, file),
|
|
394
|
+
content
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return designs;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Find all tasks
|
|
404
|
+
*/
|
|
405
|
+
async findTasks(tasksDir) {
|
|
406
|
+
const tasksPath = path.join(this.workspaceRoot, tasksDir);
|
|
407
|
+
if (!await fs.pathExists(tasksPath)) {
|
|
408
|
+
return [];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const files = glob.sync('**/*.md', { cwd: tasksPath, absolute: true });
|
|
412
|
+
const tasks = [];
|
|
413
|
+
|
|
414
|
+
for (const file of files) {
|
|
415
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
416
|
+
const taskMatches = content.matchAll(/### (TASK-\d{3}): (.+)/g);
|
|
417
|
+
|
|
418
|
+
for (const match of taskMatches) {
|
|
419
|
+
// Extract status - look for **Status**: pattern after task heading
|
|
420
|
+
const taskSection = content.substring(match.index);
|
|
421
|
+
const statusMatch = taskSection.match(/\*\*Status\*\*:\s*(\w+)/);
|
|
422
|
+
|
|
423
|
+
tasks.push({
|
|
424
|
+
id: match[1],
|
|
425
|
+
title: match[2],
|
|
426
|
+
status: statusMatch ? statusMatch[1] : 'Unknown',
|
|
427
|
+
file: path.relative(this.workspaceRoot, file),
|
|
428
|
+
content
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return tasks;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Find all code files
|
|
438
|
+
*/
|
|
439
|
+
async findCode(codeDir) {
|
|
440
|
+
const codePath = path.join(this.workspaceRoot, codeDir);
|
|
441
|
+
if (!await fs.pathExists(codePath)) {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const files = glob.sync('**/*.{js,ts,jsx,tsx,py,java,go,rs}', { cwd: codePath, absolute: true });
|
|
446
|
+
const code = [];
|
|
447
|
+
|
|
448
|
+
for (const file of files) {
|
|
449
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
450
|
+
|
|
451
|
+
// Extract functions and classes with REQ references
|
|
452
|
+
const functionMatches = content.matchAll(/(?:function|const|let|var)\s+(\w+)|class\s+(\w+)/g);
|
|
453
|
+
|
|
454
|
+
for (const match of functionMatches) {
|
|
455
|
+
code.push({
|
|
456
|
+
file: path.relative(this.workspaceRoot, file),
|
|
457
|
+
function: match[1],
|
|
458
|
+
class: match[2],
|
|
459
|
+
lines: this.getLineNumber(content, match.index),
|
|
460
|
+
content
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return code;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Find all test files
|
|
470
|
+
*/
|
|
471
|
+
async findTests(testsDir) {
|
|
472
|
+
const testsPath = path.join(this.workspaceRoot, testsDir);
|
|
473
|
+
if (!await fs.pathExists(testsPath)) {
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const files = glob.sync('**/*.{test,spec}.{js,ts,jsx,tsx,py,java,go,rs}', { cwd: testsPath, absolute: true });
|
|
478
|
+
const tests = [];
|
|
479
|
+
|
|
480
|
+
for (const file of files) {
|
|
481
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
482
|
+
|
|
483
|
+
// Extract test cases
|
|
484
|
+
const testMatches = content.matchAll(/(?:test|it|describe)\(['"](.+?)['"]/g);
|
|
485
|
+
|
|
486
|
+
for (const match of testMatches) {
|
|
487
|
+
tests.push({
|
|
488
|
+
file: path.relative(this.workspaceRoot, file),
|
|
489
|
+
test: match[1],
|
|
490
|
+
content
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return tests;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Check if document links to requirement
|
|
500
|
+
*/
|
|
501
|
+
linksToRequirement(doc, requirementId) {
|
|
502
|
+
return doc.content.includes(requirementId);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Check if test covers code
|
|
507
|
+
*/
|
|
508
|
+
testCoversCode(test, code) {
|
|
509
|
+
// Simple heuristic: test file path matches code file path
|
|
510
|
+
const testBase = path.basename(test.file, path.extname(test.file)).replace(/\.(test|spec)$/, '');
|
|
511
|
+
const codeBase = path.basename(code.file, path.extname(code.file));
|
|
512
|
+
|
|
513
|
+
return testBase === codeBase || test.content.includes(code.function) || test.content.includes(code.class);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Get line number from content index
|
|
518
|
+
*/
|
|
519
|
+
getLineNumber(content, index) {
|
|
520
|
+
return content.substring(0, index).split('\n').length;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
module.exports = TraceabilityAnalyzer;
|