musubi-sdd 0.8.1 → 0.8.2
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 +11 -2
- package/README.md +10 -1
- package/bin/musubi-design.js +320 -0
- package/package.json +3 -2
- package/src/generators/design.js +662 -0
package/README.ja.md
CHANGED
|
@@ -14,6 +14,7 @@ MUSUBIは、6つの主要フレームワークのベスト機能を統合した
|
|
|
14
14
|
- その他4エージェント: AGENTS.md(互換形式)
|
|
15
15
|
- 📋 **憲法ガバナンス** - 9つの不変条項 + フェーズ-1ゲートによる品質保証
|
|
16
16
|
- 📝 **EARS要件ジェネレーター** - 5つのEARSパターンで明確な要件を作成(v0.8.0)
|
|
17
|
+
- 🏗️ **設計ドキュメントジェネレーター** - トレーサビリティ付きC4モデルとADRを作成(v0.8.2)
|
|
17
18
|
- 🔄 **差分仕様** - ブラウンフィールドおよびグリーンフィールドプロジェクト対応
|
|
18
19
|
- 🧭 **自動更新プロジェクトメモリ** - ステアリングシステムがアーキテクチャ、技術スタック、製品コンテキストを維持
|
|
19
20
|
- 🚀 **自動オンボーディング** - `musubi-onboard` が既存プロジェクトを分析し、ステアリングドキュメントを生成(2-5分)
|
|
@@ -105,12 +106,20 @@ musubi-validate gates # フェーズ-1ゲートを検証
|
|
|
105
106
|
musubi-validate complexity # 複雑度制限をチェック
|
|
106
107
|
musubi-validate all -v # 詳細付き完全検証
|
|
107
108
|
|
|
108
|
-
# EARS
|
|
109
|
+
# EARS要件ドキュメント生成(v0.8.0)
|
|
109
110
|
musubi-requirements init "ユーザー認証" # 要件ドキュメント初期化
|
|
110
111
|
musubi-requirements add # インタラクティブに要件追加
|
|
111
112
|
musubi-requirements list # 全要件リスト表示
|
|
112
|
-
musubi-requirements validate # EARS
|
|
113
|
+
musubi-requirements validate # EARS形式を検証
|
|
113
114
|
musubi-requirements trace # トレーサビリティマトリクス表示
|
|
115
|
+
|
|
116
|
+
# 設計ドキュメント生成(v0.8.2)
|
|
117
|
+
musubi-design init "ユーザー認証" # 設計ドキュメント初期化
|
|
118
|
+
musubi-design add-c4 context # C4コンテキスト図を追加
|
|
119
|
+
musubi-design add-c4 container --format plantuml # PlantUMLでコンテナ図追加
|
|
120
|
+
musubi-design add-adr "JWTトークン使用" # アーキテクチャ決定記録追加
|
|
121
|
+
musubi-design validate # 設計完全性を検証
|
|
122
|
+
musubi-design trace # 要件トレーサビリティ表示
|
|
114
123
|
```
|
|
115
124
|
|
|
116
125
|
### プロジェクトタイプ
|
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ MUSUBI is a comprehensive SDD (Specification Driven Development) framework that
|
|
|
18
18
|
- Other 4 agents: AGENTS.md (compatible format)
|
|
19
19
|
- 📋 **Constitutional Governance** - 9 immutable articles + Phase -1 Gates for quality enforcement
|
|
20
20
|
- 📝 **EARS Requirements Generator** - Create unambiguous requirements with 5 EARS patterns (v0.8.0)
|
|
21
|
+
- 🏗️ **Design Document Generator** - Create C4 models and ADRs with traceability (v0.8.2)
|
|
21
22
|
- 🔄 **Delta Specifications** - Brownfield and greenfield project support
|
|
22
23
|
- 🧭 **Auto-Updating Project Memory** - Steering system maintains architecture, tech stack, and product context
|
|
23
24
|
- 🚀 **Automatic Onboarding** - `musubi-onboard` analyzes existing projects and generates steering docs (2-5 minutes)
|
|
@@ -109,12 +110,20 @@ musubi-validate gates # Validate Phase -1 Gates
|
|
|
109
110
|
musubi-validate complexity # Check complexity limits
|
|
110
111
|
musubi-validate all -v # Full validation with details
|
|
111
112
|
|
|
112
|
-
#
|
|
113
|
+
# Generate EARS requirements documents (v0.8.0)
|
|
113
114
|
musubi-requirements init "User Authentication" # Initialize requirements doc
|
|
114
115
|
musubi-requirements add # Add requirement interactively
|
|
115
116
|
musubi-requirements list # List all requirements
|
|
116
117
|
musubi-requirements validate # Validate EARS format
|
|
117
118
|
musubi-requirements trace # Show traceability matrix
|
|
119
|
+
|
|
120
|
+
# Generate design documents (v0.8.2)
|
|
121
|
+
musubi-design init "User Authentication" # Initialize design document
|
|
122
|
+
musubi-design add-c4 context # Add C4 Context diagram
|
|
123
|
+
musubi-design add-c4 container --format plantuml # Add Container with PlantUML
|
|
124
|
+
musubi-design add-adr "Use JWT for tokens" # Add Architecture Decision
|
|
125
|
+
musubi-design validate # Validate design completeness
|
|
126
|
+
musubi-design trace # Show requirements traceability
|
|
118
127
|
```
|
|
119
128
|
|
|
120
129
|
### Project Types
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MUSUBI Design Document Generator CLI
|
|
5
|
+
*
|
|
6
|
+
* Generates technical design documents with C4 model and ADR
|
|
7
|
+
* Complies with Article V (Traceability) and steering context
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* musubi-design init <feature> # Initialize design document
|
|
11
|
+
* musubi-design add-c4 <level> # Add C4 diagram (context|container|component|code)
|
|
12
|
+
* musubi-design add-adr <decision> # Add Architecture Decision Record
|
|
13
|
+
* musubi-design validate # Validate design completeness
|
|
14
|
+
* musubi-design trace # Show requirement traceability
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { Command } = require('commander');
|
|
18
|
+
const chalk = require('chalk');
|
|
19
|
+
const inquirer = require('inquirer');
|
|
20
|
+
const DesignGenerator = require('../src/generators/design');
|
|
21
|
+
|
|
22
|
+
const program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('musubi-design')
|
|
26
|
+
.description('Design Document Generator - Create C4 models and ADRs')
|
|
27
|
+
.version('0.8.2');
|
|
28
|
+
|
|
29
|
+
// Initialize design document
|
|
30
|
+
program
|
|
31
|
+
.command('init <feature>')
|
|
32
|
+
.description('Initialize design document for a feature')
|
|
33
|
+
.option('-o, --output <path>', 'Output directory', 'docs/design')
|
|
34
|
+
.option('-a, --author <name>', 'Author name')
|
|
35
|
+
.option('--project <name>', 'Project name')
|
|
36
|
+
.option('-r, --requirements <path>', 'Requirements file path')
|
|
37
|
+
.action(async (feature, options) => {
|
|
38
|
+
try {
|
|
39
|
+
console.log(chalk.bold(`\n🏗️ Initializing design for: ${feature}\n`));
|
|
40
|
+
|
|
41
|
+
const generator = new DesignGenerator(process.cwd());
|
|
42
|
+
const result = await generator.init(feature, options);
|
|
43
|
+
|
|
44
|
+
console.log(chalk.green('✓ Design document created'));
|
|
45
|
+
console.log(chalk.dim(` ${result.path}`));
|
|
46
|
+
console.log();
|
|
47
|
+
console.log(chalk.bold('Next steps:'));
|
|
48
|
+
console.log(chalk.dim(` 1. Edit ${result.path}`));
|
|
49
|
+
console.log(chalk.dim(' 2. Add C4 diagrams: musubi-design add-c4 <level>'));
|
|
50
|
+
console.log(chalk.dim(' 3. Add ADRs: musubi-design add-adr <decision>'));
|
|
51
|
+
console.log(chalk.dim(' 4. Validate: musubi-design validate'));
|
|
52
|
+
console.log();
|
|
53
|
+
|
|
54
|
+
process.exit(0);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Add C4 diagram
|
|
62
|
+
program
|
|
63
|
+
.command('add-c4 <level>')
|
|
64
|
+
.description('Add C4 model diagram (context|container|component|code)')
|
|
65
|
+
.option('-f, --file <path>', 'Design file path')
|
|
66
|
+
.option('--format <type>', 'Diagram format (mermaid|plantuml)', 'mermaid')
|
|
67
|
+
.action(async (level, options) => {
|
|
68
|
+
try {
|
|
69
|
+
console.log(chalk.bold(`\n📊 Adding C4 ${level} diagram\n`));
|
|
70
|
+
|
|
71
|
+
const generator = new DesignGenerator(process.cwd());
|
|
72
|
+
|
|
73
|
+
// Find design file
|
|
74
|
+
let designFile = options.file;
|
|
75
|
+
if (!designFile) {
|
|
76
|
+
const files = await generator.findDesignFiles();
|
|
77
|
+
if (files.length === 0) {
|
|
78
|
+
console.error(chalk.red('✗ No design files found'));
|
|
79
|
+
console.log(chalk.dim(' Run: musubi-design init <feature>'));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (files.length === 1) {
|
|
84
|
+
designFile = files[0];
|
|
85
|
+
} else {
|
|
86
|
+
const answer = await inquirer.prompt([{
|
|
87
|
+
type: 'list',
|
|
88
|
+
name: 'file',
|
|
89
|
+
message: 'Select design file:',
|
|
90
|
+
choices: files
|
|
91
|
+
}]);
|
|
92
|
+
designFile = answer.file;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Interactive prompts for C4 diagram
|
|
97
|
+
const answers = await inquirer.prompt([
|
|
98
|
+
{
|
|
99
|
+
type: 'input',
|
|
100
|
+
name: 'title',
|
|
101
|
+
message: 'Diagram title:',
|
|
102
|
+
default: `${level.charAt(0).toUpperCase() + level.slice(1)} Diagram`,
|
|
103
|
+
validate: (input) => input.length > 0 || 'Title is required'
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: 'input',
|
|
107
|
+
name: 'description',
|
|
108
|
+
message: 'Diagram description:',
|
|
109
|
+
default: `Shows ${level}-level architecture`
|
|
110
|
+
}
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const diagram = {
|
|
114
|
+
level,
|
|
115
|
+
title: answers.title,
|
|
116
|
+
description: answers.description,
|
|
117
|
+
format: options.format
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const result = await generator.addC4Diagram(designFile, diagram);
|
|
121
|
+
|
|
122
|
+
console.log(chalk.green('\n✓ C4 diagram added:'));
|
|
123
|
+
console.log(chalk.dim(` ${result.level}: ${result.title}`));
|
|
124
|
+
console.log(chalk.bold('\n📝 Diagram Template:'));
|
|
125
|
+
console.log(chalk.cyan(result.template));
|
|
126
|
+
console.log();
|
|
127
|
+
|
|
128
|
+
process.exit(0);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Add Architecture Decision Record
|
|
136
|
+
program
|
|
137
|
+
.command('add-adr <decision>')
|
|
138
|
+
.description('Add Architecture Decision Record')
|
|
139
|
+
.option('-f, --file <path>', 'Design file path')
|
|
140
|
+
.option('--status <status>', 'ADR status (proposed|accepted|rejected|deprecated)', 'proposed')
|
|
141
|
+
.action(async (decision, options) => {
|
|
142
|
+
try {
|
|
143
|
+
console.log(chalk.bold(`\n📜 Adding ADR: ${decision}\n`));
|
|
144
|
+
|
|
145
|
+
const generator = new DesignGenerator(process.cwd());
|
|
146
|
+
|
|
147
|
+
// Find design file
|
|
148
|
+
let designFile = options.file;
|
|
149
|
+
if (!designFile) {
|
|
150
|
+
const files = await generator.findDesignFiles();
|
|
151
|
+
if (files.length === 0) {
|
|
152
|
+
console.error(chalk.red('✗ No design files found'));
|
|
153
|
+
console.log(chalk.dim(' Run: musubi-design init <feature>'));
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (files.length === 1) {
|
|
158
|
+
designFile = files[0];
|
|
159
|
+
} else {
|
|
160
|
+
const answer = await inquirer.prompt([{
|
|
161
|
+
type: 'list',
|
|
162
|
+
name: 'file',
|
|
163
|
+
message: 'Select design file:',
|
|
164
|
+
choices: files
|
|
165
|
+
}]);
|
|
166
|
+
designFile = answer.file;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Interactive prompts for ADR
|
|
171
|
+
const answers = await inquirer.prompt([
|
|
172
|
+
{
|
|
173
|
+
type: 'input',
|
|
174
|
+
name: 'context',
|
|
175
|
+
message: 'What is the context/problem?',
|
|
176
|
+
validate: (input) => input.length > 0 || 'Context is required'
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: 'input',
|
|
180
|
+
name: 'decision',
|
|
181
|
+
message: 'What is the decision?',
|
|
182
|
+
validate: (input) => input.length > 0 || 'Decision is required'
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: 'input',
|
|
186
|
+
name: 'consequences',
|
|
187
|
+
message: 'What are the consequences?',
|
|
188
|
+
validate: (input) => input.length > 0 || 'Consequences are required'
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: 'input',
|
|
192
|
+
name: 'alternatives',
|
|
193
|
+
message: 'Alternatives considered (comma-separated):',
|
|
194
|
+
filter: (input) => input.split(',').map(s => s.trim()).filter(s => s.length > 0)
|
|
195
|
+
}
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
const adr = {
|
|
199
|
+
title: decision,
|
|
200
|
+
status: options.status,
|
|
201
|
+
context: answers.context,
|
|
202
|
+
decision: answers.decision,
|
|
203
|
+
consequences: answers.consequences,
|
|
204
|
+
alternatives: answers.alternatives
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const result = await generator.addADR(designFile, adr);
|
|
208
|
+
|
|
209
|
+
console.log(chalk.green('\n✓ ADR added:'));
|
|
210
|
+
console.log(chalk.dim(` ADR-${result.number}: ${result.title}`));
|
|
211
|
+
console.log(chalk.dim(` Status: ${result.status}`));
|
|
212
|
+
console.log();
|
|
213
|
+
|
|
214
|
+
process.exit(0);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Validate design document
|
|
222
|
+
program
|
|
223
|
+
.command('validate')
|
|
224
|
+
.description('Validate design document completeness')
|
|
225
|
+
.option('-f, --file <path>', 'Specific design file')
|
|
226
|
+
.option('-v, --verbose', 'Show detailed validation results')
|
|
227
|
+
.action(async (options) => {
|
|
228
|
+
try {
|
|
229
|
+
console.log(chalk.bold('\n🔍 Validating Design Documents\n'));
|
|
230
|
+
|
|
231
|
+
const generator = new DesignGenerator(process.cwd());
|
|
232
|
+
const results = await generator.validate(options.file);
|
|
233
|
+
|
|
234
|
+
if (results.passed) {
|
|
235
|
+
console.log(chalk.green('✓ All designs valid\n'));
|
|
236
|
+
} else {
|
|
237
|
+
console.log(chalk.red('✗ Validation failed\n'));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log(chalk.bold('Summary:'));
|
|
241
|
+
console.log(chalk.dim(` Total: ${results.total}`));
|
|
242
|
+
console.log(chalk.green(` Valid: ${results.valid}`));
|
|
243
|
+
console.log(chalk.red(` Invalid: ${results.invalid}`));
|
|
244
|
+
console.log();
|
|
245
|
+
|
|
246
|
+
if (results.violations.length > 0) {
|
|
247
|
+
console.log(chalk.bold.red('Violations:'));
|
|
248
|
+
results.violations.forEach(v => {
|
|
249
|
+
console.log(chalk.red(` • ${v}`));
|
|
250
|
+
});
|
|
251
|
+
console.log();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (options.verbose && results.details) {
|
|
255
|
+
console.log(chalk.bold('Details:'));
|
|
256
|
+
results.details.forEach(d => {
|
|
257
|
+
const icon = d.valid ? chalk.green('✓') : chalk.red('✗');
|
|
258
|
+
console.log(` ${icon} ${d.file}: ${d.message}`);
|
|
259
|
+
});
|
|
260
|
+
console.log();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
process.exit(results.passed ? 0 : 1);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Show requirement traceability
|
|
271
|
+
program
|
|
272
|
+
.command('trace')
|
|
273
|
+
.description('Show requirement-to-design traceability')
|
|
274
|
+
.option('-f, --file <path>', 'Specific design file')
|
|
275
|
+
.option('--format <type>', 'Output format (table|json|markdown)', 'table')
|
|
276
|
+
.action(async (options) => {
|
|
277
|
+
try {
|
|
278
|
+
console.log(chalk.bold('\n🔗 Design Traceability Matrix\n'));
|
|
279
|
+
|
|
280
|
+
const generator = new DesignGenerator(process.cwd());
|
|
281
|
+
const matrix = await generator.generateTraceabilityMatrix(options.file);
|
|
282
|
+
|
|
283
|
+
if (options.format === 'json') {
|
|
284
|
+
console.log(JSON.stringify(matrix, null, 2));
|
|
285
|
+
} else {
|
|
286
|
+
console.log(chalk.bold('| Requirement | Design | Components | Status |'));
|
|
287
|
+
console.log('|-------------|--------|------------|--------|');
|
|
288
|
+
|
|
289
|
+
matrix.forEach(row => {
|
|
290
|
+
const design = row.design ? '✓' : '-';
|
|
291
|
+
const components = row.components || 0;
|
|
292
|
+
const status = row.traced ? chalk.green('Traced') : chalk.yellow('Missing');
|
|
293
|
+
|
|
294
|
+
console.log(`| ${row.requirement} | ${design} | ${components} | ${status} |`);
|
|
295
|
+
});
|
|
296
|
+
console.log();
|
|
297
|
+
|
|
298
|
+
const traced = matrix.filter(r => r.traced).length;
|
|
299
|
+
const total = matrix.length;
|
|
300
|
+
const percentage = total > 0 ? Math.round((traced / total) * 100) : 0;
|
|
301
|
+
|
|
302
|
+
console.log(chalk.bold('Coverage:'));
|
|
303
|
+
console.log(chalk.dim(` ${traced}/${total} requirements traced (${percentage}%)`));
|
|
304
|
+
console.log();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
process.exit(0);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Parse arguments
|
|
315
|
+
program.parse(process.argv);
|
|
316
|
+
|
|
317
|
+
// Show help if no command provided
|
|
318
|
+
if (!process.argv.slice(2).length) {
|
|
319
|
+
program.outputHelp();
|
|
320
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musubi-sdd",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
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": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"musubi-analyze": "bin/musubi-analyze.js",
|
|
13
13
|
"musubi-share": "bin/musubi-share.js",
|
|
14
14
|
"musubi-validate": "bin/musubi-validate.js",
|
|
15
|
-
"musubi-requirements": "bin/musubi-requirements.js"
|
|
15
|
+
"musubi-requirements": "bin/musubi-requirements.js",
|
|
16
|
+
"musubi-design": "bin/musubi-design.js"
|
|
16
17
|
},
|
|
17
18
|
"scripts": {
|
|
18
19
|
"test": "jest",
|
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MUSUBI Design Document Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates technical design documents with C4 model and ADR
|
|
5
|
+
* Complies with Article V (Traceability) and steering context
|
|
6
|
+
*
|
|
7
|
+
* C4 Model Levels:
|
|
8
|
+
* 1. Context: System in its environment
|
|
9
|
+
* 2. Container: High-level technology choices
|
|
10
|
+
* 3. Component: Components within containers
|
|
11
|
+
* 4. Code: Class/component implementation details
|
|
12
|
+
*
|
|
13
|
+
* ADR Format:
|
|
14
|
+
* - Title: Decision name
|
|
15
|
+
* - Status: proposed|accepted|rejected|deprecated
|
|
16
|
+
* - Context: Problem/situation
|
|
17
|
+
* - Decision: What we decided
|
|
18
|
+
* - Consequences: Positive and negative outcomes
|
|
19
|
+
* - Alternatives: Other options considered
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs-extra');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { glob } = require('glob');
|
|
25
|
+
|
|
26
|
+
class DesignGenerator {
|
|
27
|
+
constructor(rootDir) {
|
|
28
|
+
this.rootDir = rootDir;
|
|
29
|
+
this.templatePath = path.join(__dirname, '../../src/templates/shared/documents/design.md');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Initialize design document for a feature
|
|
34
|
+
* @param {string} feature - Feature name
|
|
35
|
+
* @param {object} options - Options (output, author, project, requirements)
|
|
36
|
+
* @returns {Promise<object>} Result with path
|
|
37
|
+
*/
|
|
38
|
+
async init(feature, options = {}) {
|
|
39
|
+
const outputDir = path.join(this.rootDir, options.output || 'docs/design');
|
|
40
|
+
await fs.ensureDir(outputDir);
|
|
41
|
+
|
|
42
|
+
const fileName = `${this.slugify(feature)}.md`;
|
|
43
|
+
const filePath = path.join(outputDir, fileName);
|
|
44
|
+
|
|
45
|
+
// Check if file exists
|
|
46
|
+
if (await fs.pathExists(filePath)) {
|
|
47
|
+
throw new Error(`Design file already exists: ${filePath}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Load template
|
|
51
|
+
const template = await fs.readFile(this.templatePath, 'utf-8');
|
|
52
|
+
|
|
53
|
+
// Get project name from package.json
|
|
54
|
+
let projectName = options.project;
|
|
55
|
+
if (!projectName) {
|
|
56
|
+
try {
|
|
57
|
+
const pkg = await fs.readJSON(path.join(this.rootDir, 'package.json'));
|
|
58
|
+
projectName = pkg.name || 'Project';
|
|
59
|
+
} catch {
|
|
60
|
+
projectName = 'Project';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Get author from git config or options
|
|
65
|
+
let author = options.author;
|
|
66
|
+
if (!author) {
|
|
67
|
+
try {
|
|
68
|
+
const { execSync } = require('child_process');
|
|
69
|
+
author = execSync('git config user.name', { encoding: 'utf-8' }).trim();
|
|
70
|
+
} catch {
|
|
71
|
+
author = 'Author';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Replace template variables
|
|
76
|
+
const content = template
|
|
77
|
+
.replace(/\{\{FEATURE_NAME\}\}/g, feature)
|
|
78
|
+
.replace(/\{\{PROJECT_NAME\}\}/g, projectName)
|
|
79
|
+
.replace(/\{\{DATE\}\}/g, new Date().toISOString().split('T')[0])
|
|
80
|
+
.replace(/\{\{AUTHOR\}\}/g, author)
|
|
81
|
+
.replace(/\{\{SYSTEM\}\}/g, projectName);
|
|
82
|
+
|
|
83
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
84
|
+
|
|
85
|
+
return { path: filePath };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Find all design files in the project
|
|
90
|
+
* @returns {Promise<string[]>} List of design file paths
|
|
91
|
+
*/
|
|
92
|
+
async findDesignFiles() {
|
|
93
|
+
const patterns = [
|
|
94
|
+
'docs/design/**/*.md',
|
|
95
|
+
'docs/design/*.md',
|
|
96
|
+
'design/**/*.md',
|
|
97
|
+
'design/*.md'
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const files = [];
|
|
101
|
+
for (const pattern of patterns) {
|
|
102
|
+
const matches = await glob(pattern, { cwd: this.rootDir, absolute: true });
|
|
103
|
+
files.push(...matches);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return [...new Set(files)];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Add C4 diagram to design file
|
|
111
|
+
* @param {string} filePath - Design file path
|
|
112
|
+
* @param {object} diagram - Diagram data
|
|
113
|
+
* @returns {Promise<object>} Result with level and template
|
|
114
|
+
*/
|
|
115
|
+
async addC4Diagram(filePath, diagram) {
|
|
116
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
117
|
+
|
|
118
|
+
// Generate C4 diagram template
|
|
119
|
+
const template = this.generateC4Template(diagram);
|
|
120
|
+
|
|
121
|
+
// Find insertion point based on level
|
|
122
|
+
const insertionPoint = this.findC4InsertionPoint(content, diagram.level);
|
|
123
|
+
|
|
124
|
+
const newContent = content.slice(0, insertionPoint) + template + '\n' + content.slice(insertionPoint);
|
|
125
|
+
|
|
126
|
+
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
level: diagram.level,
|
|
130
|
+
title: diagram.title,
|
|
131
|
+
template
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generate C4 diagram template
|
|
137
|
+
* @param {object} diagram - Diagram data
|
|
138
|
+
* @returns {string} Mermaid or PlantUML template
|
|
139
|
+
*/
|
|
140
|
+
generateC4Template(diagram) {
|
|
141
|
+
const { level, title, description, format } = diagram;
|
|
142
|
+
|
|
143
|
+
if (format === 'plantuml') {
|
|
144
|
+
return this.generatePlantUMLTemplate(level, title, description);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Default: Mermaid
|
|
148
|
+
return this.generateMermaidTemplate(level, title, description);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Generate Mermaid C4 template
|
|
153
|
+
* @param {string} level - C4 level
|
|
154
|
+
* @param {string} title - Diagram title
|
|
155
|
+
* @param {string} description - Diagram description
|
|
156
|
+
* @returns {string} Mermaid template
|
|
157
|
+
*/
|
|
158
|
+
generateMermaidTemplate(level, title, description) {
|
|
159
|
+
const templates = {
|
|
160
|
+
context: `### C4 Context: ${title}
|
|
161
|
+
|
|
162
|
+
**${description}**
|
|
163
|
+
|
|
164
|
+
\`\`\`mermaid
|
|
165
|
+
C4Context
|
|
166
|
+
title ${title}
|
|
167
|
+
|
|
168
|
+
Person(user, "User", "End user of the system")
|
|
169
|
+
System(system, "System Name", "System description")
|
|
170
|
+
System_Ext(external, "External System", "External service")
|
|
171
|
+
|
|
172
|
+
Rel(user, system, "Uses", "HTTPS")
|
|
173
|
+
Rel(system, external, "Calls", "API")
|
|
174
|
+
\`\`\`
|
|
175
|
+
|
|
176
|
+
**Key Elements**:
|
|
177
|
+
- **Users**: [List users/personas]
|
|
178
|
+
- **System**: [System boundary]
|
|
179
|
+
- **External Systems**: [External dependencies]
|
|
180
|
+
`,
|
|
181
|
+
container: `### C4 Container: ${title}
|
|
182
|
+
|
|
183
|
+
**${description}**
|
|
184
|
+
|
|
185
|
+
\`\`\`mermaid
|
|
186
|
+
C4Container
|
|
187
|
+
title ${title}
|
|
188
|
+
|
|
189
|
+
Person(user, "User")
|
|
190
|
+
|
|
191
|
+
Container_Boundary(c1, "System Name") {
|
|
192
|
+
Container(web, "Web Application", "React", "User interface")
|
|
193
|
+
Container(api, "API", "Node.js", "Business logic")
|
|
194
|
+
ContainerDb(db, "Database", "PostgreSQL", "Data storage")
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
Rel(user, web, "Uses", "HTTPS")
|
|
198
|
+
Rel(web, api, "Calls", "REST/JSON")
|
|
199
|
+
Rel(api, db, "Reads/Writes", "SQL")
|
|
200
|
+
\`\`\`
|
|
201
|
+
|
|
202
|
+
**Containers**:
|
|
203
|
+
- **Frontend**: [Technology and purpose]
|
|
204
|
+
- **Backend**: [Technology and purpose]
|
|
205
|
+
- **Database**: [Technology and purpose]
|
|
206
|
+
`,
|
|
207
|
+
component: `### C4 Component: ${title}
|
|
208
|
+
|
|
209
|
+
**${description}**
|
|
210
|
+
|
|
211
|
+
\`\`\`mermaid
|
|
212
|
+
C4Component
|
|
213
|
+
title ${title}
|
|
214
|
+
|
|
215
|
+
Container_Boundary(api, "API Container") {
|
|
216
|
+
Component(controller, "Controller", "Express", "Handles requests")
|
|
217
|
+
Component(service, "Service", "TypeScript", "Business logic")
|
|
218
|
+
Component(repository, "Repository", "TypeORM", "Data access")
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
ContainerDb(db, "Database")
|
|
222
|
+
|
|
223
|
+
Rel(controller, service, "Uses")
|
|
224
|
+
Rel(service, repository, "Uses")
|
|
225
|
+
Rel(repository, db, "Queries")
|
|
226
|
+
\`\`\`
|
|
227
|
+
|
|
228
|
+
**Components**:
|
|
229
|
+
- **Controller**: [Responsibility]
|
|
230
|
+
- **Service**: [Responsibility]
|
|
231
|
+
- **Repository**: [Responsibility]
|
|
232
|
+
`,
|
|
233
|
+
code: `### C4 Code: ${title}
|
|
234
|
+
|
|
235
|
+
**${description}**
|
|
236
|
+
|
|
237
|
+
\`\`\`mermaid
|
|
238
|
+
classDiagram
|
|
239
|
+
class Controller {
|
|
240
|
+
+handleRequest()
|
|
241
|
+
+validateInput()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
class Service {
|
|
245
|
+
+executeBusinessLogic()
|
|
246
|
+
+processData()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
class Repository {
|
|
250
|
+
+findById()
|
|
251
|
+
+save()
|
|
252
|
+
+delete()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
Controller --> Service
|
|
256
|
+
Service --> Repository
|
|
257
|
+
\`\`\`
|
|
258
|
+
|
|
259
|
+
**Classes**:
|
|
260
|
+
- **Controller**: [Purpose and key methods]
|
|
261
|
+
- **Service**: [Purpose and key methods]
|
|
262
|
+
- **Repository**: [Purpose and key methods]
|
|
263
|
+
`
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return templates[level] || templates.context;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Generate PlantUML C4 template
|
|
271
|
+
* @param {string} level - C4 level
|
|
272
|
+
* @param {string} title - Diagram title
|
|
273
|
+
* @param {string} description - Diagram description
|
|
274
|
+
* @returns {string} PlantUML template
|
|
275
|
+
*/
|
|
276
|
+
generatePlantUMLTemplate(level, title, description) {
|
|
277
|
+
const templates = {
|
|
278
|
+
context: `### C4 Context: ${title}
|
|
279
|
+
|
|
280
|
+
**${description}**
|
|
281
|
+
|
|
282
|
+
\`\`\`plantuml
|
|
283
|
+
@startuml
|
|
284
|
+
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml
|
|
285
|
+
|
|
286
|
+
LAYOUT_WITH_LEGEND()
|
|
287
|
+
|
|
288
|
+
title ${title}
|
|
289
|
+
|
|
290
|
+
Person(user, "User", "End user")
|
|
291
|
+
System(system, "System Name", "System description")
|
|
292
|
+
System_Ext(external, "External System", "External service")
|
|
293
|
+
|
|
294
|
+
Rel(user, system, "Uses", "HTTPS")
|
|
295
|
+
Rel(system, external, "Calls", "API")
|
|
296
|
+
|
|
297
|
+
@enduml
|
|
298
|
+
\`\`\`
|
|
299
|
+
`,
|
|
300
|
+
container: `### C4 Container: ${title}
|
|
301
|
+
|
|
302
|
+
**${description}**
|
|
303
|
+
|
|
304
|
+
\`\`\`plantuml
|
|
305
|
+
@startuml
|
|
306
|
+
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
|
|
307
|
+
|
|
308
|
+
LAYOUT_WITH_LEGEND()
|
|
309
|
+
|
|
310
|
+
title ${title}
|
|
311
|
+
|
|
312
|
+
Person(user, "User")
|
|
313
|
+
|
|
314
|
+
System_Boundary(c1, "System Name") {
|
|
315
|
+
Container(web, "Web App", "React", "User interface")
|
|
316
|
+
Container(api, "API", "Node.js", "Business logic")
|
|
317
|
+
ContainerDb(db, "Database", "PostgreSQL", "Data storage")
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
Rel(user, web, "Uses", "HTTPS")
|
|
321
|
+
Rel(web, api, "Calls", "REST/JSON")
|
|
322
|
+
Rel(api, db, "Reads/Writes", "SQL")
|
|
323
|
+
|
|
324
|
+
@enduml
|
|
325
|
+
\`\`\`
|
|
326
|
+
`,
|
|
327
|
+
component: `### C4 Component: ${title}
|
|
328
|
+
|
|
329
|
+
**${description}**
|
|
330
|
+
|
|
331
|
+
\`\`\`plantuml
|
|
332
|
+
@startuml
|
|
333
|
+
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml
|
|
334
|
+
|
|
335
|
+
LAYOUT_WITH_LEGEND()
|
|
336
|
+
|
|
337
|
+
title ${title}
|
|
338
|
+
|
|
339
|
+
Container_Boundary(api, "API Container") {
|
|
340
|
+
Component(controller, "Controller", "Express", "Handles requests")
|
|
341
|
+
Component(service, "Service", "TypeScript", "Business logic")
|
|
342
|
+
Component(repository, "Repository", "TypeORM", "Data access")
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
ContainerDb(db, "Database")
|
|
346
|
+
|
|
347
|
+
Rel(controller, service, "Uses")
|
|
348
|
+
Rel(service, repository, "Uses")
|
|
349
|
+
Rel(repository, db, "Queries")
|
|
350
|
+
|
|
351
|
+
@enduml
|
|
352
|
+
\`\`\`
|
|
353
|
+
`,
|
|
354
|
+
code: `### C4 Code: ${title}
|
|
355
|
+
|
|
356
|
+
**${description}**
|
|
357
|
+
|
|
358
|
+
\`\`\`plantuml
|
|
359
|
+
@startuml
|
|
360
|
+
title ${title}
|
|
361
|
+
|
|
362
|
+
class Controller {
|
|
363
|
+
+handleRequest()
|
|
364
|
+
+validateInput()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
class Service {
|
|
368
|
+
+executeBusinessLogic()
|
|
369
|
+
+processData()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
class Repository {
|
|
373
|
+
+findById()
|
|
374
|
+
+save()
|
|
375
|
+
+delete()
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
Controller --> Service
|
|
379
|
+
Service --> Repository
|
|
380
|
+
|
|
381
|
+
@enduml
|
|
382
|
+
\`\`\`
|
|
383
|
+
`
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
return templates[level] || templates.context;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get C4 section name
|
|
391
|
+
* @param {string} level - C4 level
|
|
392
|
+
* @returns {string} Section name
|
|
393
|
+
*/
|
|
394
|
+
getC4SectionName(level) {
|
|
395
|
+
const names = {
|
|
396
|
+
context: 'C4 Model: Context Diagram',
|
|
397
|
+
container: 'C4 Model: Container Diagram',
|
|
398
|
+
component: 'C4 Model: Component Diagram',
|
|
399
|
+
code: 'C4 Model: Code Diagram'
|
|
400
|
+
};
|
|
401
|
+
return names[level] || 'Architecture Design';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Find C4 insertion point in document
|
|
406
|
+
* @param {string} content - Document content
|
|
407
|
+
* @param {string} _level - C4 level
|
|
408
|
+
* @returns {number} Insertion index
|
|
409
|
+
*/
|
|
410
|
+
findC4InsertionPoint(content, _level) {
|
|
411
|
+
// Find Architecture Design section
|
|
412
|
+
const archMatch = content.match(/## Architecture Design/);
|
|
413
|
+
if (!archMatch) {
|
|
414
|
+
throw new Error('Architecture Design section not found');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Find next section after Architecture Design
|
|
418
|
+
const afterArch = content.slice(archMatch.index + archMatch[0].length);
|
|
419
|
+
const nextSectionMatch = afterArch.match(/\n## [^#]/);
|
|
420
|
+
|
|
421
|
+
if (nextSectionMatch) {
|
|
422
|
+
return archMatch.index + archMatch[0].length + nextSectionMatch.index;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// If no next section, append at end
|
|
426
|
+
return content.length;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Add Architecture Decision Record
|
|
431
|
+
* @param {string} filePath - Design file path
|
|
432
|
+
* @param {object} adr - ADR data
|
|
433
|
+
* @returns {Promise<object>} Result with ADR number
|
|
434
|
+
*/
|
|
435
|
+
async addADR(filePath, adr) {
|
|
436
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
437
|
+
|
|
438
|
+
// Generate ADR number
|
|
439
|
+
const number = this.generateADRNumber(content);
|
|
440
|
+
|
|
441
|
+
// Format ADR section
|
|
442
|
+
const section = this.formatADRSection(number, adr);
|
|
443
|
+
|
|
444
|
+
// Find insertion point (Architecture Decisions section)
|
|
445
|
+
const insertionPoint = this.findADRInsertionPoint(content);
|
|
446
|
+
const newContent = content.slice(0, insertionPoint) + section + '\n' + content.slice(insertionPoint);
|
|
447
|
+
|
|
448
|
+
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
number,
|
|
452
|
+
title: adr.title,
|
|
453
|
+
status: adr.status
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Generate ADR number
|
|
459
|
+
* @param {string} content - Document content
|
|
460
|
+
* @returns {string} ADR number (ADR-001, ADR-002, etc.)
|
|
461
|
+
*/
|
|
462
|
+
generateADRNumber(content) {
|
|
463
|
+
const regex = /### ADR-(\d+):/g;
|
|
464
|
+
|
|
465
|
+
let maxNum = 0;
|
|
466
|
+
let match;
|
|
467
|
+
while ((match = regex.exec(content)) !== null) {
|
|
468
|
+
const num = parseInt(match[1], 10);
|
|
469
|
+
if (num > maxNum) maxNum = num;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const nextNum = (maxNum + 1).toString().padStart(3, '0');
|
|
473
|
+
return `ADR-${nextNum}`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Format ADR section
|
|
478
|
+
* @param {string} number - ADR number
|
|
479
|
+
* @param {object} adr - ADR data
|
|
480
|
+
* @returns {string} Formatted ADR section
|
|
481
|
+
*/
|
|
482
|
+
formatADRSection(number, adr) {
|
|
483
|
+
const date = new Date().toISOString().split('T')[0];
|
|
484
|
+
|
|
485
|
+
let section = `### ${number}: ${adr.title}\n\n`;
|
|
486
|
+
section += `**Status**: ${adr.status}\n`;
|
|
487
|
+
section += `**Date**: ${date}\n\n`;
|
|
488
|
+
section += `**Context**:\n\n${adr.context}\n\n`;
|
|
489
|
+
section += `**Decision**:\n\n${adr.decision}\n\n`;
|
|
490
|
+
section += `**Consequences**:\n\n${adr.consequences}\n\n`;
|
|
491
|
+
|
|
492
|
+
if (adr.alternatives && adr.alternatives.length > 0) {
|
|
493
|
+
section += '**Alternatives Considered**:\n\n';
|
|
494
|
+
adr.alternatives.forEach(alt => {
|
|
495
|
+
section += `- ${alt}\n`;
|
|
496
|
+
});
|
|
497
|
+
section += '\n';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return section;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Find ADR insertion point in document
|
|
505
|
+
* @param {string} content - Document content
|
|
506
|
+
* @returns {number} Insertion index
|
|
507
|
+
*/
|
|
508
|
+
findADRInsertionPoint(content) {
|
|
509
|
+
// Find Architecture Decisions section
|
|
510
|
+
const adrMatch = content.match(/## Architecture Decisions/);
|
|
511
|
+
if (!adrMatch) {
|
|
512
|
+
// If section doesn't exist, add it before next major section
|
|
513
|
+
const nextMatch = content.match(/\n## [^#]/);
|
|
514
|
+
if (nextMatch) {
|
|
515
|
+
return nextMatch.index;
|
|
516
|
+
}
|
|
517
|
+
return content.length;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Find next section after Architecture Decisions
|
|
521
|
+
const afterADR = content.slice(adrMatch.index + adrMatch[0].length);
|
|
522
|
+
const nextSectionMatch = afterADR.match(/\n## [^#]/);
|
|
523
|
+
|
|
524
|
+
if (nextSectionMatch) {
|
|
525
|
+
return adrMatch.index + adrMatch[0].length + nextSectionMatch.index;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return content.length;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Validate design documents
|
|
533
|
+
* @param {string} [filePath] - Specific file path
|
|
534
|
+
* @returns {Promise<object>} Validation results
|
|
535
|
+
*/
|
|
536
|
+
async validate(filePath = null) {
|
|
537
|
+
const files = filePath ? [filePath] : await this.findDesignFiles();
|
|
538
|
+
const violations = [];
|
|
539
|
+
const details = [];
|
|
540
|
+
|
|
541
|
+
for (const file of files) {
|
|
542
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
543
|
+
const errors = this.validateDesignDocument(content);
|
|
544
|
+
|
|
545
|
+
if (errors.length > 0) {
|
|
546
|
+
violations.push(`${path.basename(file)}: ${errors.join(', ')}`);
|
|
547
|
+
details.push({
|
|
548
|
+
file: path.basename(file),
|
|
549
|
+
valid: false,
|
|
550
|
+
message: errors.join(', ')
|
|
551
|
+
});
|
|
552
|
+
} else {
|
|
553
|
+
details.push({
|
|
554
|
+
file: path.basename(file),
|
|
555
|
+
valid: true,
|
|
556
|
+
message: 'Design document complete'
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
passed: violations.length === 0,
|
|
563
|
+
total: files.length,
|
|
564
|
+
valid: files.length - violations.length,
|
|
565
|
+
invalid: violations.length,
|
|
566
|
+
violations,
|
|
567
|
+
details
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Validate single design document
|
|
573
|
+
* @param {string} content - Document content
|
|
574
|
+
* @returns {string[]} List of errors
|
|
575
|
+
*/
|
|
576
|
+
validateDesignDocument(content) {
|
|
577
|
+
const errors = [];
|
|
578
|
+
|
|
579
|
+
// Check for required sections
|
|
580
|
+
if (!content.includes('## Architecture Design')) {
|
|
581
|
+
errors.push('Missing Architecture Design section');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (!content.includes('## Steering Context')) {
|
|
585
|
+
errors.push('Missing Steering Context section');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Check for C4 diagrams (at least one level)
|
|
589
|
+
const hasC4 = content.includes('C4Context') ||
|
|
590
|
+
content.includes('C4Container') ||
|
|
591
|
+
content.includes('C4Component') ||
|
|
592
|
+
content.includes('C4 Model');
|
|
593
|
+
|
|
594
|
+
if (!hasC4) {
|
|
595
|
+
errors.push('Missing C4 model diagrams');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return errors;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Generate traceability matrix
|
|
603
|
+
* @param {string} [_filePath] - Specific file path
|
|
604
|
+
* @returns {Promise<object[]>} Traceability matrix
|
|
605
|
+
*/
|
|
606
|
+
async generateTraceabilityMatrix(_filePath = null) {
|
|
607
|
+
const matrix = [];
|
|
608
|
+
|
|
609
|
+
// Find requirement files
|
|
610
|
+
const reqFiles = await glob('docs/requirements/**/*.md', {
|
|
611
|
+
cwd: this.rootDir,
|
|
612
|
+
absolute: true
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
for (const reqFile of reqFiles) {
|
|
616
|
+
const content = await fs.readFile(reqFile, 'utf-8');
|
|
617
|
+
const requirements = this.parseRequirements(content);
|
|
618
|
+
|
|
619
|
+
for (const req of requirements) {
|
|
620
|
+
matrix.push({
|
|
621
|
+
requirement: req.id,
|
|
622
|
+
design: false,
|
|
623
|
+
components: 0,
|
|
624
|
+
traced: false
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return matrix;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Parse requirements from content
|
|
634
|
+
* @param {string} content - File content
|
|
635
|
+
* @returns {object[]} List of requirements
|
|
636
|
+
*/
|
|
637
|
+
parseRequirements(content) {
|
|
638
|
+
const requirements = [];
|
|
639
|
+
const reqRegex = /### (REQ-[A-Z]+-\d+):/g;
|
|
640
|
+
|
|
641
|
+
let match;
|
|
642
|
+
while ((match = reqRegex.exec(content)) !== null) {
|
|
643
|
+
requirements.push({ id: match[1] });
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return requirements;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Convert string to slug
|
|
651
|
+
* @param {string} str - Input string
|
|
652
|
+
* @returns {string} Slug
|
|
653
|
+
*/
|
|
654
|
+
slugify(str) {
|
|
655
|
+
return str
|
|
656
|
+
.toLowerCase()
|
|
657
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
658
|
+
.replace(/^-|-$/g, '');
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
module.exports = DesignGenerator;
|