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 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.4",
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;