musubi-sdd 0.7.0 → 0.8.0
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 +8 -1
- package/README.md +8 -4
- package/bin/musubi-requirements.js +330 -0
- package/package.json +3 -2
- package/src/generators/requirements.js +425 -0
package/README.ja.md
CHANGED
|
@@ -13,7 +13,7 @@ MUSUBIは、6つの主要フレームワークのベスト機能を統合した
|
|
|
13
13
|
- GitHub Copilot & Cursor: AGENTS.md(公式サポート)
|
|
14
14
|
- その他4エージェント: AGENTS.md(互換形式)
|
|
15
15
|
- 📋 **憲法ガバナンス** - 9つの不変条項 + フェーズ-1ゲートによる品質保証
|
|
16
|
-
- 📝 **EARS
|
|
16
|
+
- 📝 **EARS要件ジェネレーター** - 5つのEARSパターンで明確な要件を作成(v0.8.0)
|
|
17
17
|
- 🔄 **差分仕様** - ブラウンフィールドおよびグリーンフィールドプロジェクト対応
|
|
18
18
|
- 🧭 **自動更新プロジェクトメモリ** - ステアリングシステムがアーキテクチャ、技術スタック、製品コンテキストを維持
|
|
19
19
|
- 🚀 **自動オンボーディング** - `musubi-onboard` が既存プロジェクトを分析し、ステアリングドキュメントを生成(2-5分)
|
|
@@ -104,6 +104,13 @@ musubi-validate article 3 # テストファースト原則を検証
|
|
|
104
104
|
musubi-validate gates # フェーズ-1ゲートを検証
|
|
105
105
|
musubi-validate complexity # 複雑度制限をチェック
|
|
106
106
|
musubi-validate all -v # 詳細付き完全検証
|
|
107
|
+
|
|
108
|
+
# EARS要件の作成(v0.8.0)
|
|
109
|
+
musubi-requirements init "ユーザー認証" # 要件ドキュメント初期化
|
|
110
|
+
musubi-requirements add # インタラクティブに要件追加
|
|
111
|
+
musubi-requirements list # 全要件リスト表示
|
|
112
|
+
musubi-requirements validate # EARS形式検証
|
|
113
|
+
musubi-requirements trace # トレーサビリティマトリクス表示
|
|
107
114
|
```
|
|
108
115
|
|
|
109
116
|
### プロジェクトタイプ
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ MUSUBI is a comprehensive SDD (Specification Driven Development) framework that
|
|
|
17
17
|
- GitHub Copilot & Cursor: AGENTS.md (official support)
|
|
18
18
|
- Other 4 agents: AGENTS.md (compatible format)
|
|
19
19
|
- 📋 **Constitutional Governance** - 9 immutable articles + Phase -1 Gates for quality enforcement
|
|
20
|
-
- 📝 **EARS Requirements
|
|
20
|
+
- 📝 **EARS Requirements Generator** - Create unambiguous requirements with 5 EARS patterns (v0.8.0)
|
|
21
21
|
- 🔄 **Delta Specifications** - Brownfield and greenfield project support
|
|
22
22
|
- 🧭 **Auto-Updating Project Memory** - Steering system maintains architecture, tech stack, and product context
|
|
23
23
|
- 🚀 **Automatic Onboarding** - `musubi-onboard` analyzes existing projects and generates steering docs (2-5 minutes)
|
|
@@ -108,9 +108,13 @@ musubi-validate article 3 # Validate Test-First Imperative
|
|
|
108
108
|
musubi-validate gates # Validate Phase -1 Gates
|
|
109
109
|
musubi-validate complexity # Check complexity limits
|
|
110
110
|
musubi-validate all -v # Full validation with details
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
musubi-
|
|
111
|
+
|
|
112
|
+
# Create EARS requirements (v0.8.0)
|
|
113
|
+
musubi-requirements init "User Authentication" # Initialize requirements doc
|
|
114
|
+
musubi-requirements add # Add requirement interactively
|
|
115
|
+
musubi-requirements list # List all requirements
|
|
116
|
+
musubi-requirements validate # Validate EARS format
|
|
117
|
+
musubi-requirements trace # Show traceability matrix
|
|
114
118
|
```
|
|
115
119
|
|
|
116
120
|
### Project Types
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MUSUBI Requirements Generator CLI
|
|
5
|
+
*
|
|
6
|
+
* Generates EARS (Easy Approach to Requirements Syntax) formatted requirements
|
|
7
|
+
* Supports 5 EARS patterns for unambiguous specification
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* musubi-requirements init <feature> # Initialize requirements document
|
|
11
|
+
* musubi-requirements add <pattern> <title> # Add requirement with EARS pattern
|
|
12
|
+
* musubi-requirements list # List all requirements
|
|
13
|
+
* musubi-requirements validate # Validate EARS format compliance
|
|
14
|
+
* musubi-requirements trace # Show traceability matrix
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { Command } = require('commander');
|
|
18
|
+
const chalk = require('chalk');
|
|
19
|
+
const inquirer = require('inquirer');
|
|
20
|
+
const RequirementsGenerator = require('../src/generators/requirements');
|
|
21
|
+
|
|
22
|
+
const program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('musubi-requirements')
|
|
26
|
+
.description('EARS Requirements Generator - Create unambiguous specifications')
|
|
27
|
+
.version('0.8.0');
|
|
28
|
+
|
|
29
|
+
// Initialize requirements document
|
|
30
|
+
program
|
|
31
|
+
.command('init <feature>')
|
|
32
|
+
.description('Initialize requirements document for a feature')
|
|
33
|
+
.option('-o, --output <path>', 'Output directory', 'docs/requirements')
|
|
34
|
+
.option('-a, --author <name>', 'Author name')
|
|
35
|
+
.option('--project <name>', 'Project name')
|
|
36
|
+
.action(async (feature, options) => {
|
|
37
|
+
try {
|
|
38
|
+
console.log(chalk.bold(`\n📋 Initializing requirements for: ${feature}\n`));
|
|
39
|
+
|
|
40
|
+
const generator = new RequirementsGenerator(process.cwd());
|
|
41
|
+
const result = await generator.init(feature, options);
|
|
42
|
+
|
|
43
|
+
console.log(chalk.green('✓ Requirements document created'));
|
|
44
|
+
console.log(chalk.dim(` ${result.path}`));
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(chalk.bold('Next steps:'));
|
|
47
|
+
console.log(chalk.dim(` 1. Edit ${result.path}`));
|
|
48
|
+
console.log(chalk.dim(' 2. Add requirements: musubi-requirements add <pattern> <title>'));
|
|
49
|
+
console.log(chalk.dim(' 3. Validate: musubi-requirements validate'));
|
|
50
|
+
console.log();
|
|
51
|
+
|
|
52
|
+
process.exit(0);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Add requirement with EARS pattern
|
|
60
|
+
program
|
|
61
|
+
.command('add')
|
|
62
|
+
.description('Add requirement with EARS pattern (interactive)')
|
|
63
|
+
.option('-f, --file <path>', 'Requirements file path')
|
|
64
|
+
.option('-p, --pattern <type>', 'EARS pattern (ubiquitous|event|state|unwanted|optional)')
|
|
65
|
+
.option('-t, --title <text>', 'Requirement title')
|
|
66
|
+
.action(async (options) => {
|
|
67
|
+
try {
|
|
68
|
+
console.log(chalk.bold('\n📝 Add EARS Requirement\n'));
|
|
69
|
+
|
|
70
|
+
const generator = new RequirementsGenerator(process.cwd());
|
|
71
|
+
|
|
72
|
+
// Find requirements file
|
|
73
|
+
let reqFile = options.file;
|
|
74
|
+
if (!reqFile) {
|
|
75
|
+
const files = await generator.findRequirementsFiles();
|
|
76
|
+
if (files.length === 0) {
|
|
77
|
+
console.error(chalk.red('✗ No requirements files found'));
|
|
78
|
+
console.log(chalk.dim(' Run: musubi-requirements init <feature>'));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (files.length === 1) {
|
|
83
|
+
reqFile = files[0];
|
|
84
|
+
} else {
|
|
85
|
+
const answer = await inquirer.prompt([{
|
|
86
|
+
type: 'list',
|
|
87
|
+
name: 'file',
|
|
88
|
+
message: 'Select requirements file:',
|
|
89
|
+
choices: files
|
|
90
|
+
}]);
|
|
91
|
+
reqFile = answer.file;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Interactive prompts if not provided
|
|
96
|
+
let pattern = options.pattern;
|
|
97
|
+
let title = options.title;
|
|
98
|
+
|
|
99
|
+
if (!pattern || !title) {
|
|
100
|
+
const answers = await inquirer.prompt([
|
|
101
|
+
{
|
|
102
|
+
type: 'list',
|
|
103
|
+
name: 'pattern',
|
|
104
|
+
message: 'Select EARS pattern:',
|
|
105
|
+
choices: [
|
|
106
|
+
{ name: 'Ubiquitous - The [system] SHALL [requirement]', value: 'ubiquitous' },
|
|
107
|
+
{ name: 'Event-Driven - WHEN [event], THEN [system] SHALL [response]', value: 'event' },
|
|
108
|
+
{ name: 'State-Driven - WHILE [state], [system] SHALL [response]', value: 'state' },
|
|
109
|
+
{ name: 'Unwanted Behavior - IF [error], THEN [system] SHALL [response]', value: 'unwanted' },
|
|
110
|
+
{ name: 'Optional Feature - WHERE [feature], [system] SHALL [response]', value: 'optional' }
|
|
111
|
+
],
|
|
112
|
+
when: () => !pattern
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: 'input',
|
|
116
|
+
name: 'title',
|
|
117
|
+
message: 'Requirement title:',
|
|
118
|
+
when: () => !title,
|
|
119
|
+
validate: (input) => input.length > 0 || 'Title is required'
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: 'input',
|
|
123
|
+
name: 'system',
|
|
124
|
+
message: 'System/component name:',
|
|
125
|
+
default: 'system'
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
type: 'input',
|
|
129
|
+
name: 'statement',
|
|
130
|
+
message: (answers) => {
|
|
131
|
+
const prompts = {
|
|
132
|
+
ubiquitous: 'What SHALL the system do?',
|
|
133
|
+
event: 'What event triggers this? (WHEN...)',
|
|
134
|
+
state: 'What state/condition? (WHILE...)',
|
|
135
|
+
unwanted: 'What error/unwanted condition? (IF...)',
|
|
136
|
+
optional: 'What optional feature? (WHERE...)'
|
|
137
|
+
};
|
|
138
|
+
return prompts[answers.pattern || pattern];
|
|
139
|
+
},
|
|
140
|
+
validate: (input) => input.length > 0 || 'Statement is required'
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: 'input',
|
|
144
|
+
name: 'response',
|
|
145
|
+
message: 'What SHALL the system do? (response)',
|
|
146
|
+
validate: (input) => input.length > 0 || 'Response is required'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: 'input',
|
|
150
|
+
name: 'criteria',
|
|
151
|
+
message: 'Acceptance criteria (comma-separated):',
|
|
152
|
+
filter: (input) => input.split(',').map(s => s.trim()).filter(s => s.length > 0)
|
|
153
|
+
}
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
pattern = pattern || answers.pattern;
|
|
157
|
+
title = title || answers.title;
|
|
158
|
+
|
|
159
|
+
const requirement = {
|
|
160
|
+
pattern,
|
|
161
|
+
title,
|
|
162
|
+
system: answers.system,
|
|
163
|
+
statement: answers.statement,
|
|
164
|
+
response: answers.response,
|
|
165
|
+
criteria: answers.criteria
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const result = await generator.addRequirement(reqFile, requirement);
|
|
169
|
+
|
|
170
|
+
console.log(chalk.green('\n✓ Requirement added:'));
|
|
171
|
+
console.log(chalk.dim(` ${result.id}: ${title}`));
|
|
172
|
+
console.log(chalk.bold('\n📄 EARS Statement:'));
|
|
173
|
+
console.log(chalk.cyan(result.statement));
|
|
174
|
+
console.log();
|
|
175
|
+
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// List all requirements
|
|
185
|
+
program
|
|
186
|
+
.command('list')
|
|
187
|
+
.description('List all requirements in the project')
|
|
188
|
+
.option('-f, --file <path>', 'Specific requirements file')
|
|
189
|
+
.option('--format <type>', 'Output format (table|json|markdown)', 'table')
|
|
190
|
+
.action(async (options) => {
|
|
191
|
+
try {
|
|
192
|
+
const generator = new RequirementsGenerator(process.cwd());
|
|
193
|
+
const requirements = await generator.listRequirements(options.file);
|
|
194
|
+
|
|
195
|
+
if (requirements.length === 0) {
|
|
196
|
+
console.log(chalk.yellow('No requirements found'));
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(chalk.bold(`\n📋 Requirements (${requirements.length} total)\n`));
|
|
201
|
+
|
|
202
|
+
if (options.format === 'json') {
|
|
203
|
+
console.log(JSON.stringify(requirements, null, 2));
|
|
204
|
+
} else if (options.format === 'markdown') {
|
|
205
|
+
requirements.forEach(req => {
|
|
206
|
+
console.log(`## ${req.id}: ${req.title}`);
|
|
207
|
+
console.log();
|
|
208
|
+
console.log(`**Pattern**: ${req.pattern}`);
|
|
209
|
+
console.log();
|
|
210
|
+
console.log(req.statement);
|
|
211
|
+
console.log();
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
// Table format
|
|
215
|
+
requirements.forEach(req => {
|
|
216
|
+
console.log(chalk.bold(`${req.id}: ${req.title}`));
|
|
217
|
+
console.log(chalk.dim(` Pattern: ${req.pattern}`));
|
|
218
|
+
console.log(chalk.cyan(` ${req.statement.substring(0, 100)}...`));
|
|
219
|
+
console.log();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
process.exit(0);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Validate EARS format compliance
|
|
231
|
+
program
|
|
232
|
+
.command('validate')
|
|
233
|
+
.description('Validate requirements against EARS format')
|
|
234
|
+
.option('-f, --file <path>', 'Specific requirements file')
|
|
235
|
+
.option('-v, --verbose', 'Show detailed validation results')
|
|
236
|
+
.action(async (options) => {
|
|
237
|
+
try {
|
|
238
|
+
console.log(chalk.bold('\n🔍 Validating EARS Requirements\n'));
|
|
239
|
+
|
|
240
|
+
const generator = new RequirementsGenerator(process.cwd());
|
|
241
|
+
const results = await generator.validate(options.file);
|
|
242
|
+
|
|
243
|
+
if (results.passed) {
|
|
244
|
+
console.log(chalk.green('✓ All requirements valid\n'));
|
|
245
|
+
} else {
|
|
246
|
+
console.log(chalk.red('✗ Validation failed\n'));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(chalk.bold('Summary:'));
|
|
250
|
+
console.log(chalk.dim(` Total: ${results.total}`));
|
|
251
|
+
console.log(chalk.green(` Valid: ${results.valid}`));
|
|
252
|
+
console.log(chalk.red(` Invalid: ${results.invalid}`));
|
|
253
|
+
console.log();
|
|
254
|
+
|
|
255
|
+
if (results.violations.length > 0) {
|
|
256
|
+
console.log(chalk.bold.red('Violations:'));
|
|
257
|
+
results.violations.forEach(v => {
|
|
258
|
+
console.log(chalk.red(` • ${v}`));
|
|
259
|
+
});
|
|
260
|
+
console.log();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (options.verbose && results.details) {
|
|
264
|
+
console.log(chalk.bold('Details:'));
|
|
265
|
+
results.details.forEach(d => {
|
|
266
|
+
const icon = d.valid ? chalk.green('✓') : chalk.red('✗');
|
|
267
|
+
console.log(` ${icon} ${d.id}: ${d.message}`);
|
|
268
|
+
});
|
|
269
|
+
console.log();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
process.exit(results.passed ? 0 : 1);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Show traceability matrix
|
|
280
|
+
program
|
|
281
|
+
.command('trace')
|
|
282
|
+
.description('Show requirements traceability matrix')
|
|
283
|
+
.option('-f, --file <path>', 'Specific requirements file')
|
|
284
|
+
.option('--format <type>', 'Output format (table|json|markdown)', 'table')
|
|
285
|
+
.action(async (options) => {
|
|
286
|
+
try {
|
|
287
|
+
console.log(chalk.bold('\n📊 Requirements Traceability Matrix\n'));
|
|
288
|
+
|
|
289
|
+
const generator = new RequirementsGenerator(process.cwd());
|
|
290
|
+
const matrix = await generator.generateTraceabilityMatrix(options.file);
|
|
291
|
+
|
|
292
|
+
if (options.format === 'json') {
|
|
293
|
+
console.log(JSON.stringify(matrix, null, 2));
|
|
294
|
+
} else {
|
|
295
|
+
console.log(chalk.bold('| Requirement | Design | Code | Tests | Status |'));
|
|
296
|
+
console.log('|-------------|--------|------|-------|--------|');
|
|
297
|
+
|
|
298
|
+
matrix.forEach(row => {
|
|
299
|
+
const design = row.design ? '✓' : '-';
|
|
300
|
+
const code = row.code ? '✓' : '-';
|
|
301
|
+
const tests = row.tests ? '✓' : '-';
|
|
302
|
+
const status = row.complete ? chalk.green('Complete') : chalk.yellow('Incomplete');
|
|
303
|
+
|
|
304
|
+
console.log(`| ${row.id} | ${design} | ${code} | ${tests} | ${status} |`);
|
|
305
|
+
});
|
|
306
|
+
console.log();
|
|
307
|
+
|
|
308
|
+
const complete = matrix.filter(r => r.complete).length;
|
|
309
|
+
const total = matrix.length;
|
|
310
|
+
const percentage = total > 0 ? Math.round((complete / total) * 100) : 0;
|
|
311
|
+
|
|
312
|
+
console.log(chalk.bold('Coverage:'));
|
|
313
|
+
console.log(chalk.dim(` ${complete}/${total} requirements traced (${percentage}%)`));
|
|
314
|
+
console.log();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
process.exit(0);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error(chalk.red('✗ Error:'), error.message);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Parse arguments
|
|
325
|
+
program.parse(process.argv);
|
|
326
|
+
|
|
327
|
+
// Show help if no command provided
|
|
328
|
+
if (!process.argv.slice(2).length) {
|
|
329
|
+
program.outputHelp();
|
|
330
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musubi-sdd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"musubi-sync": "bin/musubi-sync.js",
|
|
12
12
|
"musubi-analyze": "bin/musubi-analyze.js",
|
|
13
13
|
"musubi-share": "bin/musubi-share.js",
|
|
14
|
-
"musubi-validate": "bin/musubi-validate.js"
|
|
14
|
+
"musubi-validate": "bin/musubi-validate.js",
|
|
15
|
+
"musubi-requirements": "bin/musubi-requirements.js"
|
|
15
16
|
},
|
|
16
17
|
"scripts": {
|
|
17
18
|
"test": "jest",
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MUSUBI Requirements Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates EARS (Easy Approach to Requirements Syntax) formatted requirements
|
|
5
|
+
* Complies with Article IV of MUSUBI Constitution
|
|
6
|
+
*
|
|
7
|
+
* EARS Patterns:
|
|
8
|
+
* 1. Ubiquitous: The [system] SHALL [requirement]
|
|
9
|
+
* 2. Event-Driven: WHEN [event], THEN [system] SHALL [response]
|
|
10
|
+
* 3. State-Driven: WHILE [state], [system] SHALL [response]
|
|
11
|
+
* 4. Unwanted Behavior: IF [error], THEN [system] SHALL [response]
|
|
12
|
+
* 5. Optional Feature: WHERE [feature], [system] SHALL [response]
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs-extra');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { glob } = require('glob');
|
|
18
|
+
|
|
19
|
+
class RequirementsGenerator {
|
|
20
|
+
constructor(rootDir) {
|
|
21
|
+
this.rootDir = rootDir;
|
|
22
|
+
this.templatePath = path.join(__dirname, '../src/templates/shared/documents/requirements.md');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Initialize requirements document for a feature
|
|
27
|
+
* @param {string} feature - Feature name
|
|
28
|
+
* @param {object} options - Options (output, author, project)
|
|
29
|
+
* @returns {Promise<object>} Result with path
|
|
30
|
+
*/
|
|
31
|
+
async init(feature, options = {}) {
|
|
32
|
+
const outputDir = path.join(this.rootDir, options.output || 'docs/requirements');
|
|
33
|
+
await fs.ensureDir(outputDir);
|
|
34
|
+
|
|
35
|
+
const fileName = `${this.slugify(feature)}.md`;
|
|
36
|
+
const filePath = path.join(outputDir, fileName);
|
|
37
|
+
|
|
38
|
+
// Check if file exists
|
|
39
|
+
if (await fs.pathExists(filePath)) {
|
|
40
|
+
throw new Error(`Requirements file already exists: ${filePath}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Load template
|
|
44
|
+
const template = await fs.readFile(this.templatePath, 'utf-8');
|
|
45
|
+
|
|
46
|
+
// Get project name from package.json
|
|
47
|
+
let projectName = options.project;
|
|
48
|
+
if (!projectName) {
|
|
49
|
+
try {
|
|
50
|
+
const pkg = await fs.readJSON(path.join(this.rootDir, 'package.json'));
|
|
51
|
+
projectName = pkg.name || 'Project';
|
|
52
|
+
} catch {
|
|
53
|
+
projectName = 'Project';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Get author from git config or options
|
|
58
|
+
let author = options.author;
|
|
59
|
+
if (!author) {
|
|
60
|
+
try {
|
|
61
|
+
const { execSync } = require('child_process');
|
|
62
|
+
author = execSync('git config user.name', { encoding: 'utf-8' }).trim();
|
|
63
|
+
} catch {
|
|
64
|
+
author = 'Author';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Replace template variables
|
|
69
|
+
const content = template
|
|
70
|
+
.replace(/\{\{FEATURE_NAME\}\}/g, feature)
|
|
71
|
+
.replace(/\{\{PROJECT_NAME\}\}/g, projectName)
|
|
72
|
+
.replace(/\{\{DATE\}\}/g, new Date().toISOString().split('T')[0])
|
|
73
|
+
.replace(/\{\{AUTHOR\}\}/g, author)
|
|
74
|
+
.replace(/\{\{COMPONENT\}\}/g, this.slugify(feature).toUpperCase());
|
|
75
|
+
|
|
76
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
77
|
+
|
|
78
|
+
return { path: filePath };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Find all requirements files in the project
|
|
83
|
+
* @returns {Promise<string[]>} List of requirements file paths
|
|
84
|
+
*/
|
|
85
|
+
async findRequirementsFiles() {
|
|
86
|
+
const patterns = [
|
|
87
|
+
'docs/requirements/**/*.md',
|
|
88
|
+
'docs/requirements/*.md',
|
|
89
|
+
'requirements/**/*.md',
|
|
90
|
+
'requirements/*.md'
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
const files = [];
|
|
94
|
+
for (const pattern of patterns) {
|
|
95
|
+
const matches = await glob(pattern, { cwd: this.rootDir, absolute: true });
|
|
96
|
+
files.push(...matches);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return [...new Set(files)];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Add requirement to file
|
|
104
|
+
* @param {string} filePath - Requirements file path
|
|
105
|
+
* @param {object} requirement - Requirement data
|
|
106
|
+
* @returns {Promise<object>} Result with ID and statement
|
|
107
|
+
*/
|
|
108
|
+
async addRequirement(filePath, requirement) {
|
|
109
|
+
// Read existing file
|
|
110
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
111
|
+
|
|
112
|
+
// Generate requirement ID
|
|
113
|
+
const id = this.generateRequirementId(content, requirement.component);
|
|
114
|
+
|
|
115
|
+
// Generate EARS statement
|
|
116
|
+
const statement = this.generateEARSStatement(requirement);
|
|
117
|
+
|
|
118
|
+
// Format requirement section
|
|
119
|
+
const section = this.formatRequirementSection(id, requirement.title, statement, requirement.criteria);
|
|
120
|
+
|
|
121
|
+
// Find insertion point (end of Functional Requirements section)
|
|
122
|
+
const insertionPoint = this.findInsertionPoint(content);
|
|
123
|
+
const newContent = content.slice(0, insertionPoint) + section + '\n' + content.slice(insertionPoint);
|
|
124
|
+
|
|
125
|
+
await fs.writeFile(filePath, newContent, 'utf-8');
|
|
126
|
+
|
|
127
|
+
return { id, statement };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Generate unique requirement ID
|
|
132
|
+
* @param {string} content - File content
|
|
133
|
+
* @param {string} component - Component name
|
|
134
|
+
* @returns {string} Requirement ID (REQ-XXX-NNN)
|
|
135
|
+
*/
|
|
136
|
+
generateRequirementId(content, component = 'FEATURE') {
|
|
137
|
+
const prefix = `REQ-${component.toUpperCase()}-`;
|
|
138
|
+
const regex = new RegExp(`${this.escapeRegex(prefix)}(\\d+)`, 'g');
|
|
139
|
+
|
|
140
|
+
let maxNum = 0;
|
|
141
|
+
let match;
|
|
142
|
+
while ((match = regex.exec(content)) !== null) {
|
|
143
|
+
const num = parseInt(match[1], 10);
|
|
144
|
+
if (num > maxNum) maxNum = num;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const nextNum = (maxNum + 1).toString().padStart(3, '0');
|
|
148
|
+
return `${prefix}${nextNum}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Generate EARS statement based on pattern
|
|
153
|
+
* @param {object} data - Requirement data
|
|
154
|
+
* @returns {string} EARS statement
|
|
155
|
+
*/
|
|
156
|
+
generateEARSStatement(data) {
|
|
157
|
+
const { pattern, system, statement, response } = data;
|
|
158
|
+
|
|
159
|
+
switch (pattern) {
|
|
160
|
+
case 'ubiquitous':
|
|
161
|
+
return `The ${system} SHALL ${response}.`;
|
|
162
|
+
|
|
163
|
+
case 'event':
|
|
164
|
+
return `WHEN ${statement}, THEN the ${system} SHALL ${response}.`;
|
|
165
|
+
|
|
166
|
+
case 'state':
|
|
167
|
+
return `WHILE ${statement}, the ${system} SHALL ${response}.`;
|
|
168
|
+
|
|
169
|
+
case 'unwanted':
|
|
170
|
+
return `IF ${statement}, THEN the ${system} SHALL ${response}.`;
|
|
171
|
+
|
|
172
|
+
case 'optional':
|
|
173
|
+
return `WHERE ${statement}, the ${system} SHALL ${response}.`;
|
|
174
|
+
|
|
175
|
+
default:
|
|
176
|
+
throw new Error(`Unknown EARS pattern: ${pattern}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Format requirement section
|
|
182
|
+
* @param {string} id - Requirement ID
|
|
183
|
+
* @param {string} title - Title
|
|
184
|
+
* @param {string} statement - EARS statement
|
|
185
|
+
* @param {string[]} criteria - Acceptance criteria
|
|
186
|
+
* @returns {string} Formatted section
|
|
187
|
+
*/
|
|
188
|
+
formatRequirementSection(id, title, statement, criteria = []) {
|
|
189
|
+
let section = `### ${id}: ${title}\n\n`;
|
|
190
|
+
section += `${statement}\n\n`;
|
|
191
|
+
|
|
192
|
+
if (criteria.length > 0) {
|
|
193
|
+
section += '**Acceptance Criteria:**\n';
|
|
194
|
+
criteria.forEach(criterion => {
|
|
195
|
+
section += `- ${criterion}\n`;
|
|
196
|
+
});
|
|
197
|
+
section += '\n';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return section;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Find insertion point in document
|
|
205
|
+
* @param {string} content - Document content
|
|
206
|
+
* @returns {number} Insertion index
|
|
207
|
+
*/
|
|
208
|
+
findInsertionPoint(content) {
|
|
209
|
+
// Find end of Functional Requirements section
|
|
210
|
+
const functionalMatch = content.match(/## Functional Requirements/);
|
|
211
|
+
if (!functionalMatch) {
|
|
212
|
+
throw new Error('Functional Requirements section not found');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Find next section header
|
|
216
|
+
const afterFunctional = content.slice(functionalMatch.index + functionalMatch[0].length);
|
|
217
|
+
const nextSectionMatch = afterFunctional.match(/\n## [^#]/);
|
|
218
|
+
|
|
219
|
+
if (nextSectionMatch) {
|
|
220
|
+
return functionalMatch.index + functionalMatch[0].length + nextSectionMatch.index;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// If no next section, append at end
|
|
224
|
+
return content.length;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* List all requirements in file(s)
|
|
229
|
+
* @param {string} [filePath] - Specific file path
|
|
230
|
+
* @returns {Promise<object[]>} List of requirements
|
|
231
|
+
*/
|
|
232
|
+
async listRequirements(filePath = null) {
|
|
233
|
+
const files = filePath ? [filePath] : await this.findRequirementsFiles();
|
|
234
|
+
const requirements = [];
|
|
235
|
+
|
|
236
|
+
for (const file of files) {
|
|
237
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
238
|
+
const reqs = this.parseRequirements(content);
|
|
239
|
+
requirements.push(...reqs);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return requirements;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Parse requirements from content
|
|
247
|
+
* @param {string} content - File content
|
|
248
|
+
* @returns {object[]} List of requirements
|
|
249
|
+
*/
|
|
250
|
+
parseRequirements(content) {
|
|
251
|
+
const requirements = [];
|
|
252
|
+
const reqRegex = /### (REQ-[A-Z]+-\d+): (.+?)\n\n(.+?)(?=\n###|\n##|$)/gs;
|
|
253
|
+
|
|
254
|
+
let match;
|
|
255
|
+
while ((match = reqRegex.exec(content)) !== null) {
|
|
256
|
+
const [, id, title, body] = match;
|
|
257
|
+
const pattern = this.detectEARSPattern(body);
|
|
258
|
+
|
|
259
|
+
requirements.push({
|
|
260
|
+
id,
|
|
261
|
+
title,
|
|
262
|
+
statement: body.trim(),
|
|
263
|
+
pattern
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return requirements;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Detect EARS pattern in statement
|
|
272
|
+
* @param {string} statement - EARS statement
|
|
273
|
+
* @returns {string} Pattern type
|
|
274
|
+
*/
|
|
275
|
+
detectEARSPattern(statement) {
|
|
276
|
+
if (statement.startsWith('WHEN')) return 'event';
|
|
277
|
+
if (statement.startsWith('WHILE')) return 'state';
|
|
278
|
+
if (statement.startsWith('IF')) return 'unwanted';
|
|
279
|
+
if (statement.startsWith('WHERE')) return 'optional';
|
|
280
|
+
if (statement.match(/^The .+ SHALL/)) return 'ubiquitous';
|
|
281
|
+
return 'unknown';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Validate requirements against EARS format
|
|
286
|
+
* @param {string} [filePath] - Specific file path
|
|
287
|
+
* @returns {Promise<object>} Validation results
|
|
288
|
+
*/
|
|
289
|
+
async validate(filePath = null) {
|
|
290
|
+
const requirements = await this.listRequirements(filePath);
|
|
291
|
+
const violations = [];
|
|
292
|
+
const details = [];
|
|
293
|
+
|
|
294
|
+
for (const req of requirements) {
|
|
295
|
+
const errors = this.validateEARSFormat(req);
|
|
296
|
+
|
|
297
|
+
if (errors.length > 0) {
|
|
298
|
+
violations.push(`${req.id}: ${errors.join(', ')}`);
|
|
299
|
+
details.push({
|
|
300
|
+
id: req.id,
|
|
301
|
+
valid: false,
|
|
302
|
+
message: errors.join(', ')
|
|
303
|
+
});
|
|
304
|
+
} else {
|
|
305
|
+
details.push({
|
|
306
|
+
id: req.id,
|
|
307
|
+
valid: true,
|
|
308
|
+
message: 'Valid EARS format'
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
passed: violations.length === 0,
|
|
315
|
+
total: requirements.length,
|
|
316
|
+
valid: requirements.length - violations.length,
|
|
317
|
+
invalid: violations.length,
|
|
318
|
+
violations,
|
|
319
|
+
details
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Validate single requirement EARS format
|
|
325
|
+
* @param {object} requirement - Requirement object
|
|
326
|
+
* @returns {string[]} List of errors
|
|
327
|
+
*/
|
|
328
|
+
validateEARSFormat(requirement) {
|
|
329
|
+
const errors = [];
|
|
330
|
+
const { statement, pattern } = requirement;
|
|
331
|
+
|
|
332
|
+
// Check if pattern is recognized
|
|
333
|
+
if (pattern === 'unknown') {
|
|
334
|
+
errors.push('Unknown EARS pattern');
|
|
335
|
+
return errors;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check for SHALL keyword
|
|
339
|
+
if (!statement.includes('SHALL')) {
|
|
340
|
+
errors.push('Missing SHALL keyword');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Pattern-specific validation
|
|
344
|
+
switch (pattern) {
|
|
345
|
+
case 'event':
|
|
346
|
+
if (!statement.startsWith('WHEN') || !statement.includes('THEN')) {
|
|
347
|
+
errors.push('Event-driven pattern must use WHEN...THEN');
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
|
|
351
|
+
case 'state':
|
|
352
|
+
if (!statement.startsWith('WHILE')) {
|
|
353
|
+
errors.push('State-driven pattern must start with WHILE');
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
|
|
357
|
+
case 'unwanted':
|
|
358
|
+
if (!statement.startsWith('IF') || !statement.includes('THEN')) {
|
|
359
|
+
errors.push('Unwanted behavior pattern must use IF...THEN');
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
|
|
363
|
+
case 'optional':
|
|
364
|
+
if (!statement.startsWith('WHERE')) {
|
|
365
|
+
errors.push('Optional feature pattern must start with WHERE');
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
|
|
369
|
+
case 'ubiquitous':
|
|
370
|
+
if (!statement.match(/^The .+ SHALL/)) {
|
|
371
|
+
errors.push('Ubiquitous pattern must use "The [system] SHALL"');
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return errors;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Generate traceability matrix
|
|
381
|
+
* @param {string} [filePath] - Specific file path
|
|
382
|
+
* @returns {Promise<object[]>} Traceability matrix
|
|
383
|
+
*/
|
|
384
|
+
async generateTraceabilityMatrix(filePath = null) {
|
|
385
|
+
const requirements = await this.listRequirements(filePath);
|
|
386
|
+
const matrix = [];
|
|
387
|
+
|
|
388
|
+
for (const req of requirements) {
|
|
389
|
+
// TODO: Scan design docs, code, and tests for traceability
|
|
390
|
+
matrix.push({
|
|
391
|
+
id: req.id,
|
|
392
|
+
title: req.title,
|
|
393
|
+
design: false,
|
|
394
|
+
code: false,
|
|
395
|
+
tests: false,
|
|
396
|
+
complete: false
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return matrix;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Convert string to slug
|
|
405
|
+
* @param {string} str - Input string
|
|
406
|
+
* @returns {string} Slug
|
|
407
|
+
*/
|
|
408
|
+
slugify(str) {
|
|
409
|
+
return str
|
|
410
|
+
.toLowerCase()
|
|
411
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
412
|
+
.replace(/^-|-$/g, '');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Escape regex special characters
|
|
417
|
+
* @param {string} str - Input string
|
|
418
|
+
* @returns {string} Escaped string
|
|
419
|
+
*/
|
|
420
|
+
escapeRegex(str) {
|
|
421
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
module.exports = RequirementsGenerator;
|