musubi-sdd 6.2.0 → 6.2.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.
@@ -0,0 +1,434 @@
1
+ /**
2
+ * WorkflowDashboard Implementation
3
+ *
4
+ * Manages workflow state and progress visualization.
5
+ *
6
+ * Requirement: IMP-6.2-003-01
7
+ * Design: Section 4.1
8
+ */
9
+
10
+ const fs = require('fs').promises;
11
+ const path = require('path');
12
+
13
+ /**
14
+ * Valid workflow stages
15
+ */
16
+ const WORKFLOW_STAGES = [
17
+ 'steering',
18
+ 'requirements',
19
+ 'design',
20
+ 'implementation',
21
+ 'validation'
22
+ ];
23
+
24
+ /**
25
+ * Stage statuses
26
+ */
27
+ const STAGE_STATUS = {
28
+ NOT_STARTED: 'not-started',
29
+ IN_PROGRESS: 'in-progress',
30
+ COMPLETED: 'completed',
31
+ BLOCKED: 'blocked'
32
+ };
33
+
34
+ /**
35
+ * Default configuration
36
+ */
37
+ const DEFAULT_CONFIG = {
38
+ storageDir: 'storage/workflows'
39
+ };
40
+
41
+ /**
42
+ * WorkflowDashboard
43
+ *
44
+ * Manages workflow state and progress for features.
45
+ */
46
+ class WorkflowDashboard {
47
+ /**
48
+ * @param {Object} config - Configuration options
49
+ */
50
+ constructor(config = {}) {
51
+ this.config = { ...DEFAULT_CONFIG, ...config };
52
+ this.workflows = new Map();
53
+ }
54
+
55
+ /**
56
+ * Create a new workflow
57
+ * @param {string} featureId - Feature ID
58
+ * @param {Object} options - Workflow options
59
+ * @returns {Promise<Object>} Created workflow state
60
+ */
61
+ async createWorkflow(featureId, options = {}) {
62
+ const stages = {};
63
+
64
+ for (const stage of WORKFLOW_STAGES) {
65
+ stages[stage] = {
66
+ status: STAGE_STATUS.NOT_STARTED,
67
+ startedAt: null,
68
+ completedAt: null,
69
+ artifacts: []
70
+ };
71
+ }
72
+
73
+ // Set first stage as in-progress
74
+ stages['steering'].status = STAGE_STATUS.IN_PROGRESS;
75
+ stages['steering'].startedAt = new Date().toISOString();
76
+
77
+ const workflow = {
78
+ featureId,
79
+ title: options.title || featureId,
80
+ description: options.description || '',
81
+ createdAt: new Date().toISOString(),
82
+ updatedAt: new Date().toISOString(),
83
+ currentStage: 'steering',
84
+ stages,
85
+ blockers: [],
86
+ metadata: options.metadata || {}
87
+ };
88
+
89
+ this.workflows.set(featureId, workflow);
90
+ await this.saveWorkflow(workflow);
91
+
92
+ return workflow;
93
+ }
94
+
95
+ /**
96
+ * Get workflow by feature ID
97
+ * @param {string} featureId - Feature ID
98
+ * @returns {Promise<Object|null>} Workflow state
99
+ */
100
+ async getWorkflow(featureId) {
101
+ if (this.workflows.has(featureId)) {
102
+ return this.workflows.get(featureId);
103
+ }
104
+
105
+ return await this.loadWorkflow(featureId);
106
+ }
107
+
108
+ /**
109
+ * Update stage status
110
+ * @param {string} featureId - Feature ID
111
+ * @param {string} stage - Stage name
112
+ * @param {string} status - New status
113
+ * @param {Object} options - Update options
114
+ * @returns {Promise<Object>} Updated workflow
115
+ */
116
+ async updateStage(featureId, stage, status, options = {}) {
117
+ const workflow = await this.getWorkflow(featureId);
118
+ if (!workflow) {
119
+ throw new Error(`Workflow not found: ${featureId}`);
120
+ }
121
+
122
+ if (!WORKFLOW_STAGES.includes(stage)) {
123
+ throw new Error(`Invalid stage: ${stage}`);
124
+ }
125
+
126
+ const stageData = workflow.stages[stage];
127
+ stageData.status = status;
128
+
129
+ if (status === STAGE_STATUS.IN_PROGRESS && !stageData.startedAt) {
130
+ stageData.startedAt = new Date().toISOString();
131
+ }
132
+
133
+ if (status === STAGE_STATUS.COMPLETED) {
134
+ stageData.completedAt = new Date().toISOString();
135
+ }
136
+
137
+ if (options.artifacts) {
138
+ stageData.artifacts.push(...options.artifacts);
139
+ }
140
+
141
+ workflow.currentStage = this.calculateCurrentStage(workflow);
142
+ workflow.updatedAt = new Date().toISOString();
143
+
144
+ await this.saveWorkflow(workflow);
145
+
146
+ return workflow;
147
+ }
148
+
149
+ /**
150
+ * Add blocker to workflow
151
+ * @param {string} featureId - Feature ID
152
+ * @param {Object} blocker - Blocker information
153
+ * @returns {Promise<Object>} Updated workflow
154
+ */
155
+ async addBlocker(featureId, blocker) {
156
+ const workflow = await this.getWorkflow(featureId);
157
+ if (!workflow) {
158
+ throw new Error(`Workflow not found: ${featureId}`);
159
+ }
160
+
161
+ const blockerEntry = {
162
+ id: `BLK-${Date.now()}`,
163
+ stage: blocker.stage,
164
+ description: blocker.description,
165
+ severity: blocker.severity || 'medium',
166
+ createdAt: new Date().toISOString(),
167
+ resolvedAt: null,
168
+ resolution: null
169
+ };
170
+
171
+ workflow.blockers.push(blockerEntry);
172
+
173
+ // Mark stage as blocked
174
+ if (blocker.stage && workflow.stages[blocker.stage]) {
175
+ workflow.stages[blocker.stage].status = STAGE_STATUS.BLOCKED;
176
+ }
177
+
178
+ workflow.updatedAt = new Date().toISOString();
179
+ await this.saveWorkflow(workflow);
180
+
181
+ return workflow;
182
+ }
183
+
184
+ /**
185
+ * Resolve blocker
186
+ * @param {string} featureId - Feature ID
187
+ * @param {string} blockerId - Blocker ID
188
+ * @param {string} resolution - Resolution description
189
+ * @returns {Promise<Object>} Updated workflow
190
+ */
191
+ async resolveBlocker(featureId, blockerId, resolution) {
192
+ const workflow = await this.getWorkflow(featureId);
193
+ if (!workflow) {
194
+ throw new Error(`Workflow not found: ${featureId}`);
195
+ }
196
+
197
+ const blocker = workflow.blockers.find(b => b.id === blockerId);
198
+ if (!blocker) {
199
+ throw new Error(`Blocker not found: ${blockerId}`);
200
+ }
201
+
202
+ blocker.resolvedAt = new Date().toISOString();
203
+ blocker.resolution = resolution;
204
+
205
+ // Check if stage can be unblocked
206
+ const stageBlockers = workflow.blockers.filter(
207
+ b => b.stage === blocker.stage && !b.resolvedAt
208
+ );
209
+
210
+ if (stageBlockers.length === 0 && workflow.stages[blocker.stage]) {
211
+ workflow.stages[blocker.stage].status = STAGE_STATUS.IN_PROGRESS;
212
+ }
213
+
214
+ workflow.updatedAt = new Date().toISOString();
215
+ await this.saveWorkflow(workflow);
216
+
217
+ return workflow;
218
+ }
219
+
220
+ /**
221
+ * Suggest next actions based on current state
222
+ * @param {string} featureId - Feature ID
223
+ * @returns {Promise<Array>} Suggested next actions
224
+ */
225
+ async suggestNextActions(featureId) {
226
+ const workflow = await this.getWorkflow(featureId);
227
+ if (!workflow) {
228
+ throw new Error(`Workflow not found: ${featureId}`);
229
+ }
230
+
231
+ const actions = [];
232
+ const currentStage = workflow.currentStage;
233
+ const stageData = workflow.stages[currentStage];
234
+
235
+ // Check for blockers
236
+ const unresolvedBlockers = workflow.blockers.filter(b => !b.resolvedAt);
237
+ if (unresolvedBlockers.length > 0) {
238
+ actions.push({
239
+ type: 'resolve-blocker',
240
+ priority: 'high',
241
+ description: `${unresolvedBlockers.length}件のブロッカーを解決してください`,
242
+ blockers: unresolvedBlockers
243
+ });
244
+ }
245
+
246
+ // Stage-specific suggestions
247
+ switch (currentStage) {
248
+ case 'steering':
249
+ actions.push({
250
+ type: 'create-artifact',
251
+ priority: 'medium',
252
+ description: 'プロジェクトメモリファイルを作成してください',
253
+ artifacts: ['structure.md', 'tech.md', 'product.md']
254
+ });
255
+ break;
256
+ case 'requirements':
257
+ actions.push({
258
+ type: 'create-artifact',
259
+ priority: 'medium',
260
+ description: 'EARS形式の要件ドキュメントを作成してください',
261
+ artifacts: ['requirements.md']
262
+ });
263
+ break;
264
+ case 'design':
265
+ actions.push({
266
+ type: 'create-artifact',
267
+ priority: 'medium',
268
+ description: 'C4設計ドキュメントとADRを作成してください',
269
+ artifacts: ['design.md', 'adr-*.md']
270
+ });
271
+ break;
272
+ case 'implementation':
273
+ actions.push({
274
+ type: 'run-review',
275
+ priority: 'medium',
276
+ description: 'レビューゲートを実行して実装を検証してください'
277
+ });
278
+ break;
279
+ case 'validation':
280
+ actions.push({
281
+ type: 'complete-validation',
282
+ priority: 'medium',
283
+ description: '全てのテストが通過していることを確認してください'
284
+ });
285
+ break;
286
+ }
287
+
288
+ return actions;
289
+ }
290
+
291
+ /**
292
+ * Calculate overall completion percentage
293
+ * @param {string} featureId - Feature ID
294
+ * @returns {Promise<number>} Completion percentage
295
+ */
296
+ async calculateCompletion(featureId) {
297
+ const workflow = await this.getWorkflow(featureId);
298
+ if (!workflow) {
299
+ throw new Error(`Workflow not found: ${featureId}`);
300
+ }
301
+
302
+ const totalStages = WORKFLOW_STAGES.length;
303
+ let completed = 0;
304
+
305
+ for (const stage of WORKFLOW_STAGES) {
306
+ if (workflow.stages[stage].status === STAGE_STATUS.COMPLETED) {
307
+ completed++;
308
+ }
309
+ }
310
+
311
+ return Math.round((completed / totalStages) * 100);
312
+ }
313
+
314
+ /**
315
+ * Get workflow summary
316
+ * @param {string} featureId - Feature ID
317
+ * @returns {Promise<Object>} Workflow summary
318
+ */
319
+ async getSummary(featureId) {
320
+ const workflow = await this.getWorkflow(featureId);
321
+ if (!workflow) {
322
+ throw new Error(`Workflow not found: ${featureId}`);
323
+ }
324
+
325
+ const completion = await this.calculateCompletion(featureId);
326
+ const unresolvedBlockers = workflow.blockers.filter(b => !b.resolvedAt);
327
+ const actions = await this.suggestNextActions(featureId);
328
+
329
+ return {
330
+ featureId,
331
+ title: workflow.title,
332
+ currentStage: workflow.currentStage,
333
+ completion,
334
+ blockerCount: unresolvedBlockers.length,
335
+ nextAction: actions[0] || null,
336
+ stages: Object.entries(workflow.stages).map(([name, data]) => ({
337
+ name,
338
+ status: data.status,
339
+ artifactCount: data.artifacts.length
340
+ }))
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Calculate current stage based on statuses
346
+ * @param {Object} workflow - Workflow object
347
+ * @returns {string} Current stage name
348
+ */
349
+ calculateCurrentStage(workflow) {
350
+ for (const stage of WORKFLOW_STAGES) {
351
+ const stageData = workflow.stages[stage];
352
+ if (stageData.status !== STAGE_STATUS.COMPLETED) {
353
+ return stage;
354
+ }
355
+ }
356
+ return 'validation';
357
+ }
358
+
359
+ /**
360
+ * Save workflow to storage
361
+ * @param {Object} workflow - Workflow to save
362
+ */
363
+ async saveWorkflow(workflow) {
364
+ await this.ensureStorageDir();
365
+
366
+ const filePath = path.join(
367
+ this.config.storageDir,
368
+ `${workflow.featureId}.json`
369
+ );
370
+
371
+ await fs.writeFile(filePath, JSON.stringify(workflow, null, 2), 'utf-8');
372
+ this.workflows.set(workflow.featureId, workflow);
373
+ }
374
+
375
+ /**
376
+ * Load workflow from storage
377
+ * @param {string} featureId - Feature ID
378
+ * @returns {Promise<Object|null>} Workflow
379
+ */
380
+ async loadWorkflow(featureId) {
381
+ try {
382
+ const filePath = path.join(this.config.storageDir, `${featureId}.json`);
383
+ const content = await fs.readFile(filePath, 'utf-8');
384
+ const workflow = JSON.parse(content);
385
+ this.workflows.set(featureId, workflow);
386
+ return workflow;
387
+ } catch {
388
+ return null;
389
+ }
390
+ }
391
+
392
+ /**
393
+ * List all workflows
394
+ * @returns {Promise<Array>} List of workflows
395
+ */
396
+ async listWorkflows() {
397
+ try {
398
+ await this.ensureStorageDir();
399
+ const files = await fs.readdir(this.config.storageDir);
400
+ const workflows = [];
401
+
402
+ for (const file of files) {
403
+ if (file.endsWith('.json')) {
404
+ const featureId = file.replace('.json', '');
405
+ const workflow = await this.getWorkflow(featureId);
406
+ if (workflow) {
407
+ workflows.push(workflow);
408
+ }
409
+ }
410
+ }
411
+
412
+ return workflows;
413
+ } catch {
414
+ return [];
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Ensure storage directory exists
420
+ */
421
+ async ensureStorageDir() {
422
+ try {
423
+ await fs.access(this.config.storageDir);
424
+ } catch {
425
+ await fs.mkdir(this.config.storageDir, { recursive: true });
426
+ }
427
+ }
428
+ }
429
+
430
+ module.exports = {
431
+ WorkflowDashboard,
432
+ WORKFLOW_STAGES,
433
+ STAGE_STATUS
434
+ };