musubi-sdd 6.2.0 → 6.2.2
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 +60 -1
- package/README.md +60 -1
- package/bin/musubi-dashboard.js +340 -0
- package/bin/musubi-upgrade.js +395 -0
- package/bin/musubi.js +15 -0
- package/package.json +5 -3
- package/src/cli/dashboard-cli.js +536 -0
- package/src/constitutional/checker.js +633 -0
- package/src/constitutional/ci-reporter.js +338 -0
- package/src/constitutional/index.js +22 -0
- package/src/constitutional/phase-minus-one.js +404 -0
- package/src/constitutional/steering-sync.js +473 -0
- package/src/dashboard/index.js +20 -0
- package/src/dashboard/sprint-planner.js +361 -0
- package/src/dashboard/sprint-reporter.js +378 -0
- package/src/dashboard/transition-recorder.js +209 -0
- package/src/dashboard/workflow-dashboard.js +434 -0
- package/src/enterprise/error-recovery.js +524 -0
- package/src/enterprise/experiment-report.js +573 -0
- package/src/enterprise/index.js +57 -4
- package/src/enterprise/rollback-manager.js +584 -0
- package/src/enterprise/tech-article.js +509 -0
- package/src/traceability/extractor.js +294 -0
- package/src/traceability/gap-detector.js +230 -0
- package/src/traceability/index.js +15 -0
- package/src/traceability/matrix-storage.js +368 -0
|
@@ -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
|
+
};
|