musubi-sdd 0.7.0 → 0.8.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/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 Format** - Unambiguous requirements with complete traceability
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
- musubi-share sync # Sync across AI platforms
113
- musubi-share status # Show sharing status
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.7.0",
3
+ "version": "0.8.1",
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;