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 +10 -1
- package/README.md +10 -1
- package/bin/musubi-change.js +250 -0
- package/bin/musubi-design.js +0 -0
- package/bin/musubi-tasks.js +0 -0
- package/bin/musubi-trace.js +0 -0
- package/package.json +3 -2
- package/src/managers/change.js +476 -0
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
|
-
- 🔄 **
|
|
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();
|
package/bin/musubi-design.js
CHANGED
|
File without changes
|
package/bin/musubi-tasks.js
CHANGED
|
File without changes
|
package/bin/musubi-trace.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musubi-sdd",
|
|
3
|
-
"version": "0.8.
|
|
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;
|