@nahisaho/musubix-core 1.0.0 → 1.0.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.
- package/dist/cli/commands/codegen.d.ts +125 -0
- package/dist/cli/commands/codegen.d.ts.map +1 -0
- package/dist/cli/commands/codegen.js +684 -0
- package/dist/cli/commands/codegen.js.map +1 -0
- package/dist/cli/commands/design.d.ts +158 -0
- package/dist/cli/commands/design.d.ts.map +1 -0
- package/dist/cli/commands/design.js +562 -0
- package/dist/cli/commands/design.js.map +1 -0
- package/dist/cli/commands/explain.d.ts +116 -0
- package/dist/cli/commands/explain.d.ts.map +1 -0
- package/dist/cli/commands/explain.js +419 -0
- package/dist/cli/commands/explain.js.map +1 -0
- package/dist/cli/commands/index.d.ts +13 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +31 -7
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/requirements.d.ts +103 -0
- package/dist/cli/commands/requirements.d.ts.map +1 -0
- package/dist/cli/commands/requirements.js +403 -0
- package/dist/cli/commands/requirements.js.map +1 -0
- package/dist/cli/commands/skills.d.ts +99 -0
- package/dist/cli/commands/skills.d.ts.map +1 -0
- package/dist/cli/commands/skills.js +363 -0
- package/dist/cli/commands/skills.js.map +1 -0
- package/dist/cli/commands/test.d.ts +113 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/test.js +532 -0
- package/dist/cli/commands/test.js.map +1 -0
- package/dist/cli/commands/trace.d.ts +132 -0
- package/dist/cli/commands/trace.d.ts.map +1 -0
- package/dist/cli/commands/trace.js +553 -0
- package/dist/cli/commands/trace.js.map +1 -0
- package/package.json +5 -3
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace Command
|
|
3
|
+
*
|
|
4
|
+
* CLI commands for traceability management
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
* @module cli/commands/trace
|
|
8
|
+
*
|
|
9
|
+
* @see REQ-CLI-005 - Trace CLI
|
|
10
|
+
* @see REQ-TM-001 - Traceability Matrix
|
|
11
|
+
* @see REQ-TM-002 - Impact Analysis
|
|
12
|
+
* @see DES-MUSUBIX-001 Section 16-C.5 - traceコマンド設計
|
|
13
|
+
* @see TSK-076〜078 - Trace CLI実装
|
|
14
|
+
*/
|
|
15
|
+
import type { Command } from 'commander';
|
|
16
|
+
/**
|
|
17
|
+
* Trace command options
|
|
18
|
+
*/
|
|
19
|
+
export interface TraceOptions {
|
|
20
|
+
output?: string;
|
|
21
|
+
format?: 'markdown' | 'json' | 'csv' | 'html';
|
|
22
|
+
verbose?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Traceability link
|
|
26
|
+
*/
|
|
27
|
+
export interface TraceabilityLink {
|
|
28
|
+
source: string;
|
|
29
|
+
target: string;
|
|
30
|
+
type: 'implements' | 'derives' | 'tests' | 'refines' | 'relates';
|
|
31
|
+
status: 'valid' | 'broken' | 'pending';
|
|
32
|
+
file?: string;
|
|
33
|
+
line?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Traceability matrix entry
|
|
37
|
+
*/
|
|
38
|
+
export interface MatrixEntry {
|
|
39
|
+
id: string;
|
|
40
|
+
type: 'requirement' | 'design' | 'task' | 'code' | 'test';
|
|
41
|
+
description: string;
|
|
42
|
+
links: TraceabilityLink[];
|
|
43
|
+
coverage: {
|
|
44
|
+
design: boolean;
|
|
45
|
+
implementation: boolean;
|
|
46
|
+
test: boolean;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Matrix result
|
|
51
|
+
*/
|
|
52
|
+
export interface MatrixResult {
|
|
53
|
+
success: boolean;
|
|
54
|
+
entries: MatrixEntry[];
|
|
55
|
+
statistics: {
|
|
56
|
+
requirements: number;
|
|
57
|
+
designs: number;
|
|
58
|
+
tasks: number;
|
|
59
|
+
implementations: number;
|
|
60
|
+
tests: number;
|
|
61
|
+
coverage: number;
|
|
62
|
+
};
|
|
63
|
+
message: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Impact item
|
|
67
|
+
*/
|
|
68
|
+
export interface ImpactItem {
|
|
69
|
+
id: string;
|
|
70
|
+
type: string;
|
|
71
|
+
impactLevel: 'direct' | 'indirect' | 'potential';
|
|
72
|
+
description: string;
|
|
73
|
+
affectedFiles?: string[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Impact result
|
|
77
|
+
*/
|
|
78
|
+
export interface ImpactResult {
|
|
79
|
+
success: boolean;
|
|
80
|
+
sourceId: string;
|
|
81
|
+
impacts: ImpactItem[];
|
|
82
|
+
riskLevel: 'low' | 'medium' | 'high' | 'critical';
|
|
83
|
+
recommendations: string[];
|
|
84
|
+
message: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Validation issue
|
|
88
|
+
*/
|
|
89
|
+
export interface TraceValidationIssue {
|
|
90
|
+
type: 'missing_link' | 'broken_link' | 'orphan' | 'circular';
|
|
91
|
+
severity: 'error' | 'warning';
|
|
92
|
+
source?: string;
|
|
93
|
+
target?: string;
|
|
94
|
+
message: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Validation result
|
|
98
|
+
*/
|
|
99
|
+
export interface TraceValidationResult {
|
|
100
|
+
success: boolean;
|
|
101
|
+
valid: boolean;
|
|
102
|
+
issues: TraceValidationIssue[];
|
|
103
|
+
summary: {
|
|
104
|
+
total: number;
|
|
105
|
+
valid: number;
|
|
106
|
+
broken: number;
|
|
107
|
+
orphans: number;
|
|
108
|
+
};
|
|
109
|
+
message: string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Register trace command
|
|
113
|
+
*/
|
|
114
|
+
export declare function registerTraceCommand(program: Command): void;
|
|
115
|
+
/**
|
|
116
|
+
* Collect artifacts from directories
|
|
117
|
+
*/
|
|
118
|
+
declare function collectArtifacts(specsDir: string, srcDir: string): Promise<MatrixEntry[]>;
|
|
119
|
+
/**
|
|
120
|
+
* Build traceability links between artifacts
|
|
121
|
+
*/
|
|
122
|
+
declare function buildTraceabilityLinks(entries: MatrixEntry[], _specsDir: string, _srcDir: string): Promise<void>;
|
|
123
|
+
/**
|
|
124
|
+
* Analyze impact
|
|
125
|
+
*/
|
|
126
|
+
declare function analyzeImpact(source: MatrixEntry, entries: MatrixEntry[], depth: number): ImpactItem[];
|
|
127
|
+
/**
|
|
128
|
+
* Validate traceability
|
|
129
|
+
*/
|
|
130
|
+
declare function validateTraceability(entries: MatrixEntry[], strict: boolean): TraceValidationIssue[];
|
|
131
|
+
export { collectArtifacts, buildTraceabilityLinks, analyzeImpact, validateTraceability, };
|
|
132
|
+
//# sourceMappingURL=trace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/trace.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace Command
|
|
3
|
+
*
|
|
4
|
+
* CLI commands for traceability management
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
* @module cli/commands/trace
|
|
8
|
+
*
|
|
9
|
+
* @see REQ-CLI-005 - Trace CLI
|
|
10
|
+
* @see REQ-TM-001 - Traceability Matrix
|
|
11
|
+
* @see REQ-TM-002 - Impact Analysis
|
|
12
|
+
* @see DES-MUSUBIX-001 Section 16-C.5 - traceコマンド設計
|
|
13
|
+
* @see TSK-076〜078 - Trace CLI実装
|
|
14
|
+
*/
|
|
15
|
+
import { readFile, writeFile, readdir, mkdir } from 'fs/promises';
|
|
16
|
+
import { resolve, join, extname } from 'path';
|
|
17
|
+
import { ExitCode, getGlobalOptions, outputResult } from '../base.js';
|
|
18
|
+
/**
|
|
19
|
+
* ID patterns for different artifact types
|
|
20
|
+
*/
|
|
21
|
+
const ID_PATTERNS = {
|
|
22
|
+
requirement: /REQ-[A-Z]+-\d+/g,
|
|
23
|
+
design: /DES-[A-Z]+-\d+/g,
|
|
24
|
+
task: /TSK-[A-Z]*-?\d+/g,
|
|
25
|
+
test: /TEST-[A-Z]+-\d+/g,
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Register trace command
|
|
29
|
+
*/
|
|
30
|
+
export function registerTraceCommand(program) {
|
|
31
|
+
const trace = program
|
|
32
|
+
.command('trace')
|
|
33
|
+
.description('Traceability management');
|
|
34
|
+
// trace matrix
|
|
35
|
+
trace
|
|
36
|
+
.command('matrix')
|
|
37
|
+
.description('Generate traceability matrix')
|
|
38
|
+
.option('-o, --output <file>', 'Output file')
|
|
39
|
+
.option('-f, --format <format>', 'Output format (markdown|json|csv|html)', 'markdown')
|
|
40
|
+
.option('--specs <dir>', 'Specs directory', 'storage/specs')
|
|
41
|
+
.option('--src <dir>', 'Source directory', 'packages')
|
|
42
|
+
.action(async (options) => {
|
|
43
|
+
const globalOpts = getGlobalOptions(program);
|
|
44
|
+
try {
|
|
45
|
+
const specsDir = resolve(process.cwd(), options.specs ?? 'storage/specs');
|
|
46
|
+
const srcDir = resolve(process.cwd(), options.src ?? 'packages');
|
|
47
|
+
// Collect all artifacts
|
|
48
|
+
const entries = await collectArtifacts(specsDir, srcDir);
|
|
49
|
+
// Build traceability links
|
|
50
|
+
await buildTraceabilityLinks(entries, specsDir, srcDir);
|
|
51
|
+
// Calculate statistics
|
|
52
|
+
const statistics = calculateStatistics(entries);
|
|
53
|
+
const result = {
|
|
54
|
+
success: true,
|
|
55
|
+
entries,
|
|
56
|
+
statistics,
|
|
57
|
+
message: `Generated traceability matrix with ${entries.length} entries (${statistics.coverage.toFixed(1)}% coverage)`,
|
|
58
|
+
};
|
|
59
|
+
// Output
|
|
60
|
+
if (options.output) {
|
|
61
|
+
const outputPath = resolve(process.cwd(), options.output);
|
|
62
|
+
await mkdir(resolve(outputPath, '..'), { recursive: true });
|
|
63
|
+
let content;
|
|
64
|
+
if (options.format === 'json' || globalOpts.json) {
|
|
65
|
+
content = JSON.stringify(result, null, 2);
|
|
66
|
+
}
|
|
67
|
+
else if (options.format === 'csv') {
|
|
68
|
+
content = generateCSVMatrix(entries);
|
|
69
|
+
}
|
|
70
|
+
else if (options.format === 'html') {
|
|
71
|
+
content = generateHTMLMatrix(entries, statistics);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
content = generateMarkdownMatrix(entries, statistics);
|
|
75
|
+
}
|
|
76
|
+
await writeFile(outputPath, content, 'utf-8');
|
|
77
|
+
if (!globalOpts.quiet) {
|
|
78
|
+
console.log(`✅ Traceability matrix saved to ${outputPath}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
outputResult(result, globalOpts);
|
|
83
|
+
}
|
|
84
|
+
process.exit(ExitCode.SUCCESS);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (!globalOpts.quiet) {
|
|
88
|
+
console.error(`❌ Matrix generation failed: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// trace impact
|
|
94
|
+
trace
|
|
95
|
+
.command('impact <id>')
|
|
96
|
+
.description('Analyze change impact')
|
|
97
|
+
.option('--depth <number>', 'Analysis depth', '3')
|
|
98
|
+
.action(async (id, options) => {
|
|
99
|
+
const globalOpts = getGlobalOptions(program);
|
|
100
|
+
try {
|
|
101
|
+
const depth = parseInt(options.depth ?? '3', 10);
|
|
102
|
+
const specsDir = resolve(process.cwd(), 'storage/specs');
|
|
103
|
+
const srcDir = resolve(process.cwd(), 'packages');
|
|
104
|
+
// Find artifact by ID
|
|
105
|
+
const entries = await collectArtifacts(specsDir, srcDir);
|
|
106
|
+
await buildTraceabilityLinks(entries, specsDir, srcDir);
|
|
107
|
+
// Find source entry
|
|
108
|
+
const sourceEntry = entries.find(e => e.id === id);
|
|
109
|
+
if (!sourceEntry) {
|
|
110
|
+
throw new Error(`Artifact ${id} not found`);
|
|
111
|
+
}
|
|
112
|
+
// Analyze impact
|
|
113
|
+
const impacts = analyzeImpact(sourceEntry, entries, depth);
|
|
114
|
+
const riskLevel = calculateRiskLevel(impacts);
|
|
115
|
+
const recommendations = generateRecommendations(sourceEntry, impacts);
|
|
116
|
+
const result = {
|
|
117
|
+
success: true,
|
|
118
|
+
sourceId: id,
|
|
119
|
+
impacts,
|
|
120
|
+
riskLevel,
|
|
121
|
+
recommendations,
|
|
122
|
+
message: `Found ${impacts.length} impacted items (Risk: ${riskLevel})`,
|
|
123
|
+
};
|
|
124
|
+
outputResult(result, globalOpts);
|
|
125
|
+
process.exit(ExitCode.SUCCESS);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (!globalOpts.quiet) {
|
|
129
|
+
console.error(`❌ Impact analysis failed: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// trace validate
|
|
135
|
+
trace
|
|
136
|
+
.command('validate')
|
|
137
|
+
.description('Validate traceability links')
|
|
138
|
+
.option('--strict', 'Enable strict mode', false)
|
|
139
|
+
.action(async (options) => {
|
|
140
|
+
const globalOpts = getGlobalOptions(program);
|
|
141
|
+
try {
|
|
142
|
+
const specsDir = resolve(process.cwd(), 'storage/specs');
|
|
143
|
+
const srcDir = resolve(process.cwd(), 'packages');
|
|
144
|
+
// Collect and link artifacts
|
|
145
|
+
const entries = await collectArtifacts(specsDir, srcDir);
|
|
146
|
+
await buildTraceabilityLinks(entries, specsDir, srcDir);
|
|
147
|
+
// Validate links
|
|
148
|
+
const issues = validateTraceability(entries, options.strict ?? false);
|
|
149
|
+
const total = entries.reduce((sum, e) => sum + e.links.length, 0);
|
|
150
|
+
const broken = issues.filter(i => i.type === 'broken_link').length;
|
|
151
|
+
const orphans = issues.filter(i => i.type === 'orphan').length;
|
|
152
|
+
const result = {
|
|
153
|
+
success: true,
|
|
154
|
+
valid: issues.filter(i => i.severity === 'error').length === 0,
|
|
155
|
+
issues,
|
|
156
|
+
summary: {
|
|
157
|
+
total,
|
|
158
|
+
valid: total - broken,
|
|
159
|
+
broken,
|
|
160
|
+
orphans,
|
|
161
|
+
},
|
|
162
|
+
message: issues.length === 0
|
|
163
|
+
? '✅ All traceability links are valid'
|
|
164
|
+
: `⚠️ Found ${issues.length} issues (${broken} broken links, ${orphans} orphans)`,
|
|
165
|
+
};
|
|
166
|
+
outputResult(result, globalOpts);
|
|
167
|
+
process.exit(result.valid ? ExitCode.SUCCESS : ExitCode.VALIDATION_ERROR);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (!globalOpts.quiet) {
|
|
171
|
+
console.error(`❌ Validation failed: ${error.message}`);
|
|
172
|
+
}
|
|
173
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Collect artifacts from directories
|
|
179
|
+
*/
|
|
180
|
+
async function collectArtifacts(specsDir, srcDir) {
|
|
181
|
+
const entries = [];
|
|
182
|
+
const seenIds = new Set();
|
|
183
|
+
// Collect from specs directory
|
|
184
|
+
try {
|
|
185
|
+
const specFiles = await readdir(specsDir);
|
|
186
|
+
for (const file of specFiles) {
|
|
187
|
+
if (!file.endsWith('.md'))
|
|
188
|
+
continue;
|
|
189
|
+
const content = await readFile(join(specsDir, file), 'utf-8');
|
|
190
|
+
// Extract requirement IDs
|
|
191
|
+
const reqMatches = content.match(ID_PATTERNS.requirement) || [];
|
|
192
|
+
for (const id of reqMatches) {
|
|
193
|
+
if (seenIds.has(id))
|
|
194
|
+
continue;
|
|
195
|
+
seenIds.add(id);
|
|
196
|
+
// Extract description
|
|
197
|
+
const descRegex = new RegExp(`${id}[^\\n]*\\n([^\\n]+)`, 'i');
|
|
198
|
+
const descMatch = content.match(descRegex);
|
|
199
|
+
entries.push({
|
|
200
|
+
id,
|
|
201
|
+
type: 'requirement',
|
|
202
|
+
description: descMatch?.[1]?.trim() || 'No description',
|
|
203
|
+
links: [],
|
|
204
|
+
coverage: { design: false, implementation: false, test: false },
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// Extract design IDs
|
|
208
|
+
const desMatches = content.match(ID_PATTERNS.design) || [];
|
|
209
|
+
for (const id of desMatches) {
|
|
210
|
+
if (seenIds.has(id))
|
|
211
|
+
continue;
|
|
212
|
+
seenIds.add(id);
|
|
213
|
+
entries.push({
|
|
214
|
+
id,
|
|
215
|
+
type: 'design',
|
|
216
|
+
description: 'Design element',
|
|
217
|
+
links: [],
|
|
218
|
+
coverage: { design: true, implementation: false, test: false },
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
// Extract task IDs
|
|
222
|
+
const taskMatches = content.match(ID_PATTERNS.task) || [];
|
|
223
|
+
for (const id of taskMatches) {
|
|
224
|
+
if (seenIds.has(id))
|
|
225
|
+
continue;
|
|
226
|
+
seenIds.add(id);
|
|
227
|
+
entries.push({
|
|
228
|
+
id,
|
|
229
|
+
type: 'task',
|
|
230
|
+
description: 'Implementation task',
|
|
231
|
+
links: [],
|
|
232
|
+
coverage: { design: true, implementation: false, test: false },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Specs directory doesn't exist
|
|
239
|
+
}
|
|
240
|
+
// Collect from source directory
|
|
241
|
+
await scanSourceDir(srcDir, entries, seenIds);
|
|
242
|
+
return entries;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Scan source directory for code references
|
|
246
|
+
*/
|
|
247
|
+
async function scanSourceDir(dir, entries, seenIds) {
|
|
248
|
+
try {
|
|
249
|
+
const items = await readdir(dir, { withFileTypes: true });
|
|
250
|
+
for (const item of items) {
|
|
251
|
+
const fullPath = join(dir, item.name);
|
|
252
|
+
if (item.isDirectory()) {
|
|
253
|
+
if (!item.name.startsWith('.') && item.name !== 'node_modules' && item.name !== 'dist') {
|
|
254
|
+
await scanSourceDir(fullPath, entries, seenIds);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else if (item.isFile()) {
|
|
258
|
+
const ext = extname(item.name);
|
|
259
|
+
if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) {
|
|
260
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
261
|
+
// Find referenced IDs in code comments
|
|
262
|
+
for (const pattern of Object.values(ID_PATTERNS)) {
|
|
263
|
+
const matches = content.match(pattern) || [];
|
|
264
|
+
for (const id of matches) {
|
|
265
|
+
// Update existing entry or add code reference
|
|
266
|
+
const existing = entries.find(e => e.id === id);
|
|
267
|
+
if (existing) {
|
|
268
|
+
existing.coverage.implementation = true;
|
|
269
|
+
existing.links.push({
|
|
270
|
+
source: id,
|
|
271
|
+
target: fullPath,
|
|
272
|
+
type: 'implements',
|
|
273
|
+
status: 'valid',
|
|
274
|
+
file: fullPath,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Check for test files
|
|
280
|
+
if (item.name.includes('.test.') || item.name.includes('.spec.')) {
|
|
281
|
+
const testMatches = content.match(ID_PATTERNS.requirement) || [];
|
|
282
|
+
for (const id of testMatches) {
|
|
283
|
+
const existing = entries.find(e => e.id === id);
|
|
284
|
+
if (existing) {
|
|
285
|
+
existing.coverage.test = true;
|
|
286
|
+
existing.links.push({
|
|
287
|
+
source: id,
|
|
288
|
+
target: fullPath,
|
|
289
|
+
type: 'tests',
|
|
290
|
+
status: 'valid',
|
|
291
|
+
file: fullPath,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Directory doesn't exist or access error
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Build traceability links between artifacts
|
|
306
|
+
*/
|
|
307
|
+
async function buildTraceabilityLinks(entries, _specsDir, _srcDir) {
|
|
308
|
+
// Build links between requirements and designs
|
|
309
|
+
for (const entry of entries) {
|
|
310
|
+
if (entry.type === 'requirement') {
|
|
311
|
+
// Find related designs
|
|
312
|
+
const designs = entries.filter(e => e.type === 'design');
|
|
313
|
+
for (const design of designs) {
|
|
314
|
+
entry.links.push({
|
|
315
|
+
source: entry.id,
|
|
316
|
+
target: design.id,
|
|
317
|
+
type: 'derives',
|
|
318
|
+
status: 'valid',
|
|
319
|
+
});
|
|
320
|
+
entry.coverage.design = true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Link requirements to tasks
|
|
325
|
+
const requirements = entries.filter(e => e.type === 'requirement');
|
|
326
|
+
const tasks = entries.filter(e => e.type === 'task');
|
|
327
|
+
for (const req of requirements) {
|
|
328
|
+
for (const task of tasks) {
|
|
329
|
+
// Check if task references requirement
|
|
330
|
+
if (task.description.includes(req.id) ||
|
|
331
|
+
task.links.some(l => l.target === req.id)) {
|
|
332
|
+
req.links.push({
|
|
333
|
+
source: req.id,
|
|
334
|
+
target: task.id,
|
|
335
|
+
type: 'refines',
|
|
336
|
+
status: 'valid',
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Calculate statistics
|
|
344
|
+
*/
|
|
345
|
+
function calculateStatistics(entries) {
|
|
346
|
+
const requirements = entries.filter(e => e.type === 'requirement').length;
|
|
347
|
+
const designs = entries.filter(e => e.type === 'design').length;
|
|
348
|
+
const tasks = entries.filter(e => e.type === 'task').length;
|
|
349
|
+
const implementations = entries.filter(e => e.coverage.implementation).length;
|
|
350
|
+
const tests = entries.filter(e => e.coverage.test).length;
|
|
351
|
+
const coveredRequirements = entries.filter(e => e.type === 'requirement' && e.coverage.design && e.coverage.implementation).length;
|
|
352
|
+
const coverage = requirements > 0 ? (coveredRequirements / requirements) * 100 : 0;
|
|
353
|
+
return { requirements, designs, tasks, implementations, tests, coverage };
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Analyze impact
|
|
357
|
+
*/
|
|
358
|
+
function analyzeImpact(source, entries, depth) {
|
|
359
|
+
const impacts = [];
|
|
360
|
+
const visited = new Set();
|
|
361
|
+
function traverse(entry, level) {
|
|
362
|
+
if (level > depth || visited.has(entry.id))
|
|
363
|
+
return;
|
|
364
|
+
visited.add(entry.id);
|
|
365
|
+
for (const link of entry.links) {
|
|
366
|
+
const target = entries.find(e => e.id === link.target);
|
|
367
|
+
if (target && !visited.has(target.id)) {
|
|
368
|
+
impacts.push({
|
|
369
|
+
id: target.id,
|
|
370
|
+
type: target.type,
|
|
371
|
+
impactLevel: level === 1 ? 'direct' : level === 2 ? 'indirect' : 'potential',
|
|
372
|
+
description: target.description,
|
|
373
|
+
affectedFiles: target.links
|
|
374
|
+
.filter(l => l.file)
|
|
375
|
+
.map(l => l.file),
|
|
376
|
+
});
|
|
377
|
+
traverse(target, level + 1);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
traverse(source, 1);
|
|
382
|
+
return impacts;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Calculate risk level
|
|
386
|
+
*/
|
|
387
|
+
function calculateRiskLevel(impacts) {
|
|
388
|
+
const direct = impacts.filter(i => i.impactLevel === 'direct').length;
|
|
389
|
+
const totalIndirect = impacts.filter(i => i.impactLevel === 'indirect' || i.impactLevel === 'potential').length;
|
|
390
|
+
if (direct > 10 || impacts.length > 20)
|
|
391
|
+
return 'critical';
|
|
392
|
+
if (direct > 5 || impacts.length > 10)
|
|
393
|
+
return 'high';
|
|
394
|
+
if (direct > 2 || impacts.length > 5 || totalIndirect > 10)
|
|
395
|
+
return 'medium';
|
|
396
|
+
return 'low';
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Generate recommendations
|
|
400
|
+
*/
|
|
401
|
+
function generateRecommendations(source, impacts) {
|
|
402
|
+
const recommendations = [];
|
|
403
|
+
if (impacts.length > 10) {
|
|
404
|
+
recommendations.push(`Consider breaking the change to ${source.id} into smaller increments`);
|
|
405
|
+
}
|
|
406
|
+
const codeImpacts = impacts.filter(i => i.affectedFiles && i.affectedFiles.length > 0);
|
|
407
|
+
if (codeImpacts.length > 0) {
|
|
408
|
+
recommendations.push(`Review ${codeImpacts.length} code files before making changes to ${source.id}`);
|
|
409
|
+
}
|
|
410
|
+
if (impacts.some(i => i.type === 'test')) {
|
|
411
|
+
recommendations.push('Update affected tests after implementation');
|
|
412
|
+
}
|
|
413
|
+
if (impacts.length === 0) {
|
|
414
|
+
recommendations.push('This change has minimal impact - safe to proceed');
|
|
415
|
+
}
|
|
416
|
+
return recommendations;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Validate traceability
|
|
420
|
+
*/
|
|
421
|
+
function validateTraceability(entries, strict) {
|
|
422
|
+
const issues = [];
|
|
423
|
+
const allIds = new Set(entries.map(e => e.id));
|
|
424
|
+
for (const entry of entries) {
|
|
425
|
+
// Check for orphans (requirements without implementation)
|
|
426
|
+
if (entry.type === 'requirement' && !entry.coverage.implementation) {
|
|
427
|
+
issues.push({
|
|
428
|
+
type: 'orphan',
|
|
429
|
+
severity: strict ? 'error' : 'warning',
|
|
430
|
+
source: entry.id,
|
|
431
|
+
message: `Requirement ${entry.id} has no implementation`,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
// Check for broken links
|
|
435
|
+
for (const link of entry.links) {
|
|
436
|
+
if (typeof link.target === 'string' && link.target.match(/^(REQ|DES|TSK)-/)) {
|
|
437
|
+
if (!allIds.has(link.target)) {
|
|
438
|
+
issues.push({
|
|
439
|
+
type: 'broken_link',
|
|
440
|
+
severity: 'error',
|
|
441
|
+
source: entry.id,
|
|
442
|
+
target: link.target,
|
|
443
|
+
message: `Link from ${entry.id} to ${link.target} is broken (target not found)`,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Check for missing design traceability
|
|
449
|
+
if (entry.type === 'requirement' && !entry.coverage.design && strict) {
|
|
450
|
+
issues.push({
|
|
451
|
+
type: 'missing_link',
|
|
452
|
+
severity: 'warning',
|
|
453
|
+
source: entry.id,
|
|
454
|
+
message: `Requirement ${entry.id} has no design element`,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
// Check for missing tests
|
|
458
|
+
if (entry.type === 'requirement' && !entry.coverage.test && strict) {
|
|
459
|
+
issues.push({
|
|
460
|
+
type: 'missing_link',
|
|
461
|
+
severity: 'warning',
|
|
462
|
+
source: entry.id,
|
|
463
|
+
message: `Requirement ${entry.id} has no test coverage`,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return issues;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Generate markdown matrix
|
|
471
|
+
*/
|
|
472
|
+
function generateMarkdownMatrix(entries, stats) {
|
|
473
|
+
let output = '# Traceability Matrix\n\n';
|
|
474
|
+
output += '## Statistics\n\n';
|
|
475
|
+
output += `| Metric | Count |\n`;
|
|
476
|
+
output += `|--------|-------|\n`;
|
|
477
|
+
output += `| Requirements | ${stats.requirements} |\n`;
|
|
478
|
+
output += `| Designs | ${stats.designs} |\n`;
|
|
479
|
+
output += `| Tasks | ${stats.tasks} |\n`;
|
|
480
|
+
output += `| Implementations | ${stats.implementations} |\n`;
|
|
481
|
+
output += `| Tests | ${stats.tests} |\n`;
|
|
482
|
+
output += `| **Coverage** | **${stats.coverage.toFixed(1)}%** |\n\n`;
|
|
483
|
+
output += '## Matrix\n\n';
|
|
484
|
+
output += '| ID | Type | Design | Impl | Test |\n';
|
|
485
|
+
output += '|----|------|--------|------|------|\n';
|
|
486
|
+
for (const entry of entries) {
|
|
487
|
+
output += `| ${entry.id} | ${entry.type} | ${entry.coverage.design ? '✅' : '❌'} | ${entry.coverage.implementation ? '✅' : '❌'} | ${entry.coverage.test ? '✅' : '❌'} |\n`;
|
|
488
|
+
}
|
|
489
|
+
output += `\n*Generated: ${new Date().toISOString()}*\n`;
|
|
490
|
+
return output;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Generate CSV matrix
|
|
494
|
+
*/
|
|
495
|
+
function generateCSVMatrix(entries) {
|
|
496
|
+
let output = 'ID,Type,Description,Design,Implementation,Test,Links\n';
|
|
497
|
+
for (const entry of entries) {
|
|
498
|
+
const links = entry.links.map(l => `${l.type}:${l.target}`).join(';');
|
|
499
|
+
output += `"${entry.id}","${entry.type}","${entry.description}",${entry.coverage.design},${entry.coverage.implementation},${entry.coverage.test},"${links}"\n`;
|
|
500
|
+
}
|
|
501
|
+
return output;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Generate HTML matrix
|
|
505
|
+
*/
|
|
506
|
+
function generateHTMLMatrix(entries, stats) {
|
|
507
|
+
return `<!DOCTYPE html>
|
|
508
|
+
<html>
|
|
509
|
+
<head>
|
|
510
|
+
<title>Traceability Matrix</title>
|
|
511
|
+
<style>
|
|
512
|
+
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
513
|
+
table { border-collapse: collapse; width: 100%; }
|
|
514
|
+
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
|
515
|
+
th { background-color: #4CAF50; color: white; }
|
|
516
|
+
tr:nth-child(even) { background-color: #f2f2f2; }
|
|
517
|
+
.check { color: #4CAF50; }
|
|
518
|
+
.cross { color: #f44336; }
|
|
519
|
+
.summary { margin: 20px 0; padding: 15px; background: #f5f5f5; border-radius: 5px; }
|
|
520
|
+
</style>
|
|
521
|
+
</head>
|
|
522
|
+
<body>
|
|
523
|
+
<h1>Traceability Matrix</h1>
|
|
524
|
+
|
|
525
|
+
<div class="summary">
|
|
526
|
+
<h2>Coverage: ${stats.coverage.toFixed(1)}%</h2>
|
|
527
|
+
<p>Requirements: ${stats.requirements} | Designs: ${stats.designs} | Tasks: ${stats.tasks} | Tests: ${stats.tests}</p>
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
<table>
|
|
531
|
+
<tr>
|
|
532
|
+
<th>ID</th>
|
|
533
|
+
<th>Type</th>
|
|
534
|
+
<th>Design</th>
|
|
535
|
+
<th>Implementation</th>
|
|
536
|
+
<th>Test</th>
|
|
537
|
+
</tr>
|
|
538
|
+
${entries.map(e => `
|
|
539
|
+
<tr>
|
|
540
|
+
<td>${e.id}</td>
|
|
541
|
+
<td>${e.type}</td>
|
|
542
|
+
<td class="${e.coverage.design ? 'check' : 'cross'}">${e.coverage.design ? '✓' : '✗'}</td>
|
|
543
|
+
<td class="${e.coverage.implementation ? 'check' : 'cross'}">${e.coverage.implementation ? '✓' : '✗'}</td>
|
|
544
|
+
<td class="${e.coverage.test ? 'check' : 'cross'}">${e.coverage.test ? '✓' : '✗'}</td>
|
|
545
|
+
</tr>`).join('')}
|
|
546
|
+
</table>
|
|
547
|
+
|
|
548
|
+
<p><em>Generated: ${new Date().toISOString()}</em></p>
|
|
549
|
+
</body>
|
|
550
|
+
</html>`;
|
|
551
|
+
}
|
|
552
|
+
export { collectArtifacts, buildTraceabilityLinks, analyzeImpact, validateTraceability, };
|
|
553
|
+
//# sourceMappingURL=trace.js.map
|