musubi-sdd 6.1.2 → 6.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,294 @@
1
+ /**
2
+ * TraceabilityExtractor Implementation
3
+ *
4
+ * Extracts requirement ID patterns from code, tests, commits, and documents.
5
+ *
6
+ * Requirement: IMP-6.2-004-01
7
+ * Design: Section 5.1
8
+ */
9
+
10
+ const fs = require('fs').promises;
11
+ const path = require('path');
12
+ const { exec } = require('child_process');
13
+ const { promisify } = require('util');
14
+
15
+ const execAsync = promisify(exec);
16
+
17
+ /**
18
+ * Requirement ID patterns
19
+ */
20
+ const REQ_PATTERNS = [
21
+ /REQ-[A-Z0-9]+-\d{3}/g, // REQ-XXX-NNN
22
+ /IMP-\d+\.\d+-\d{3}(?:-\d{2})?/g, // IMP-6.2-001 or IMP-6.2-001-01
23
+ /FEAT-\d{3}/g, // FEAT-001
24
+ /TASK-\d{3}/g, // TASK-001
25
+ ];
26
+
27
+ /**
28
+ * Default configuration
29
+ */
30
+ const DEFAULT_CONFIG = {
31
+ sourceDir: 'src',
32
+ testDir: 'tests',
33
+ docDir: 'docs',
34
+ includePatterns: ['*.js', '*.ts', '*.tsx', '*.jsx', '*.md'],
35
+ excludePatterns: ['node_modules/**', '*.test.js', '*.spec.js'],
36
+ scanCommits: false,
37
+ maxCommits: 100
38
+ };
39
+
40
+ /**
41
+ * TraceabilityExtractor
42
+ *
43
+ * Extracts requirement references from various sources.
44
+ */
45
+ class TraceabilityExtractor {
46
+ /**
47
+ * @param {Object} config - Configuration options
48
+ */
49
+ constructor(config = {}) {
50
+ this.config = { ...DEFAULT_CONFIG, ...config };
51
+ }
52
+
53
+ /**
54
+ * Extract references from a single file
55
+ * @param {string} filePath - Path to file
56
+ * @param {string} sourceType - Type of source (code|test|document)
57
+ * @returns {Promise<Array>} Requirement references
58
+ */
59
+ async extractFromFile(filePath, sourceType) {
60
+ try {
61
+ await fs.access(filePath);
62
+ const content = await fs.readFile(filePath, 'utf-8');
63
+
64
+ return this.extractFromContent(content, filePath, sourceType);
65
+ } catch {
66
+ return [];
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Extract references from content string
72
+ * @param {string} content - File content
73
+ * @param {string} filePath - File path
74
+ * @param {string} sourceType - Source type
75
+ * @returns {Array} Requirement references
76
+ */
77
+ extractFromContent(content, filePath, sourceType) {
78
+ const refs = [];
79
+ const lines = content.split('\n');
80
+ const now = new Date().toISOString();
81
+
82
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
83
+ const line = lines[lineNum];
84
+
85
+ for (const pattern of REQ_PATTERNS) {
86
+ // Reset regex state
87
+ pattern.lastIndex = 0;
88
+
89
+ let match;
90
+ while ((match = pattern.exec(line)) !== null) {
91
+ refs.push({
92
+ requirementId: match[0],
93
+ sourceType,
94
+ filePath,
95
+ lineNumber: lineNum + 1,
96
+ context: line.trim().substring(0, 100),
97
+ foundAt: now
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ return refs;
104
+ }
105
+
106
+ /**
107
+ * Extract references from git commits
108
+ * @returns {Promise<Array>} Requirement references from commits
109
+ */
110
+ async extractFromCommits() {
111
+ const refs = [];
112
+ const now = new Date().toISOString();
113
+
114
+ try {
115
+ const maxCommits = this.config.maxCommits || 100;
116
+ const { stdout } = await execAsync(
117
+ `git log -${maxCommits} --format="%H|%s|%ad|%ae" --date=short`
118
+ );
119
+
120
+ const lines = stdout.trim().split('\n').filter(l => l.length > 0);
121
+
122
+ for (const line of lines) {
123
+ const [hash, message, date, author] = line.split('|');
124
+
125
+ for (const pattern of REQ_PATTERNS) {
126
+ pattern.lastIndex = 0;
127
+
128
+ let match;
129
+ while ((match = pattern.exec(message)) !== null) {
130
+ refs.push({
131
+ requirementId: match[0],
132
+ sourceType: 'commit',
133
+ commitHash: hash,
134
+ commitMessage: message,
135
+ context: message,
136
+ foundAt: now
137
+ });
138
+ }
139
+ }
140
+ }
141
+ } catch {
142
+ // Git not available or not in a git repo
143
+ }
144
+
145
+ return refs;
146
+ }
147
+
148
+ /**
149
+ * Scan a directory for references
150
+ * @param {string} dirPath - Directory path
151
+ * @param {string} sourceType - Source type
152
+ * @returns {Promise<Array>} Requirement references
153
+ */
154
+ async scanDirectory(dirPath, sourceType) {
155
+ const refs = [];
156
+
157
+ try {
158
+ const entries = await fs.readdir(dirPath);
159
+
160
+ for (const entry of entries) {
161
+ const fullPath = path.join(dirPath, entry);
162
+ const stat = await fs.stat(fullPath);
163
+
164
+ if (stat.isDirectory()) {
165
+ // Skip excluded directories
166
+ if (this.isExcluded(entry)) continue;
167
+
168
+ const subRefs = await this.scanDirectory(fullPath, sourceType);
169
+ refs.push(...subRefs);
170
+ } else if (stat.isFile() && this.shouldInclude(entry)) {
171
+ const fileRefs = await this.extractFromFile(fullPath, sourceType);
172
+ refs.push(...fileRefs);
173
+ }
174
+ }
175
+ } catch {
176
+ // Directory not accessible
177
+ }
178
+
179
+ return refs;
180
+ }
181
+
182
+ /**
183
+ * Extract from all configured sources
184
+ * @returns {Promise<Array>} All requirement references
185
+ */
186
+ async extractAll() {
187
+ const refs = [];
188
+
189
+ // Extract from source code
190
+ if (this.config.sourceDir) {
191
+ const codeRefs = await this.scanDirectory(this.config.sourceDir, 'code');
192
+ refs.push(...codeRefs);
193
+ }
194
+
195
+ // Extract from tests
196
+ if (this.config.testDir) {
197
+ const testRefs = await this.scanDirectory(this.config.testDir, 'test');
198
+ refs.push(...testRefs);
199
+ }
200
+
201
+ // Extract from documents
202
+ if (this.config.docDir) {
203
+ const docRefs = await this.scanDirectory(this.config.docDir, 'document');
204
+ refs.push(...docRefs);
205
+ }
206
+
207
+ // Extract from commits
208
+ if (this.config.scanCommits) {
209
+ const commitRefs = await this.extractFromCommits();
210
+ refs.push(...commitRefs);
211
+ }
212
+
213
+ return refs;
214
+ }
215
+
216
+ /**
217
+ * Group references by requirement ID
218
+ * @param {Array} refs - References to group
219
+ * @returns {Map<string, Array>} Grouped references
220
+ */
221
+ groupByRequirement(refs) {
222
+ const grouped = new Map();
223
+
224
+ for (const ref of refs) {
225
+ const existing = grouped.get(ref.requirementId) || [];
226
+ existing.push(ref);
227
+ grouped.set(ref.requirementId, existing);
228
+ }
229
+
230
+ return grouped;
231
+ }
232
+
233
+ /**
234
+ * Check if file should be included
235
+ * @param {string} filename - Filename to check
236
+ * @returns {boolean} Should include
237
+ */
238
+ shouldInclude(filename) {
239
+ const includePatterns = this.config.includePatterns || [];
240
+ const excludePatterns = this.config.excludePatterns || [];
241
+
242
+ // Check exclude patterns first
243
+ for (const pattern of excludePatterns) {
244
+ if (this.matchPattern(filename, pattern)) {
245
+ return false;
246
+ }
247
+ }
248
+
249
+ // Check include patterns
250
+ for (const pattern of includePatterns) {
251
+ if (this.matchPattern(filename, pattern)) {
252
+ return true;
253
+ }
254
+ }
255
+
256
+ return includePatterns.length === 0;
257
+ }
258
+
259
+ /**
260
+ * Check if directory/file should be excluded
261
+ * @param {string} name - Name to check
262
+ * @returns {boolean} Should exclude
263
+ */
264
+ isExcluded(name) {
265
+ const excludePatterns = this.config.excludePatterns || [];
266
+
267
+ for (const pattern of excludePatterns) {
268
+ if (pattern.includes('**')) {
269
+ // Glob pattern with directory
270
+ const dirPattern = pattern.split('**')[0].replace(/\/$/, '');
271
+ if (name === dirPattern) return true;
272
+ }
273
+ }
274
+
275
+ return name === 'node_modules' || name.startsWith('.');
276
+ }
277
+
278
+ /**
279
+ * Simple glob pattern matching
280
+ * @param {string} filename - Filename to match
281
+ * @param {string} pattern - Pattern to match against
282
+ * @returns {boolean} Matches
283
+ */
284
+ matchPattern(filename, pattern) {
285
+ // Convert glob pattern to regex
286
+ const regexPattern = pattern
287
+ .replace(/\./g, '\\.')
288
+ .replace(/\*/g, '.*');
289
+
290
+ return new RegExp(`^${regexPattern}$`).test(filename);
291
+ }
292
+ }
293
+
294
+ module.exports = { TraceabilityExtractor };
@@ -0,0 +1,230 @@
1
+ /**
2
+ * GapDetector Implementation
3
+ *
4
+ * Detects requirements without implementation or tests.
5
+ *
6
+ * Requirement: IMP-6.2-004-02
7
+ * Design: Section 5.2
8
+ */
9
+
10
+ /**
11
+ * Severity levels for gaps
12
+ */
13
+ const SEVERITY_MAP = {
14
+ 'no-test': 'critical',
15
+ 'no-code': 'high',
16
+ 'no-design': 'medium',
17
+ 'no-commit': 'low'
18
+ };
19
+
20
+ /**
21
+ * Severity ordering for sorting
22
+ */
23
+ const SEVERITY_ORDER = {
24
+ 'critical': 0,
25
+ 'high': 1,
26
+ 'medium': 2,
27
+ 'low': 3
28
+ };
29
+
30
+ /**
31
+ * Suggestions for each gap type
32
+ */
33
+ const SUGGESTIONS = {
34
+ 'no-test': 'この要件に対するテストを作成してください。Article III (Test-First) に従い、実装前にテストを書くことを推奨します。',
35
+ 'no-code': '要件の実装が必要です。設計ドキュメントを参照して実装を開始してください。',
36
+ 'no-design': '設計ドキュメントを作成してください。C4モデルに従い、コンポーネント図とADRを追加することを推奨します。',
37
+ 'no-commit': 'この要件に関連するコミットがありません。コミットメッセージに要件IDを含めてください。'
38
+ };
39
+
40
+ /**
41
+ * GapDetector
42
+ *
43
+ * Detects gaps in traceability between requirements and artifacts.
44
+ */
45
+ class GapDetector {
46
+ /**
47
+ * Detect all gaps in traceability links
48
+ * @param {Array} links - Traceability links
49
+ * @returns {Array} Detected gaps
50
+ */
51
+ detectGaps(links) {
52
+ const gaps = [];
53
+
54
+ for (const link of links) {
55
+ // Check for design gap
56
+ if (link.design.length === 0) {
57
+ gaps.push(this.createGap(link.requirementId, 'no-design'));
58
+ }
59
+
60
+ // Check for code gap
61
+ if (link.code.length === 0) {
62
+ gaps.push(this.createGap(link.requirementId, 'no-code'));
63
+ }
64
+
65
+ // Check for test gap
66
+ if (link.tests.length === 0) {
67
+ gaps.push(this.createGap(link.requirementId, 'no-test'));
68
+ }
69
+ }
70
+
71
+ return gaps;
72
+ }
73
+
74
+ /**
75
+ * Create a gap entry
76
+ * @param {string} requirementId - Requirement ID
77
+ * @param {string} gapType - Gap type
78
+ * @returns {Object} Gap entry
79
+ */
80
+ createGap(requirementId, gapType) {
81
+ return {
82
+ requirementId,
83
+ gapType,
84
+ severity: SEVERITY_MAP[gapType],
85
+ suggestion: SUGGESTIONS[gapType]
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Analyze a traceability matrix for gaps
91
+ * @param {Object} matrix - Traceability matrix
92
+ * @returns {Object} Gap analysis
93
+ */
94
+ analyzeMatrix(matrix) {
95
+ const links = Object.values(matrix.requirements);
96
+ const gaps = this.detectGaps(links);
97
+
98
+ return this.calculateAnalysis(gaps);
99
+ }
100
+
101
+ /**
102
+ * Calculate gap analysis
103
+ * @param {Array} gaps - Detected gaps
104
+ * @returns {Object} Gap analysis
105
+ */
106
+ calculateAnalysis(gaps) {
107
+ const gapsByType = {};
108
+ const gapsByRequirement = {};
109
+
110
+ let criticalGaps = 0;
111
+ let highGaps = 0;
112
+ let mediumGaps = 0;
113
+ let lowGaps = 0;
114
+
115
+ for (const gap of gaps) {
116
+ // Count by type
117
+ gapsByType[gap.gapType] = (gapsByType[gap.gapType] || 0) + 1;
118
+
119
+ // Count by requirement
120
+ gapsByRequirement[gap.requirementId] =
121
+ (gapsByRequirement[gap.requirementId] || 0) + 1;
122
+
123
+ // Count by severity
124
+ switch (gap.severity) {
125
+ case 'critical':
126
+ criticalGaps++;
127
+ break;
128
+ case 'high':
129
+ highGaps++;
130
+ break;
131
+ case 'medium':
132
+ mediumGaps++;
133
+ break;
134
+ case 'low':
135
+ lowGaps++;
136
+ break;
137
+ }
138
+ }
139
+
140
+ return {
141
+ totalGaps: gaps.length,
142
+ criticalGaps,
143
+ highGaps,
144
+ mediumGaps,
145
+ lowGaps,
146
+ gapsByType,
147
+ gapsByRequirement
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Generate a gap report
153
+ * @param {Array} links - Traceability links
154
+ * @returns {Object} Gap report
155
+ */
156
+ getGapReport(links) {
157
+ const gaps = this.detectGaps(links);
158
+ const sortedGaps = this.sortGapsBySeverity(gaps);
159
+ const summary = this.calculateAnalysis(gaps);
160
+
161
+ return {
162
+ generatedAt: new Date().toISOString(),
163
+ gaps: sortedGaps,
164
+ summary
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Sort gaps by severity (critical first)
170
+ * @param {Array} gaps - Gaps to sort
171
+ * @returns {Array} Sorted gaps
172
+ */
173
+ sortGapsBySeverity(gaps) {
174
+ return [...gaps].sort((a, b) => {
175
+ return SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Filter gaps by severity
181
+ * @param {Array} gaps - Gaps to filter
182
+ * @param {string} severity - Severity to filter by
183
+ * @returns {Array} Filtered gaps
184
+ */
185
+ filterGapsBySeverity(gaps, severity) {
186
+ return gaps.filter(g => g.severity === severity);
187
+ }
188
+
189
+ /**
190
+ * Get unique requirement IDs that have gaps
191
+ * @param {Array} links - Traceability links
192
+ * @returns {Array} Requirement IDs with gaps
193
+ */
194
+ getRequirementsWithGaps(links) {
195
+ const gaps = this.detectGaps(links);
196
+ const reqIds = new Set();
197
+
198
+ for (const gap of gaps) {
199
+ reqIds.add(gap.requirementId);
200
+ }
201
+
202
+ return Array.from(reqIds);
203
+ }
204
+
205
+ /**
206
+ * Check if a specific requirement has critical gaps
207
+ * @param {Object} link - Traceability link
208
+ * @returns {boolean} Has critical gap
209
+ */
210
+ hasCriticalGap(link) {
211
+ // No tests is critical
212
+ return link.tests.length === 0;
213
+ }
214
+
215
+ /**
216
+ * Get gap coverage percentage
217
+ * @param {Array} links - Traceability links
218
+ * @returns {number} Coverage percentage
219
+ */
220
+ getGapCoverage(links) {
221
+ if (links.length === 0) return 100;
222
+
223
+ const reqsWithGaps = this.getRequirementsWithGaps(links);
224
+ const covered = links.length - reqsWithGaps.length;
225
+
226
+ return Math.round((covered / links.length) * 100);
227
+ }
228
+ }
229
+
230
+ module.exports = { GapDetector };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Traceability Module
3
+ *
4
+ * Requirement: IMP-6.2-004
5
+ */
6
+
7
+ const { TraceabilityExtractor } = require('./extractor');
8
+ const { GapDetector } = require('./gap-detector');
9
+ const { MatrixStorage } = require('./matrix-storage');
10
+
11
+ module.exports = {
12
+ TraceabilityExtractor,
13
+ GapDetector,
14
+ MatrixStorage
15
+ };