musubi-sdd 0.8.6 → 0.8.8

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
@@ -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
  ### プロジェクトタイプ
@@ -198,6 +208,16 @@ musubi-change list --format json # JSON形式でリスト
198
208
  - 成熟度が異なるマルチコンポーネントシステム
199
209
  - **有効化される機能**:
200
210
  - すべてのGreenfield + Brownfield機能
211
+
212
+ ## ドキュメント
213
+
214
+ 包括的なガイドは `docs/guides/` で利用可能です:
215
+
216
+ - **[ブラウンフィールドチュートリアル](docs/guides/brownfield-tutorial.md)** - 既存プロジェクトでの変更管理ステップバイステップガイド
217
+ - **[差分仕様ガイド](docs/guides/delta-spec-guide.md)** - 変更追跡のフォーマットリファレンス
218
+ - **[変更管理ワークフロー](docs/guides/change-management-workflow.md)** - エンドツーエンドワークフロードキュメント
219
+ - **[トレーサビリティマトリクスガイド](docs/guides/traceability-matrix-guide.md)** - トレーサビリティシステム使用方法
220
+ - **[ビデオチュートリアル計画](docs/guides/video-tutorial-plan.md)** - ビデオコンテンツスクリプト
201
221
  - コンポーネントごとにワークフローを選択する柔軟性
202
222
  - 同一プロジェクト内で差分仕様とグリーンフィールド仕様を混在
203
223
  - **メリット**:
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,8 +152,27 @@ 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
 
166
+ ## Documentation
167
+
168
+ Comprehensive guides are available in `docs/guides/`:
169
+
170
+ - **[Brownfield Tutorial](docs/guides/brownfield-tutorial.md)** - Step-by-step guide for managing changes in existing projects
171
+ - **[Delta Specification Guide](docs/guides/delta-spec-guide.md)** - Format reference for change tracking
172
+ - **[Change Management Workflow](docs/guides/change-management-workflow.md)** - End-to-end workflow documentation
173
+ - **[Traceability Matrix Guide](docs/guides/traceability-matrix-guide.md)** - Traceability system usage
174
+ - **[Video Tutorial Plan](docs/guides/video-tutorial-plan.md)** - Video content script
175
+
156
176
  ### Project Types
157
177
 
158
178
  During initialization, MUSUBI asks you to select a **Project Type**. This determines the workflow and features available:
@@ -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.6",
3
+ "version": "0.8.8",
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;