musubi-sdd 0.8.6 → 0.8.7
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 +10 -0
- package/README.md +10 -0
- package/bin/musubi-gaps.js +199 -0
- package/package.json +2 -1
- package/src/analyzers/gap-detector.js +508 -0
package/README.ja.md
CHANGED
|
@@ -16,6 +16,7 @@ MUSUBIは、6つの主要フレームワークのベスト機能を統合した
|
|
|
16
16
|
- 📝 **EARS要件ジェネレーター** - 5つのEARSパターンで明確な要件を作成(v0.8.0)
|
|
17
17
|
- 🏗️ **設計ドキュメントジェネレーター** - トレーサビリティ付きC4モデルとADRを作成(v0.8.2)
|
|
18
18
|
- 🔄 **変更管理システム** - ブラウンフィールドプロジェクト向け差分仕様(v0.8.6)
|
|
19
|
+
- 🔍 **ギャップ検出システム** - 孤立した要件とテストされていないコードを特定(v0.8.7)
|
|
19
20
|
- 🧭 **自動更新プロジェクトメモリ** - ステアリングシステムがアーキテクチャ、技術スタック、製品コンテキストを維持
|
|
20
21
|
- 🚀 **自動オンボーディング** - `musubi-onboard` が既存プロジェクトを分析し、ステアリングドキュメントを生成(2-5分)
|
|
21
22
|
- 🔄 **自動同期** - `musubi-sync` がコードベースの変更を検出し、ステアリングドキュメントを最新に保つ
|
|
@@ -147,6 +148,15 @@ musubi-change apply CHANGE-001 # コードベースに変更を
|
|
|
147
148
|
musubi-change archive CHANGE-001 # specs/にアーカイブ
|
|
148
149
|
musubi-change list --status pending # 保留中の変更をリスト
|
|
149
150
|
musubi-change list --format json # JSON形式でリスト
|
|
151
|
+
|
|
152
|
+
# ギャップ検出とカバレッジ検証(v0.8.7)
|
|
153
|
+
musubi-gaps detect # 全ギャップを検出
|
|
154
|
+
musubi-gaps detect --verbose # 詳細なギャップ情報を表示
|
|
155
|
+
musubi-gaps requirements # 孤立した要件を検出
|
|
156
|
+
musubi-gaps code # テストされていないコードを検出
|
|
157
|
+
musubi-gaps coverage # カバレッジ統計を計算
|
|
158
|
+
musubi-gaps coverage --min-coverage 100 # 100%カバレッジを要求
|
|
159
|
+
musubi-gaps detect --format markdown > gaps.md # ギャップレポートをエクスポート
|
|
150
160
|
```
|
|
151
161
|
|
|
152
162
|
### プロジェクトタイプ
|
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ MUSUBI is a comprehensive SDD (Specification Driven Development) framework that
|
|
|
20
20
|
- 📝 **EARS Requirements Generator** - Create unambiguous requirements with 5 EARS patterns (v0.8.0)
|
|
21
21
|
- 🏗️ **Design Document Generator** - Create C4 models and ADRs with traceability (v0.8.2)
|
|
22
22
|
- 🔄 **Change Management System** - Delta specifications for brownfield projects (v0.8.6)
|
|
23
|
+
- 🔍 **Gap Detection System** - Identify orphaned requirements and untested code (v0.8.7)
|
|
23
24
|
- 🧭 **Auto-Updating Project Memory** - Steering system maintains architecture, tech stack, and product context
|
|
24
25
|
- 🚀 **Automatic Onboarding** - `musubi-onboard` analyzes existing projects and generates steering docs (2-5 minutes)
|
|
25
26
|
- 🔄 **Auto-Sync** - `musubi-sync` detects codebase changes and keeps steering docs current
|
|
@@ -151,6 +152,15 @@ musubi-change apply CHANGE-001 # Apply changes to codebase
|
|
|
151
152
|
musubi-change archive CHANGE-001 # Archive to specs/
|
|
152
153
|
musubi-change list --status pending # List pending changes
|
|
153
154
|
musubi-change list --format json # List in JSON format
|
|
155
|
+
|
|
156
|
+
# Gap detection and coverage validation (v0.8.7)
|
|
157
|
+
musubi-gaps detect # Detect all gaps
|
|
158
|
+
musubi-gaps detect --verbose # Show detailed gap information
|
|
159
|
+
musubi-gaps requirements # Detect orphaned requirements
|
|
160
|
+
musubi-gaps code # Detect untested code
|
|
161
|
+
musubi-gaps coverage # Calculate coverage statistics
|
|
162
|
+
musubi-gaps coverage --min-coverage 100 # Require 100% coverage
|
|
163
|
+
musubi-gaps detect --format markdown > gaps.md # Export gap report
|
|
154
164
|
```
|
|
155
165
|
|
|
156
166
|
### Project Types
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MUSUBI Gap Detection CLI
|
|
5
|
+
* Identifies orphaned requirements, untested code, and missing traceability
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { Command } = require('commander');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const GapDetector = require('../src/analyzers/gap-detector');
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('musubi-gaps')
|
|
16
|
+
.description('Detect gaps in requirements, code, and test coverage')
|
|
17
|
+
.version('0.8.7');
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.command('detect')
|
|
21
|
+
.description('Detect all gaps (requirements, code, tests)')
|
|
22
|
+
.option('--requirements <dir>', 'Requirements directory', 'docs/requirements')
|
|
23
|
+
.option('--design <dir>', 'Design directory', 'docs/design')
|
|
24
|
+
.option('--tasks <dir>', 'Tasks directory', 'docs/tasks')
|
|
25
|
+
.option('--src <dir>', 'Source code directory', 'src')
|
|
26
|
+
.option('--tests <dir>', 'Test directory', 'tests')
|
|
27
|
+
.option('--format <format>', 'Output format (table|json|markdown)', 'table')
|
|
28
|
+
.option('--verbose', 'Show detailed gap information')
|
|
29
|
+
.action(async (options) => {
|
|
30
|
+
try {
|
|
31
|
+
console.log(chalk.blue('\n🔍 Detecting gaps in traceability...\n'));
|
|
32
|
+
|
|
33
|
+
const detector = new GapDetector({
|
|
34
|
+
requirementsDir: options.requirements,
|
|
35
|
+
designDir: options.design,
|
|
36
|
+
tasksDir: options.tasks,
|
|
37
|
+
srcDir: options.src,
|
|
38
|
+
testsDir: options.tests
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const gaps = await detector.detectAllGaps();
|
|
42
|
+
|
|
43
|
+
if (options.format === 'json') {
|
|
44
|
+
console.log(JSON.stringify(gaps, null, 2));
|
|
45
|
+
} else if (options.format === 'markdown') {
|
|
46
|
+
console.log(detector.formatMarkdown(gaps));
|
|
47
|
+
} else {
|
|
48
|
+
detector.displayTable(gaps, options.verbose);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const totalGaps = gaps.orphanedRequirements.length +
|
|
52
|
+
gaps.unimplementedRequirements.length +
|
|
53
|
+
gaps.untestedCode.length +
|
|
54
|
+
gaps.missingTests.length;
|
|
55
|
+
|
|
56
|
+
if (totalGaps === 0) {
|
|
57
|
+
console.log(chalk.green('\n✓ No gaps detected! 100% traceability achieved.\n'));
|
|
58
|
+
process.exit(0);
|
|
59
|
+
} else {
|
|
60
|
+
console.log(chalk.yellow(`\n⚠ Found ${totalGaps} gap(s). See details above.\n`));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(chalk.red(`\n✗ Error: ${error.message}\n`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
program
|
|
70
|
+
.command('requirements')
|
|
71
|
+
.description('Detect orphaned requirements (no design/code)')
|
|
72
|
+
.option('--requirements <dir>', 'Requirements directory', 'docs/requirements')
|
|
73
|
+
.option('--design <dir>', 'Design directory', 'docs/design')
|
|
74
|
+
.option('--tasks <dir>', 'Tasks directory', 'docs/tasks')
|
|
75
|
+
.option('--format <format>', 'Output format (table|json|markdown)', 'table')
|
|
76
|
+
.action(async (options) => {
|
|
77
|
+
try {
|
|
78
|
+
console.log(chalk.blue('\n🔍 Detecting orphaned requirements...\n'));
|
|
79
|
+
|
|
80
|
+
const detector = new GapDetector({
|
|
81
|
+
requirementsDir: options.requirements,
|
|
82
|
+
designDir: options.design,
|
|
83
|
+
tasksDir: options.tasks
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const orphaned = await detector.detectOrphanedRequirements();
|
|
87
|
+
|
|
88
|
+
if (options.format === 'json') {
|
|
89
|
+
console.log(JSON.stringify(orphaned, null, 2));
|
|
90
|
+
} else if (options.format === 'markdown') {
|
|
91
|
+
console.log(detector.formatRequirementsMarkdown(orphaned));
|
|
92
|
+
} else {
|
|
93
|
+
detector.displayRequirementsTable(orphaned);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (orphaned.length === 0) {
|
|
97
|
+
console.log(chalk.green('\n✓ No orphaned requirements detected.\n'));
|
|
98
|
+
process.exit(0);
|
|
99
|
+
} else {
|
|
100
|
+
console.log(chalk.yellow(`\n⚠ Found ${orphaned.length} orphaned requirement(s).\n`));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(chalk.red(`\n✗ Error: ${error.message}\n`));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
program
|
|
110
|
+
.command('code')
|
|
111
|
+
.description('Detect untested code')
|
|
112
|
+
.option('--src <dir>', 'Source code directory', 'src')
|
|
113
|
+
.option('--tests <dir>', 'Test directory', 'tests')
|
|
114
|
+
.option('--format <format>', 'Output format (table|json|markdown)', 'table')
|
|
115
|
+
.action(async (options) => {
|
|
116
|
+
try {
|
|
117
|
+
console.log(chalk.blue('\n🔍 Detecting untested code...\n'));
|
|
118
|
+
|
|
119
|
+
const detector = new GapDetector({
|
|
120
|
+
srcDir: options.src,
|
|
121
|
+
testsDir: options.tests
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const untested = await detector.detectUntestedCode();
|
|
125
|
+
|
|
126
|
+
if (options.format === 'json') {
|
|
127
|
+
console.log(JSON.stringify(untested, null, 2));
|
|
128
|
+
} else if (options.format === 'markdown') {
|
|
129
|
+
console.log(detector.formatCodeMarkdown(untested));
|
|
130
|
+
} else {
|
|
131
|
+
detector.displayCodeTable(untested);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (untested.length === 0) {
|
|
135
|
+
console.log(chalk.green('\n✓ All code is tested.\n'));
|
|
136
|
+
process.exit(0);
|
|
137
|
+
} else {
|
|
138
|
+
console.log(chalk.yellow(`\n⚠ Found ${untested.length} untested file(s).\n`));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(chalk.red(`\n✗ Error: ${error.message}\n`));
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
program
|
|
148
|
+
.command('coverage')
|
|
149
|
+
.description('Calculate coverage statistics')
|
|
150
|
+
.option('--requirements <dir>', 'Requirements directory', 'docs/requirements')
|
|
151
|
+
.option('--design <dir>', 'Design directory', 'docs/design')
|
|
152
|
+
.option('--tasks <dir>', 'Tasks directory', 'docs/tasks')
|
|
153
|
+
.option('--src <dir>', 'Source code directory', 'src')
|
|
154
|
+
.option('--tests <dir>', 'Test directory', 'tests')
|
|
155
|
+
.option('--min-coverage <percent>', 'Minimum required coverage', '100')
|
|
156
|
+
.option('--format <format>', 'Output format (table|json|markdown)', 'table')
|
|
157
|
+
.action(async (options) => {
|
|
158
|
+
try {
|
|
159
|
+
console.log(chalk.blue('\n📊 Calculating coverage statistics...\n'));
|
|
160
|
+
|
|
161
|
+
const detector = new GapDetector({
|
|
162
|
+
requirementsDir: options.requirements,
|
|
163
|
+
designDir: options.design,
|
|
164
|
+
tasksDir: options.tasks,
|
|
165
|
+
srcDir: options.src,
|
|
166
|
+
testsDir: options.tests
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const coverage = await detector.calculateCoverage();
|
|
170
|
+
const minCoverage = parseFloat(options.minCoverage);
|
|
171
|
+
|
|
172
|
+
if (options.format === 'json') {
|
|
173
|
+
console.log(JSON.stringify(coverage, null, 2));
|
|
174
|
+
} else if (options.format === 'markdown') {
|
|
175
|
+
console.log(detector.formatCoverageMarkdown(coverage));
|
|
176
|
+
} else {
|
|
177
|
+
detector.displayCoverageTable(coverage);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const avgCoverage = (
|
|
181
|
+
coverage.requirements.implementationCoverage +
|
|
182
|
+
coverage.requirements.testCoverage +
|
|
183
|
+
coverage.code.testCoverage
|
|
184
|
+
) / 3;
|
|
185
|
+
|
|
186
|
+
if (avgCoverage >= minCoverage) {
|
|
187
|
+
console.log(chalk.green(`\n✓ Coverage ${avgCoverage.toFixed(1)}% meets minimum ${minCoverage}%\n`));
|
|
188
|
+
process.exit(0);
|
|
189
|
+
} else {
|
|
190
|
+
console.log(chalk.red(`\n✗ Coverage ${avgCoverage.toFixed(1)}% below minimum ${minCoverage}%\n`));
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error(chalk.red(`\n✗ Error: ${error.message}\n`));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musubi-sdd",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.7",
|
|
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": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"musubi-design": "bin/musubi-design.js",
|
|
17
17
|
"musubi-tasks": "bin/musubi-tasks.js",
|
|
18
18
|
"musubi-trace": "bin/musubi-trace.js",
|
|
19
|
+
"musubi-gaps": "bin/musubi-gaps.js",
|
|
19
20
|
"musubi-change": "bin/musubi-change.js"
|
|
20
21
|
},
|
|
21
22
|
"scripts": {
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const glob = require('glob');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Gap Detector - Identifies orphaned requirements, untested code, and missing traceability
|
|
8
|
+
*/
|
|
9
|
+
class GapDetector {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.requirementsDir = options.requirementsDir || 'docs/requirements';
|
|
12
|
+
this.designDir = options.designDir || 'docs/design';
|
|
13
|
+
this.tasksDir = options.tasksDir || 'docs/tasks';
|
|
14
|
+
this.srcDir = options.srcDir || 'src';
|
|
15
|
+
this.testsDir = options.testsDir || 'tests';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Detect all gaps (requirements, code, tests)
|
|
20
|
+
*/
|
|
21
|
+
async detectAllGaps() {
|
|
22
|
+
const orphanedRequirements = await this.detectOrphanedRequirements();
|
|
23
|
+
const unimplementedRequirements = await this.detectUnimplementedRequirements();
|
|
24
|
+
const untestedCode = await this.detectUntestedCode();
|
|
25
|
+
const missingTests = await this.detectMissingTests();
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
orphanedRequirements,
|
|
29
|
+
unimplementedRequirements,
|
|
30
|
+
untestedCode,
|
|
31
|
+
missingTests,
|
|
32
|
+
summary: {
|
|
33
|
+
total: orphanedRequirements.length + unimplementedRequirements.length +
|
|
34
|
+
untestedCode.length + missingTests.length,
|
|
35
|
+
orphanedRequirements: orphanedRequirements.length,
|
|
36
|
+
unimplementedRequirements: unimplementedRequirements.length,
|
|
37
|
+
untestedCode: untestedCode.length,
|
|
38
|
+
missingTests: missingTests.length
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Detect orphaned requirements (no design/task references)
|
|
45
|
+
*/
|
|
46
|
+
async detectOrphanedRequirements() {
|
|
47
|
+
const requirements = await this.extractRequirements();
|
|
48
|
+
const designRefs = await this.extractDesignReferences();
|
|
49
|
+
const taskRefs = await this.extractTaskReferences();
|
|
50
|
+
|
|
51
|
+
const orphaned = [];
|
|
52
|
+
for (const req of requirements) {
|
|
53
|
+
const hasDesign = designRefs.some(ref => ref === req.id);
|
|
54
|
+
const hasTask = taskRefs.some(ref => ref === req.id);
|
|
55
|
+
|
|
56
|
+
if (!hasDesign && !hasTask) {
|
|
57
|
+
orphaned.push({
|
|
58
|
+
id: req.id,
|
|
59
|
+
title: req.title,
|
|
60
|
+
file: req.file,
|
|
61
|
+
reason: 'No design or task references found'
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return orphaned;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Detect unimplemented requirements (no code references)
|
|
71
|
+
*/
|
|
72
|
+
async detectUnimplementedRequirements() {
|
|
73
|
+
const requirements = await this.extractRequirements();
|
|
74
|
+
const codeRefs = await this.extractCodeReferences();
|
|
75
|
+
|
|
76
|
+
const unimplemented = [];
|
|
77
|
+
for (const req of requirements) {
|
|
78
|
+
const hasCode = codeRefs.some(ref => ref === req.id);
|
|
79
|
+
|
|
80
|
+
if (!hasCode) {
|
|
81
|
+
unimplemented.push({
|
|
82
|
+
id: req.id,
|
|
83
|
+
title: req.title,
|
|
84
|
+
file: req.file,
|
|
85
|
+
reason: 'No code implementation found'
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return unimplemented;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Detect untested code (no test files)
|
|
95
|
+
*/
|
|
96
|
+
async detectUntestedCode() {
|
|
97
|
+
const srcFiles = await this.getSourceFiles();
|
|
98
|
+
const testFiles = await this.getTestFiles();
|
|
99
|
+
|
|
100
|
+
const untested = [];
|
|
101
|
+
for (const srcFile of srcFiles) {
|
|
102
|
+
const baseName = path.basename(srcFile, path.extname(srcFile));
|
|
103
|
+
const hasTest = testFiles.some(testFile => {
|
|
104
|
+
const testBaseName = path.basename(testFile, path.extname(testFile));
|
|
105
|
+
return testBaseName.includes(baseName) || testBaseName.replace('.test', '') === baseName;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!hasTest) {
|
|
109
|
+
untested.push({
|
|
110
|
+
file: srcFile,
|
|
111
|
+
reason: 'No corresponding test file found'
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return untested;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Detect missing tests (requirements without test coverage)
|
|
121
|
+
*/
|
|
122
|
+
async detectMissingTests() {
|
|
123
|
+
const requirements = await this.extractRequirements();
|
|
124
|
+
const testRefs = await this.extractTestReferences();
|
|
125
|
+
|
|
126
|
+
const missing = [];
|
|
127
|
+
for (const req of requirements) {
|
|
128
|
+
const hasTest = testRefs.some(ref => ref === req.id);
|
|
129
|
+
|
|
130
|
+
if (!hasTest) {
|
|
131
|
+
missing.push({
|
|
132
|
+
id: req.id,
|
|
133
|
+
title: req.title,
|
|
134
|
+
file: req.file,
|
|
135
|
+
reason: 'No test coverage found'
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return missing;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Calculate coverage statistics
|
|
145
|
+
*/
|
|
146
|
+
async calculateCoverage() {
|
|
147
|
+
const requirements = await this.extractRequirements();
|
|
148
|
+
const designRefs = await this.extractDesignReferences();
|
|
149
|
+
const taskRefs = await this.extractTaskReferences();
|
|
150
|
+
const codeRefs = await this.extractCodeReferences();
|
|
151
|
+
const testRefs = await this.extractTestReferences();
|
|
152
|
+
const srcFiles = await this.getSourceFiles();
|
|
153
|
+
const testFiles = await this.getTestFiles();
|
|
154
|
+
|
|
155
|
+
const totalReqs = requirements.length;
|
|
156
|
+
const withDesign = requirements.filter(req =>
|
|
157
|
+
designRefs.some(ref => ref === req.id)
|
|
158
|
+
).length;
|
|
159
|
+
const withTasks = requirements.filter(req =>
|
|
160
|
+
taskRefs.some(ref => ref === req.id)
|
|
161
|
+
).length;
|
|
162
|
+
const withCode = requirements.filter(req =>
|
|
163
|
+
codeRefs.some(ref => ref === req.id)
|
|
164
|
+
).length;
|
|
165
|
+
const withTests = requirements.filter(req =>
|
|
166
|
+
testRefs.some(ref => ref === req.id)
|
|
167
|
+
).length;
|
|
168
|
+
|
|
169
|
+
const totalSrc = srcFiles.length;
|
|
170
|
+
const testedSrc = srcFiles.filter(srcFile => {
|
|
171
|
+
const baseName = path.basename(srcFile, path.extname(srcFile));
|
|
172
|
+
return testFiles.some(testFile => {
|
|
173
|
+
const testBaseName = path.basename(testFile, path.extname(testFile));
|
|
174
|
+
return testBaseName.includes(baseName) || testBaseName.replace('.test', '') === baseName;
|
|
175
|
+
});
|
|
176
|
+
}).length;
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
requirements: {
|
|
180
|
+
total: totalReqs,
|
|
181
|
+
withDesign,
|
|
182
|
+
withTasks,
|
|
183
|
+
withCode,
|
|
184
|
+
withTests,
|
|
185
|
+
designCoverage: totalReqs > 0 ? (withDesign / totalReqs) * 100 : 100,
|
|
186
|
+
taskCoverage: totalReqs > 0 ? (withTasks / totalReqs) * 100 : 100,
|
|
187
|
+
implementationCoverage: totalReqs > 0 ? (withCode / totalReqs) * 100 : 100,
|
|
188
|
+
testCoverage: totalReqs > 0 ? (withTests / totalReqs) * 100 : 100
|
|
189
|
+
},
|
|
190
|
+
code: {
|
|
191
|
+
total: totalSrc,
|
|
192
|
+
tested: testedSrc,
|
|
193
|
+
untested: totalSrc - testedSrc,
|
|
194
|
+
testCoverage: totalSrc > 0 ? (testedSrc / totalSrc) * 100 : 100
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Extract requirements from requirements directory
|
|
201
|
+
*/
|
|
202
|
+
async extractRequirements() {
|
|
203
|
+
if (!await fs.pathExists(this.requirementsDir)) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const files = glob.sync(`${this.requirementsDir}/**/*.md`);
|
|
208
|
+
const requirements = [];
|
|
209
|
+
|
|
210
|
+
for (const file of files) {
|
|
211
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
212
|
+
const matches = content.matchAll(/^##\s+(REQ-[A-Z]+-\d+):\s+(.+)$/gm);
|
|
213
|
+
|
|
214
|
+
for (const match of matches) {
|
|
215
|
+
requirements.push({
|
|
216
|
+
id: match[1],
|
|
217
|
+
title: match[2],
|
|
218
|
+
file: path.relative(process.cwd(), file)
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return requirements;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Extract design references from design directory
|
|
228
|
+
*/
|
|
229
|
+
async extractDesignReferences() {
|
|
230
|
+
if (!await fs.pathExists(this.designDir)) {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const files = glob.sync(`${this.designDir}/**/*.md`);
|
|
235
|
+
const references = new Set();
|
|
236
|
+
|
|
237
|
+
for (const file of files) {
|
|
238
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
239
|
+
const matches = content.matchAll(/REQ-[A-Z]+-\d+/g);
|
|
240
|
+
|
|
241
|
+
for (const match of matches) {
|
|
242
|
+
references.add(match[0]);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return Array.from(references);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Extract task references from tasks directory
|
|
251
|
+
*/
|
|
252
|
+
async extractTaskReferences() {
|
|
253
|
+
if (!await fs.pathExists(this.tasksDir)) {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const files = glob.sync(`${this.tasksDir}/**/*.md`);
|
|
258
|
+
const references = new Set();
|
|
259
|
+
|
|
260
|
+
for (const file of files) {
|
|
261
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
262
|
+
const matches = content.matchAll(/REQ-[A-Z]+-\d+/g);
|
|
263
|
+
|
|
264
|
+
for (const match of matches) {
|
|
265
|
+
references.add(match[0]);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return Array.from(references);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Extract code references from source directory
|
|
274
|
+
*/
|
|
275
|
+
async extractCodeReferences() {
|
|
276
|
+
if (!await fs.pathExists(this.srcDir)) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const files = glob.sync(`${this.srcDir}/**/*.{js,ts,jsx,tsx,py,java,go,rb}`);
|
|
281
|
+
const references = new Set();
|
|
282
|
+
|
|
283
|
+
for (const file of files) {
|
|
284
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
285
|
+
const matches = content.matchAll(/REQ-[A-Z]+-\d+/g);
|
|
286
|
+
|
|
287
|
+
for (const match of matches) {
|
|
288
|
+
references.add(match[0]);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return Array.from(references);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Extract test references from tests directory
|
|
297
|
+
*/
|
|
298
|
+
async extractTestReferences() {
|
|
299
|
+
if (!await fs.pathExists(this.testsDir)) {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const files = glob.sync(`${this.testsDir}/**/*.{test,spec}.{js,ts,jsx,tsx,py,java,go,rb}`);
|
|
304
|
+
const references = new Set();
|
|
305
|
+
|
|
306
|
+
for (const file of files) {
|
|
307
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
308
|
+
const matches = content.matchAll(/REQ-[A-Z]+-\d+/g);
|
|
309
|
+
|
|
310
|
+
for (const match of matches) {
|
|
311
|
+
references.add(match[0]);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return Array.from(references);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get source files
|
|
320
|
+
*/
|
|
321
|
+
async getSourceFiles() {
|
|
322
|
+
if (!await fs.pathExists(this.srcDir)) {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return glob.sync(`${this.srcDir}/**/*.{js,ts,jsx,tsx}`, {
|
|
327
|
+
ignore: ['**/*.test.*', '**/*.spec.*', '**/node_modules/**']
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Get test files
|
|
333
|
+
*/
|
|
334
|
+
async getTestFiles() {
|
|
335
|
+
if (!await fs.pathExists(this.testsDir)) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return glob.sync(`${this.testsDir}/**/*.{test,spec}.{js,ts,jsx,tsx}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Display gaps in table format
|
|
344
|
+
*/
|
|
345
|
+
displayTable(gaps, verbose = false) {
|
|
346
|
+
console.log(chalk.bold('\n📊 Gap Detection Summary\n'));
|
|
347
|
+
console.log(`Total Gaps: ${chalk.yellow(gaps.summary.total)}`);
|
|
348
|
+
console.log(` Orphaned Requirements: ${chalk.yellow(gaps.summary.orphanedRequirements)}`);
|
|
349
|
+
console.log(` Unimplemented Requirements: ${chalk.yellow(gaps.summary.unimplementedRequirements)}`);
|
|
350
|
+
console.log(` Untested Code: ${chalk.yellow(gaps.summary.untestedCode)}`);
|
|
351
|
+
console.log(` Missing Tests: ${chalk.yellow(gaps.summary.missingTests)}`);
|
|
352
|
+
|
|
353
|
+
if (verbose && gaps.orphanedRequirements.length > 0) {
|
|
354
|
+
console.log(chalk.bold('\n🔴 Orphaned Requirements:\n'));
|
|
355
|
+
gaps.orphanedRequirements.forEach(req => {
|
|
356
|
+
console.log(` ${chalk.red(req.id)}: ${req.title}`);
|
|
357
|
+
console.log(` File: ${req.file}`);
|
|
358
|
+
console.log(` Reason: ${req.reason}\n`);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (verbose && gaps.untestedCode.length > 0) {
|
|
363
|
+
console.log(chalk.bold('\n🔴 Untested Code:\n'));
|
|
364
|
+
gaps.untestedCode.forEach(item => {
|
|
365
|
+
console.log(` ${chalk.red(item.file)}`);
|
|
366
|
+
console.log(` Reason: ${item.reason}\n`);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Display requirements table
|
|
373
|
+
*/
|
|
374
|
+
displayRequirementsTable(orphaned) {
|
|
375
|
+
if (orphaned.length === 0) {
|
|
376
|
+
console.log(chalk.green('No orphaned requirements found.'));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
console.log(chalk.bold('\n🔴 Orphaned Requirements:\n'));
|
|
381
|
+
orphaned.forEach(req => {
|
|
382
|
+
console.log(` ${chalk.red(req.id)}: ${req.title}`);
|
|
383
|
+
console.log(` File: ${req.file}`);
|
|
384
|
+
console.log(` Reason: ${req.reason}\n`);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Display code table
|
|
390
|
+
*/
|
|
391
|
+
displayCodeTable(untested) {
|
|
392
|
+
if (untested.length === 0) {
|
|
393
|
+
console.log(chalk.green('All code is tested.'));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
console.log(chalk.bold('\n🔴 Untested Code:\n'));
|
|
398
|
+
untested.forEach(item => {
|
|
399
|
+
console.log(` ${chalk.red(item.file)}`);
|
|
400
|
+
console.log(` Reason: ${item.reason}\n`);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Display coverage table
|
|
406
|
+
*/
|
|
407
|
+
displayCoverageTable(coverage) {
|
|
408
|
+
console.log(chalk.bold('\n📊 Coverage Statistics\n'));
|
|
409
|
+
|
|
410
|
+
console.log(chalk.bold('Requirements Coverage:'));
|
|
411
|
+
console.log(` Total Requirements: ${coverage.requirements.total}`);
|
|
412
|
+
console.log(` With Design: ${coverage.requirements.withDesign} (${coverage.requirements.designCoverage.toFixed(1)}%)`);
|
|
413
|
+
console.log(` With Tasks: ${coverage.requirements.withTasks} (${coverage.requirements.taskCoverage.toFixed(1)}%)`);
|
|
414
|
+
console.log(` With Code: ${coverage.requirements.withCode} (${coverage.requirements.implementationCoverage.toFixed(1)}%)`);
|
|
415
|
+
console.log(` With Tests: ${coverage.requirements.withTests} (${coverage.requirements.testCoverage.toFixed(1)}%)`);
|
|
416
|
+
|
|
417
|
+
console.log(chalk.bold('\nCode Coverage:'));
|
|
418
|
+
console.log(` Total Source Files: ${coverage.code.total}`);
|
|
419
|
+
console.log(` Tested: ${coverage.code.tested} (${coverage.code.testCoverage.toFixed(1)}%)`);
|
|
420
|
+
console.log(` Untested: ${coverage.code.untested}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Format gaps as markdown
|
|
425
|
+
*/
|
|
426
|
+
formatMarkdown(gaps) {
|
|
427
|
+
let md = '# Gap Detection Report\n\n';
|
|
428
|
+
md += '## Summary\n\n';
|
|
429
|
+
md += `- **Total Gaps**: ${gaps.summary.total}\n`;
|
|
430
|
+
md += `- **Orphaned Requirements**: ${gaps.summary.orphanedRequirements}\n`;
|
|
431
|
+
md += `- **Unimplemented Requirements**: ${gaps.summary.unimplementedRequirements}\n`;
|
|
432
|
+
md += `- **Untested Code**: ${gaps.summary.untestedCode}\n`;
|
|
433
|
+
md += `- **Missing Tests**: ${gaps.summary.missingTests}\n\n`;
|
|
434
|
+
|
|
435
|
+
if (gaps.orphanedRequirements.length > 0) {
|
|
436
|
+
md += '## Orphaned Requirements\n\n';
|
|
437
|
+
gaps.orphanedRequirements.forEach(req => {
|
|
438
|
+
md += `### ${req.id}: ${req.title}\n\n`;
|
|
439
|
+
md += `- **File**: ${req.file}\n`;
|
|
440
|
+
md += `- **Reason**: ${req.reason}\n\n`;
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (gaps.untestedCode.length > 0) {
|
|
445
|
+
md += '## Untested Code\n\n';
|
|
446
|
+
gaps.untestedCode.forEach(item => {
|
|
447
|
+
md += `### ${item.file}\n\n`;
|
|
448
|
+
md += `- **Reason**: ${item.reason}\n\n`;
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return md;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Format requirements as markdown
|
|
457
|
+
*/
|
|
458
|
+
formatRequirementsMarkdown(orphaned) {
|
|
459
|
+
let md = '# Orphaned Requirements Report\n\n';
|
|
460
|
+
md += `**Total**: ${orphaned.length}\n\n`;
|
|
461
|
+
|
|
462
|
+
orphaned.forEach(req => {
|
|
463
|
+
md += `## ${req.id}: ${req.title}\n\n`;
|
|
464
|
+
md += `- **File**: ${req.file}\n`;
|
|
465
|
+
md += `- **Reason**: ${req.reason}\n\n`;
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
return md;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Format code as markdown
|
|
473
|
+
*/
|
|
474
|
+
formatCodeMarkdown(untested) {
|
|
475
|
+
let md = '# Untested Code Report\n\n';
|
|
476
|
+
md += `**Total**: ${untested.length}\n\n`;
|
|
477
|
+
|
|
478
|
+
untested.forEach(item => {
|
|
479
|
+
md += `## ${item.file}\n\n`;
|
|
480
|
+
md += `- **Reason**: ${item.reason}\n\n`;
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
return md;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Format coverage as markdown
|
|
488
|
+
*/
|
|
489
|
+
formatCoverageMarkdown(coverage) {
|
|
490
|
+
let md = '# Coverage Report\n\n';
|
|
491
|
+
|
|
492
|
+
md += '## Requirements Coverage\n\n';
|
|
493
|
+
md += `- **Total Requirements**: ${coverage.requirements.total}\n`;
|
|
494
|
+
md += `- **With Design**: ${coverage.requirements.withDesign} (${coverage.requirements.designCoverage.toFixed(1)}%)\n`;
|
|
495
|
+
md += `- **With Tasks**: ${coverage.requirements.withTasks} (${coverage.requirements.taskCoverage.toFixed(1)}%)\n`;
|
|
496
|
+
md += `- **With Code**: ${coverage.requirements.withCode} (${coverage.requirements.implementationCoverage.toFixed(1)}%)\n`;
|
|
497
|
+
md += `- **With Tests**: ${coverage.requirements.withTests} (${coverage.requirements.testCoverage.toFixed(1)}%)\n\n`;
|
|
498
|
+
|
|
499
|
+
md += '## Code Coverage\n\n';
|
|
500
|
+
md += `- **Total Source Files**: ${coverage.code.total}\n`;
|
|
501
|
+
md += `- **Tested**: ${coverage.code.tested} (${coverage.code.testCoverage.toFixed(1)}%)\n`;
|
|
502
|
+
md += `- **Untested**: ${coverage.code.untested}\n`;
|
|
503
|
+
|
|
504
|
+
return md;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
module.exports = GapDetector;
|