musubi-sdd 0.8.5 → 0.8.6

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
@@ -15,7 +15,7 @@ MUSUBIは、6つの主要フレームワークのベスト機能を統合した
15
15
  - 📋 **憲法ガバナンス** - 9つの不変条項 + フェーズ-1ゲートによる品質保証
16
16
  - 📝 **EARS要件ジェネレーター** - 5つのEARSパターンで明確な要件を作成(v0.8.0)
17
17
  - 🏗️ **設計ドキュメントジェネレーター** - トレーサビリティ付きC4モデルとADRを作成(v0.8.2)
18
- - 🔄 **差分仕様** - ブラウンフィールドおよびグリーンフィールドプロジェクト対応
18
+ - 🔄 **変更管理システム** - ブラウンフィールドプロジェクト向け差分仕様(v0.8.6)
19
19
  - 🧭 **自動更新プロジェクトメモリ** - ステアリングシステムがアーキテクチャ、技術スタック、製品コンテキストを維持
20
20
  - 🚀 **自動オンボーディング** - `musubi-onboard` が既存プロジェクトを分析し、ステアリングドキュメントを生成(2-5分)
21
21
  - 🔄 **自動同期** - `musubi-sync` がコードベースの変更を検出し、ステアリングドキュメントを最新に保つ
@@ -138,6 +138,15 @@ musubi-trace coverage --min-coverage 100 # 100%カバレッジ要求
138
138
  musubi-trace gaps # 孤立した要件/コード検出
139
139
  musubi-trace requirement REQ-AUTH-001 # 特定要件をトレース
140
140
  musubi-trace validate # 100%トレーサビリティ検証(第5条)
141
+
142
+ # ブラウンフィールドプロジェクト向け変更管理(v0.8.6)
143
+ musubi-change init CHANGE-001 --title "認証機能追加" # 変更提案を作成
144
+ musubi-change validate CHANGE-001 --verbose # 差分仕様を検証
145
+ musubi-change apply CHANGE-001 --dry-run # 変更をプレビュー
146
+ musubi-change apply CHANGE-001 # コードベースに変更を適用
147
+ musubi-change archive CHANGE-001 # specs/にアーカイブ
148
+ musubi-change list --status pending # 保留中の変更をリスト
149
+ musubi-change list --format json # JSON形式でリスト
141
150
  ```
142
151
 
143
152
  ### プロジェクトタイプ
package/README.md CHANGED
@@ -19,7 +19,7 @@ MUSUBI is a comprehensive SDD (Specification Driven Development) framework that
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
21
  - 🏗️ **Design Document Generator** - Create C4 models and ADRs with traceability (v0.8.2)
22
- - 🔄 **Delta Specifications** - Brownfield and greenfield project support
22
+ - 🔄 **Change Management System** - Delta specifications for brownfield projects (v0.8.6)
23
23
  - 🧭 **Auto-Updating Project Memory** - Steering system maintains architecture, tech stack, and product context
24
24
  - 🚀 **Automatic Onboarding** - `musubi-onboard` analyzes existing projects and generates steering docs (2-5 minutes)
25
25
  - 🔄 **Auto-Sync** - `musubi-sync` detects codebase changes and keeps steering docs current
@@ -142,6 +142,15 @@ musubi-trace coverage --min-coverage 100 # Require 100% coverage
142
142
  musubi-trace gaps # Detect orphaned requirements/code
143
143
  musubi-trace requirement REQ-AUTH-001 # Trace specific requirement
144
144
  musubi-trace validate # Validate 100% traceability (Article V)
145
+
146
+ # Change management for brownfield projects (v0.8.6)
147
+ musubi-change init CHANGE-001 --title "Add authentication" # Create change proposal
148
+ musubi-change validate CHANGE-001 --verbose # Validate delta specification
149
+ musubi-change apply CHANGE-001 --dry-run # Preview changes
150
+ musubi-change apply CHANGE-001 # Apply changes to codebase
151
+ musubi-change archive CHANGE-001 # Archive to specs/
152
+ musubi-change list --status pending # List pending changes
153
+ musubi-change list --format json # List in JSON format
145
154
  ```
146
155
 
147
156
  ### Project Types
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MUSUBI Change Management CLI
5
+ *
6
+ * Manages delta specifications for brownfield projects
7
+ * Implements ADDED/MODIFIED/REMOVED/RENAMED change tracking
8
+ *
9
+ * Usage:
10
+ * musubi-change init <change-id> # Create change proposal
11
+ * musubi-change apply <change-id> # Apply change to codebase
12
+ * musubi-change archive <change-id> # Archive completed change
13
+ * musubi-change list # List all changes
14
+ * musubi-change validate <change-id> # Validate delta format
15
+ */
16
+
17
+ const { Command } = require('commander');
18
+ const chalk = require('chalk');
19
+ const ChangeManager = require('../src/managers/change.js');
20
+
21
+ const program = new Command();
22
+
23
+ program
24
+ .name('musubi-change')
25
+ .description('MUSUBI Change Management - Delta specifications for brownfield projects')
26
+ .version('0.8.6');
27
+
28
+ // Initialize change proposal
29
+ program
30
+ .command('init <change-id>')
31
+ .description('Create new change proposal with delta specification')
32
+ .option('-t, --title <title>', 'Change title')
33
+ .option('-d, --description <description>', 'Change description')
34
+ .option('--changes <dir>', 'Changes directory', 'storage/changes')
35
+ .option('--template <path>', 'Custom delta template')
36
+ .action(async (changeId, options) => {
37
+ try {
38
+ const workspaceRoot = process.cwd();
39
+ const manager = new ChangeManager(workspaceRoot);
40
+
41
+ console.log(chalk.blue('📝 Creating change proposal...'));
42
+ console.log(chalk.dim(`Change ID: ${changeId}`));
43
+
44
+ const result = await manager.initChange(changeId, {
45
+ title: options.title,
46
+ description: options.description,
47
+ changesDir: options.changes,
48
+ template: options.template
49
+ });
50
+
51
+ console.log(chalk.green('✓ Change proposal created successfully'));
52
+ console.log(chalk.dim(`Location: ${result.file}`));
53
+ console.log();
54
+ console.log(chalk.yellow('Next steps:'));
55
+ console.log(chalk.dim('1. Edit the delta specification'));
56
+ console.log(chalk.dim(`2. Run: musubi-change validate ${changeId}`));
57
+ console.log(chalk.dim(`3. Run: musubi-change apply ${changeId}`));
58
+ } catch (error) {
59
+ console.error(chalk.red('✗ Failed to create change proposal'));
60
+ console.error(chalk.dim(error.message));
61
+ process.exit(1);
62
+ }
63
+ });
64
+
65
+ // Apply change to codebase
66
+ program
67
+ .command('apply <change-id>')
68
+ .description('Apply change proposal to codebase')
69
+ .option('--changes <dir>', 'Changes directory', 'storage/changes')
70
+ .option('--dry-run', 'Preview changes without applying')
71
+ .option('--force', 'Force apply even with validation errors')
72
+ .action(async (changeId, options) => {
73
+ try {
74
+ const workspaceRoot = process.cwd();
75
+ const manager = new ChangeManager(workspaceRoot);
76
+
77
+ if (options.dryRun) {
78
+ console.log(chalk.blue('🔍 Previewing changes (dry run)...'));
79
+ } else {
80
+ console.log(chalk.blue('⚙️ Applying changes...'));
81
+ }
82
+
83
+ const result = await manager.applyChange(changeId, {
84
+ changesDir: options.changes,
85
+ dryRun: options.dryRun,
86
+ force: options.force
87
+ });
88
+
89
+ if (options.dryRun) {
90
+ console.log(chalk.yellow('\nPreview (no changes applied):'));
91
+ console.log(chalk.green(` ${result.stats.added} files to be added`));
92
+ console.log(chalk.blue(` ${result.stats.modified} files to be modified`));
93
+ console.log(chalk.red(` ${result.stats.removed} files to be removed`));
94
+ console.log(chalk.yellow(` ${result.stats.renamed} files to be renamed`));
95
+ } else {
96
+ console.log(chalk.green('\n✓ Changes applied successfully'));
97
+ console.log(chalk.green(` ${result.stats.added} files added`));
98
+ console.log(chalk.blue(` ${result.stats.modified} files modified`));
99
+ console.log(chalk.red(` ${result.stats.removed} files removed`));
100
+ console.log(chalk.yellow(` ${result.stats.renamed} files renamed`));
101
+
102
+ console.log();
103
+ console.log(chalk.yellow('Next steps:'));
104
+ console.log(chalk.dim('1. Test the changes'));
105
+ console.log(chalk.dim(`2. Run: musubi-change archive ${changeId}`));
106
+ }
107
+ } catch (error) {
108
+ console.error(chalk.red('✗ Failed to apply changes'));
109
+ console.error(chalk.dim(error.message));
110
+ process.exit(1);
111
+ }
112
+ });
113
+
114
+ // Archive completed change
115
+ program
116
+ .command('archive <change-id>')
117
+ .description('Archive completed change to specs/')
118
+ .option('--changes <dir>', 'Changes directory', 'storage/changes')
119
+ .option('--specs <dir>', 'Specs archive directory', 'specs')
120
+ .action(async (changeId, options) => {
121
+ try {
122
+ const workspaceRoot = process.cwd();
123
+ const manager = new ChangeManager(workspaceRoot);
124
+
125
+ console.log(chalk.blue('📦 Archiving change...'));
126
+
127
+ const result = await manager.archiveChange(changeId, {
128
+ changesDir: options.changes,
129
+ specsDir: options.specs
130
+ });
131
+
132
+ console.log(chalk.green('✓ Change archived successfully'));
133
+ console.log(chalk.dim(`Source: ${result.source}`));
134
+ console.log(chalk.dim(`Archive: ${result.archive}`));
135
+ console.log();
136
+ console.log(chalk.yellow('Delta merged to canonical specification'));
137
+ } catch (error) {
138
+ console.error(chalk.red('✗ Failed to archive change'));
139
+ console.error(chalk.dim(error.message));
140
+ process.exit(1);
141
+ }
142
+ });
143
+
144
+ // List all changes
145
+ program
146
+ .command('list')
147
+ .description('List all change proposals')
148
+ .option('--changes <dir>', 'Changes directory', 'storage/changes')
149
+ .option('--status <status>', 'Filter by status (pending|applied|archived)')
150
+ .option('--format <format>', 'Output format (table|json)', 'table')
151
+ .action(async (options) => {
152
+ try {
153
+ const workspaceRoot = process.cwd();
154
+ const manager = new ChangeManager(workspaceRoot);
155
+
156
+ const changes = await manager.listChanges({
157
+ changesDir: options.changes,
158
+ status: options.status
159
+ });
160
+
161
+ if (options.format === 'json') {
162
+ console.log(JSON.stringify(changes, null, 2));
163
+ return;
164
+ }
165
+
166
+ // Table format
167
+ console.log(chalk.bold('\nChange Proposals:\n'));
168
+
169
+ if (changes.length === 0) {
170
+ console.log(chalk.dim('No changes found'));
171
+ return;
172
+ }
173
+
174
+ console.log(chalk.dim('ID'.padEnd(20) + 'Title'.padEnd(40) + 'Status'.padEnd(15) + 'Date'));
175
+ console.log(chalk.dim('-'.repeat(90)));
176
+
177
+ changes.forEach(change => {
178
+ const statusColor = {
179
+ pending: chalk.yellow,
180
+ applied: chalk.blue,
181
+ archived: chalk.green
182
+ }[change.status] || chalk.white;
183
+
184
+ console.log(
185
+ change.id.padEnd(20) +
186
+ change.title.padEnd(40) +
187
+ statusColor(change.status.padEnd(15)) +
188
+ change.date
189
+ );
190
+ });
191
+
192
+ console.log();
193
+ console.log(chalk.dim(`Total: ${changes.length} change(s)`));
194
+ } catch (error) {
195
+ console.error(chalk.red('✗ Failed to list changes'));
196
+ console.error(chalk.dim(error.message));
197
+ process.exit(1);
198
+ }
199
+ });
200
+
201
+ // Validate delta format
202
+ program
203
+ .command('validate <change-id>')
204
+ .description('Validate delta specification format')
205
+ .option('--changes <dir>', 'Changes directory', 'storage/changes')
206
+ .option('-v, --verbose', 'Show detailed validation results')
207
+ .action(async (changeId, options) => {
208
+ try {
209
+ const workspaceRoot = process.cwd();
210
+ const manager = new ChangeManager(workspaceRoot);
211
+
212
+ console.log(chalk.blue('🔍 Validating delta specification...'));
213
+
214
+ const result = await manager.validateChange(changeId, {
215
+ changesDir: options.changes,
216
+ verbose: options.verbose
217
+ });
218
+
219
+ if (result.valid) {
220
+ console.log(chalk.green('✓ Delta specification is valid'));
221
+
222
+ if (options.verbose) {
223
+ console.log();
224
+ console.log(chalk.bold('Summary:'));
225
+ console.log(chalk.green(` ${result.stats.added} ADDED items`));
226
+ console.log(chalk.blue(` ${result.stats.modified} MODIFIED items`));
227
+ console.log(chalk.red(` ${result.stats.removed} REMOVED items`));
228
+ console.log(chalk.yellow(` ${result.stats.renamed} RENAMED items`));
229
+ }
230
+ } else {
231
+ console.log(chalk.red('✗ Validation failed'));
232
+ console.log();
233
+
234
+ result.errors.forEach(error => {
235
+ console.log(chalk.red(` • ${error.message}`));
236
+ if (error.line) {
237
+ console.log(chalk.dim(` Line ${error.line}`));
238
+ }
239
+ });
240
+
241
+ process.exit(1);
242
+ }
243
+ } catch (error) {
244
+ console.error(chalk.red('✗ Failed to validate change'));
245
+ console.error(chalk.dim(error.message));
246
+ process.exit(1);
247
+ }
248
+ });
249
+
250
+ program.parse();
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musubi-sdd",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
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": {
@@ -15,7 +15,8 @@
15
15
  "musubi-requirements": "bin/musubi-requirements.js",
16
16
  "musubi-design": "bin/musubi-design.js",
17
17
  "musubi-tasks": "bin/musubi-tasks.js",
18
- "musubi-trace": "bin/musubi-trace.js"
18
+ "musubi-trace": "bin/musubi-trace.js",
19
+ "musubi-change": "bin/musubi-change.js"
19
20
  },
20
21
  "scripts": {
21
22
  "test": "jest",
@@ -0,0 +1,476 @@
1
+ /**
2
+ * MUSUBI Change Manager
3
+ *
4
+ * Manages delta specifications for brownfield projects
5
+ * Implements ADDED/MODIFIED/REMOVED/RENAMED change tracking
6
+ */
7
+
8
+ const fs = require('fs-extra');
9
+ const path = require('path');
10
+ const glob = require('glob');
11
+
12
+ class ChangeManager {
13
+ constructor(workspaceRoot) {
14
+ this.workspaceRoot = workspaceRoot;
15
+ }
16
+
17
+ /**
18
+ * Initialize new change proposal
19
+ */
20
+ async initChange(changeId, options = {}) {
21
+ const changesDir = path.join(this.workspaceRoot, options.changesDir || 'storage/changes');
22
+ const changeFile = path.join(changesDir, `${changeId}.md`);
23
+
24
+ // Check if change already exists
25
+ if (await fs.pathExists(changeFile)) {
26
+ throw new Error(`Change ${changeId} already exists`);
27
+ }
28
+
29
+ // Create changes directory if it doesn't exist
30
+ await fs.mkdirp(changesDir);
31
+
32
+ // Load template
33
+ const template = await this.loadTemplate(options.template);
34
+
35
+ // Create change document
36
+ const content = this.renderTemplate(template, {
37
+ changeId,
38
+ title: options.title || 'New Change',
39
+ description: options.description || 'Description of the change',
40
+ date: new Date().toISOString().split('T')[0]
41
+ });
42
+
43
+ await fs.writeFile(changeFile, content, 'utf-8');
44
+
45
+ return {
46
+ file: changeFile,
47
+ changeId
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Apply change to codebase
53
+ */
54
+ async applyChange(changeId, options = {}) {
55
+ const changesDir = path.join(this.workspaceRoot, options.changesDir || 'storage/changes');
56
+ const changeFile = path.join(changesDir, `${changeId}.md`);
57
+
58
+ // Check if change exists
59
+ if (!await fs.pathExists(changeFile)) {
60
+ throw new Error(`Change ${changeId} not found`);
61
+ }
62
+
63
+ // Parse delta specification
64
+ const delta = await this.parseDelta(changeFile);
65
+
66
+ // Validate if not force
67
+ if (!options.force) {
68
+ const validation = await this.validateDelta(delta);
69
+ if (!validation.valid) {
70
+ throw new Error(`Validation failed: ${validation.errors.map(e => e.message).join(', ')}`);
71
+ }
72
+ }
73
+
74
+ const stats = {
75
+ added: 0,
76
+ modified: 0,
77
+ removed: 0,
78
+ renamed: 0
79
+ };
80
+
81
+ if (!options.dryRun) {
82
+ // Apply ADDED items
83
+ for (const item of delta.added) {
84
+ await this.applyAdded(item);
85
+ stats.added++;
86
+ }
87
+
88
+ // Apply MODIFIED items
89
+ for (const item of delta.modified) {
90
+ await this.applyModified(item);
91
+ stats.modified++;
92
+ }
93
+
94
+ // Apply REMOVED items
95
+ for (const item of delta.removed) {
96
+ await this.applyRemoved(item);
97
+ stats.removed++;
98
+ }
99
+
100
+ // Apply RENAMED items
101
+ for (const item of delta.renamed) {
102
+ await this.applyRenamed(item);
103
+ stats.renamed++;
104
+ }
105
+ } else {
106
+ // Dry run - just count
107
+ stats.added = delta.added.length;
108
+ stats.modified = delta.modified.length;
109
+ stats.removed = delta.removed.length;
110
+ stats.renamed = delta.renamed.length;
111
+ }
112
+
113
+ return { stats };
114
+ }
115
+
116
+ /**
117
+ * Archive completed change
118
+ */
119
+ async archiveChange(changeId, options = {}) {
120
+ const changesDir = path.join(this.workspaceRoot, options.changesDir || 'storage/changes');
121
+ const specsDir = path.join(this.workspaceRoot, options.specsDir || 'specs');
122
+ const sourceFile = path.join(changesDir, `${changeId}.md`);
123
+ const archiveFile = path.join(specsDir, 'changes', `${changeId}.md`);
124
+
125
+ // Check if change exists
126
+ if (!await fs.pathExists(sourceFile)) {
127
+ throw new Error(`Change ${changeId} not found`);
128
+ }
129
+
130
+ // Create specs/changes directory
131
+ await fs.mkdirp(path.dirname(archiveFile));
132
+
133
+ // Parse delta and merge to canonical specs
134
+ const delta = await this.parseDelta(sourceFile);
135
+ await this.mergeDeltaToSpecs(delta, specsDir);
136
+
137
+ // Move change file to archive
138
+ await fs.move(sourceFile, archiveFile, { overwrite: true });
139
+
140
+ return {
141
+ source: sourceFile,
142
+ archive: archiveFile
143
+ };
144
+ }
145
+
146
+ /**
147
+ * List all changes
148
+ */
149
+ async listChanges(options = {}) {
150
+ const changesDir = path.join(this.workspaceRoot, options.changesDir || 'storage/changes');
151
+
152
+ if (!await fs.pathExists(changesDir)) {
153
+ return [];
154
+ }
155
+
156
+ const files = glob.sync('*.md', { cwd: changesDir, absolute: true });
157
+ const changes = [];
158
+
159
+ for (const file of files) {
160
+ const content = await fs.readFile(file, 'utf-8');
161
+ const changeId = path.basename(file, '.md');
162
+
163
+ // Extract title and date
164
+ const titleMatch = content.match(/# (.+)/);
165
+ const dateMatch = content.match(/\*\*Date\*\*: (.+)/);
166
+
167
+ // Determine status (simplified - could be more sophisticated)
168
+ const status = 'pending'; // In real implementation, track applied/archived status
169
+
170
+ changes.push({
171
+ id: changeId,
172
+ title: titleMatch ? titleMatch[1] : changeId,
173
+ date: dateMatch ? dateMatch[1] : 'Unknown',
174
+ status,
175
+ file: path.relative(this.workspaceRoot, file)
176
+ });
177
+ }
178
+
179
+ // Filter by status if specified
180
+ if (options.status) {
181
+ return changes.filter(c => c.status === options.status);
182
+ }
183
+
184
+ // Sort by ID for consistent ordering
185
+ return changes.sort((a, b) => a.id.localeCompare(b.id));
186
+ }
187
+
188
+ /**
189
+ * Validate delta specification
190
+ */
191
+ async validateChange(changeId, options = {}) {
192
+ const changesDir = path.join(this.workspaceRoot, options.changesDir || 'storage/changes');
193
+ const changeFile = path.join(changesDir, `${changeId}.md`);
194
+
195
+ // Check if change exists
196
+ if (!await fs.pathExists(changeFile)) {
197
+ throw new Error(`Change ${changeId} not found`);
198
+ }
199
+
200
+ // Parse delta
201
+ const delta = await this.parseDelta(changeFile);
202
+
203
+ // Validate
204
+ const validation = await this.validateDelta(delta);
205
+
206
+ return {
207
+ valid: validation.valid,
208
+ errors: validation.errors,
209
+ stats: {
210
+ added: delta.added.length,
211
+ modified: delta.modified.length,
212
+ removed: delta.removed.length,
213
+ renamed: delta.renamed.length
214
+ }
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Load template
220
+ */
221
+ async loadTemplate(customTemplate) {
222
+ if (customTemplate && await fs.pathExists(customTemplate)) {
223
+ return fs.readFile(customTemplate, 'utf-8');
224
+ }
225
+
226
+ // Default template
227
+ return `# {{title}}
228
+
229
+ **Change ID**: {{changeId}}
230
+ **Date**: {{date}}
231
+ **Status**: Pending
232
+
233
+ ## Description
234
+
235
+ {{description}}
236
+
237
+ ## Requirements Changes
238
+
239
+ ### ADDED
240
+
241
+ <!-- List new requirements here -->
242
+
243
+ ### MODIFIED
244
+
245
+ <!-- List modified requirements here -->
246
+
247
+ ### REMOVED
248
+
249
+ <!-- List removed requirements here -->
250
+
251
+ ### RENAMED
252
+
253
+ <!-- List renamed requirements here -->
254
+
255
+ ## Design Changes
256
+
257
+ ### ADDED
258
+
259
+ <!-- List new design elements -->
260
+
261
+ ### MODIFIED
262
+
263
+ <!-- List modified design elements -->
264
+
265
+ ### REMOVED
266
+
267
+ <!-- List removed design elements -->
268
+
269
+ ## Code Changes
270
+
271
+ ### ADDED
272
+
273
+ <!-- List new files/modules -->
274
+
275
+ ### MODIFIED
276
+
277
+ <!-- List modified files/modules -->
278
+
279
+ ### REMOVED
280
+
281
+ <!-- List removed files/modules -->
282
+
283
+ ### RENAMED
284
+
285
+ <!-- List renamed files/modules -->
286
+
287
+ ## Impact Analysis
288
+
289
+ ### Affected Components
290
+
291
+ - [Component 1]
292
+ - [Component 2]
293
+
294
+ ### Breaking Changes
295
+
296
+ - [ ] No breaking changes
297
+ - [ ] Breaking changes (list below)
298
+
299
+ ### Migration Steps
300
+
301
+ 1. [Migration step 1]
302
+ 2. [Migration step 2]
303
+
304
+ ## Testing
305
+
306
+ ### Test Changes
307
+
308
+ - [ ] Unit tests updated
309
+ - [ ] Integration tests updated
310
+ - [ ] E2E tests updated
311
+
312
+ ### Test Coverage
313
+
314
+ - Current coverage: XX%
315
+ - Target coverage: XX%
316
+
317
+ ## Traceability
318
+
319
+ ### Requirements → Design → Code → Tests
320
+
321
+ - REQ-XXX-001 → Design-A → Module-B → Test-C
322
+
323
+ ## Approval
324
+
325
+ - [ ] Technical review complete
326
+ - [ ] Product review complete
327
+ - [ ] Security review complete (if needed)
328
+ - [ ] Ready to apply
329
+ `;
330
+ }
331
+
332
+ /**
333
+ * Render template with data
334
+ */
335
+ renderTemplate(template, data) {
336
+ let result = template;
337
+ for (const [key, value] of Object.entries(data)) {
338
+ result = result.replace(new RegExp(`{{${key}}}`, 'g'), value);
339
+ }
340
+ return result;
341
+ }
342
+
343
+ /**
344
+ * Parse delta specification
345
+ */
346
+ async parseDelta(deltaFile) {
347
+ const content = await fs.readFile(deltaFile, 'utf-8');
348
+
349
+ const delta = {
350
+ added: [],
351
+ modified: [],
352
+ removed: [],
353
+ renamed: []
354
+ };
355
+
356
+ // Simple parser - extract items under each section
357
+ const sections = ['ADDED', 'MODIFIED', 'REMOVED', 'RENAMED'];
358
+
359
+ for (const section of sections) {
360
+ const sectionRegex = new RegExp(`### ${section}\\s+([\\s\\S]*?)(?=###|$)`, 'g');
361
+ const matches = content.matchAll(sectionRegex);
362
+
363
+ for (const match of matches) {
364
+ const sectionContent = match[1];
365
+ const items = this.parseItems(sectionContent);
366
+ delta[section.toLowerCase()].push(...items);
367
+ }
368
+ }
369
+
370
+ return delta;
371
+ }
372
+
373
+ /**
374
+ * Parse items from section content
375
+ */
376
+ parseItems(content) {
377
+ const items = [];
378
+ const lines = content.split('\n');
379
+
380
+ for (const line of lines) {
381
+ const trimmed = line.trim();
382
+ // Skip empty lines and HTML comments
383
+ if (!trimmed || trimmed.startsWith('<!--') || trimmed.startsWith('##')) {
384
+ continue;
385
+ }
386
+
387
+ if (trimmed.startsWith('- REQ-')) {
388
+ // Extract requirement ID and title
389
+ const match = trimmed.match(/- (REQ-[A-Z0-9]+-\d{3}):?\s*(.+)?/);
390
+ if (match) {
391
+ items.push({
392
+ id: match[1],
393
+ title: match[2] || '',
394
+ line: trimmed
395
+ });
396
+ }
397
+ }
398
+ }
399
+
400
+ return items;
401
+ }
402
+
403
+ /**
404
+ * Validate delta specification
405
+ */
406
+ async validateDelta(delta) {
407
+ const errors = [];
408
+
409
+ // Check for valid REQ IDs
410
+ const allItems = [
411
+ ...delta.added,
412
+ ...delta.modified,
413
+ ...delta.removed,
414
+ ...delta.renamed
415
+ ];
416
+
417
+ for (const item of allItems) {
418
+ if (!item.id || !item.id.match(/^REQ-[A-Z0-9]+-\d{3}$/)) {
419
+ errors.push({
420
+ message: `Invalid requirement ID: ${item.id}`,
421
+ line: item.line
422
+ });
423
+ }
424
+ }
425
+
426
+ return {
427
+ valid: errors.length === 0,
428
+ errors
429
+ };
430
+ }
431
+
432
+ /**
433
+ * Apply ADDED item
434
+ */
435
+ async applyAdded(item) {
436
+ // In real implementation, this would create new requirement/design/code
437
+ // For now, just a placeholder
438
+ return item;
439
+ }
440
+
441
+ /**
442
+ * Apply MODIFIED item
443
+ */
444
+ async applyModified(item) {
445
+ // In real implementation, this would update existing requirement/design/code
446
+ return item;
447
+ }
448
+
449
+ /**
450
+ * Apply REMOVED item
451
+ */
452
+ async applyRemoved(item) {
453
+ // In real implementation, this would remove requirement/design/code
454
+ return item;
455
+ }
456
+
457
+ /**
458
+ * Apply RENAMED item
459
+ */
460
+ async applyRenamed(item) {
461
+ // In real implementation, this would rename requirement/design/code
462
+ return item;
463
+ }
464
+
465
+ /**
466
+ * Merge delta to canonical specs
467
+ */
468
+ async mergeDeltaToSpecs(delta, specsDir) {
469
+ // In real implementation, this would merge changes to canonical specs
470
+ // For now, just ensure specs directory exists
471
+ await fs.mkdirp(specsDir);
472
+ return delta;
473
+ }
474
+ }
475
+
476
+ module.exports = ChangeManager;