musubi-sdd 6.1.2 → 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,536 @@
1
+ /**
2
+ * Dashboard CLI
3
+ *
4
+ * Command-line interface for dashboard operations.
5
+ *
6
+ * Requirement: IMP-6.2-003-05
7
+ * Design: Section 4.5
8
+ */
9
+
10
+ const { WorkflowDashboard } = require('../dashboard/workflow-dashboard');
11
+ const { TransitionRecorder } = require('../dashboard/transition-recorder');
12
+ const { SprintPlanner, PRIORITY } = require('../dashboard/sprint-planner');
13
+ const { SprintReporter } = require('../dashboard/sprint-reporter');
14
+ const { TraceabilityExtractor } = require('../traceability/extractor');
15
+ const { GapDetector } = require('../traceability/gap-detector');
16
+ const { MatrixStorage } = require('../traceability/matrix-storage');
17
+
18
+ /**
19
+ * DashboardCLI
20
+ *
21
+ * Provides CLI commands for dashboard operations.
22
+ */
23
+ class DashboardCLI {
24
+ /**
25
+ * @param {Object} config - Configuration options
26
+ */
27
+ constructor(config = {}) {
28
+ this.config = config;
29
+ this.dashboard = new WorkflowDashboard(config.dashboardConfig);
30
+ this.recorder = new TransitionRecorder(config.recorderConfig);
31
+ this.planner = new SprintPlanner(config.plannerConfig);
32
+ this.reporter = new SprintReporter(config.reporterConfig);
33
+ this.extractor = new TraceabilityExtractor(config.extractorConfig);
34
+ this.gapDetector = new GapDetector(config.gapDetectorConfig);
35
+ this.matrixStorage = new MatrixStorage(config.matrixConfig);
36
+ }
37
+
38
+ /**
39
+ * Execute CLI command
40
+ * @param {string} command - Command name
41
+ * @param {Array} args - Command arguments
42
+ * @param {Object} options - Command options
43
+ * @returns {Promise<Object>} Command result
44
+ */
45
+ async execute(command, args = [], options = {}) {
46
+ const commands = {
47
+ // Workflow commands
48
+ 'workflow:create': () => this.createWorkflow(args[0], options),
49
+ 'workflow:status': () => this.getWorkflowStatus(args[0]),
50
+ 'workflow:advance': () => this.advanceWorkflow(args[0], options),
51
+ 'workflow:list': () => this.listWorkflows(),
52
+
53
+ // Sprint commands
54
+ 'sprint:create': () => this.createSprint(options),
55
+ 'sprint:start': () => this.startSprint(args[0]),
56
+ 'sprint:complete': () => this.completeSprint(args[0]),
57
+ 'sprint:status': () => this.getSprintStatus(args[0]),
58
+ 'sprint:add-task': () => this.addSprintTask(args[0], options),
59
+ 'sprint:report': () => this.generateSprintReport(args[0]),
60
+
61
+ // Traceability commands
62
+ 'trace:scan': () => this.scanTraceability(args[0], options),
63
+ 'trace:gaps': () => this.detectGaps(args[0]),
64
+ 'trace:matrix': () => this.showMatrix(args[0]),
65
+ 'trace:save': () => this.saveMatrix(args[0], options),
66
+
67
+ // Summary commands
68
+ 'summary': () => this.getSummary(args[0]),
69
+ 'help': () => this.showHelp()
70
+ };
71
+
72
+ const handler = commands[command];
73
+ if (!handler) {
74
+ throw new Error(`Unknown command: ${command}. Use 'help' to see available commands.`);
75
+ }
76
+
77
+ return await handler();
78
+ }
79
+
80
+ /**
81
+ * Create a new workflow
82
+ * @param {string} featureId - Feature ID
83
+ * @param {Object} options - Options
84
+ * @returns {Promise<Object>} Created workflow
85
+ */
86
+ async createWorkflow(featureId, options = {}) {
87
+ if (!featureId) {
88
+ throw new Error('Feature ID is required');
89
+ }
90
+
91
+ const workflow = await this.dashboard.createWorkflow(featureId, {
92
+ title: options.name || featureId,
93
+ description: options.description || ''
94
+ });
95
+
96
+ return {
97
+ success: true,
98
+ message: `Workflow created: ${workflow.featureId}`,
99
+ workflow
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Get workflow status
105
+ * @param {string} workflowId - Workflow ID
106
+ * @returns {Promise<Object>} Workflow status
107
+ */
108
+ async getWorkflowStatus(workflowId) {
109
+ if (!workflowId) {
110
+ throw new Error('Workflow ID is required');
111
+ }
112
+
113
+ const workflow = await this.dashboard.getWorkflow(workflowId);
114
+ if (!workflow) {
115
+ throw new Error(`Workflow not found: ${workflowId}`);
116
+ }
117
+
118
+ const summary = await this.dashboard.getSummary(workflowId);
119
+
120
+ return {
121
+ success: true,
122
+ workflow,
123
+ summary
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Advance workflow to next stage
129
+ * @param {string} workflowId - Workflow ID
130
+ * @param {Object} options - Options
131
+ * @returns {Promise<Object>} Updated workflow
132
+ */
133
+ async advanceWorkflow(workflowId, options = {}) {
134
+ if (!workflowId) {
135
+ throw new Error('Workflow ID is required');
136
+ }
137
+
138
+ const workflow = await this.dashboard.getWorkflow(workflowId);
139
+ if (!workflow) {
140
+ throw new Error(`Workflow not found: ${workflowId}`);
141
+ }
142
+
143
+ const nextStage = options.toStage || this.getNextStage(workflow.currentStage);
144
+
145
+ // Record transition
146
+ await this.recorder.recordTransition(workflowId, {
147
+ fromStage: workflow.currentStage,
148
+ toStage: nextStage,
149
+ reviewer: options.reviewer,
150
+ status: 'approved'
151
+ });
152
+
153
+ // Complete current stage
154
+ await this.dashboard.updateStage(
155
+ workflowId,
156
+ workflow.currentStage,
157
+ 'completed'
158
+ );
159
+
160
+ // Start next stage
161
+ const updated = await this.dashboard.updateStage(
162
+ workflowId,
163
+ nextStage,
164
+ 'in-progress'
165
+ );
166
+
167
+ return {
168
+ success: true,
169
+ message: `Workflow advanced to ${updated.currentStage}`,
170
+ workflow: updated
171
+ };
172
+ }
173
+
174
+ /**
175
+ * List all workflows
176
+ * @returns {Promise<Object>} Workflow list
177
+ */
178
+ async listWorkflows() {
179
+ const workflows = await this.dashboard.listWorkflows();
180
+ const workflowSummaries = [];
181
+
182
+ for (const w of workflows) {
183
+ const completion = await this.dashboard.calculateCompletion(w.featureId);
184
+ workflowSummaries.push({
185
+ id: w.featureId,
186
+ name: w.title,
187
+ currentStage: w.currentStage,
188
+ status: w.status || 'active',
189
+ completion
190
+ });
191
+ }
192
+
193
+ return {
194
+ success: true,
195
+ count: workflows.length,
196
+ workflows: workflowSummaries
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Create a new sprint
202
+ * @param {Object} options - Sprint options
203
+ * @returns {Promise<Object>} Created sprint
204
+ */
205
+ async createSprint(options = {}) {
206
+ const sprint = await this.planner.createSprint({
207
+ sprintId: options.id,
208
+ name: options.name,
209
+ featureId: options.featureId,
210
+ goal: options.goal,
211
+ velocity: options.velocity ? parseInt(options.velocity) : undefined
212
+ });
213
+
214
+ return {
215
+ success: true,
216
+ message: `Sprint created: ${sprint.id}`,
217
+ sprint
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Start a sprint
223
+ * @param {string} sprintId - Sprint ID
224
+ * @returns {Promise<Object>} Started sprint
225
+ */
226
+ async startSprint(sprintId) {
227
+ if (!sprintId) {
228
+ throw new Error('Sprint ID is required');
229
+ }
230
+
231
+ const sprint = await this.planner.startSprint(sprintId);
232
+
233
+ return {
234
+ success: true,
235
+ message: `Sprint started: ${sprint.id}`,
236
+ sprint
237
+ };
238
+ }
239
+
240
+ /**
241
+ * Complete a sprint
242
+ * @param {string} sprintId - Sprint ID
243
+ * @returns {Promise<Object>} Completed sprint
244
+ */
245
+ async completeSprint(sprintId) {
246
+ if (!sprintId) {
247
+ throw new Error('Sprint ID is required');
248
+ }
249
+
250
+ const sprint = await this.planner.completeSprint(sprintId);
251
+
252
+ return {
253
+ success: true,
254
+ message: `Sprint completed: ${sprint.id}`,
255
+ sprint
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Get sprint status
261
+ * @param {string} sprintId - Sprint ID
262
+ * @returns {Promise<Object>} Sprint status
263
+ */
264
+ async getSprintStatus(sprintId) {
265
+ if (!sprintId) {
266
+ throw new Error('Sprint ID is required');
267
+ }
268
+
269
+ const sprint = await this.planner.getSprint(sprintId);
270
+ if (!sprint) {
271
+ throw new Error(`Sprint not found: ${sprintId}`);
272
+ }
273
+
274
+ const metrics = await this.planner.getMetrics(sprintId);
275
+
276
+ return {
277
+ success: true,
278
+ sprint,
279
+ metrics
280
+ };
281
+ }
282
+
283
+ /**
284
+ * Add task to sprint
285
+ * @param {string} sprintId - Sprint ID
286
+ * @param {Object} options - Task options
287
+ * @returns {Promise<Object>} Updated sprint
288
+ */
289
+ async addSprintTask(sprintId, options = {}) {
290
+ if (!sprintId) {
291
+ throw new Error('Sprint ID is required');
292
+ }
293
+
294
+ if (!options.title) {
295
+ throw new Error('Task title is required');
296
+ }
297
+
298
+ const sprint = await this.planner.addTasks(sprintId, [{
299
+ id: options.taskId,
300
+ title: options.title,
301
+ description: options.description,
302
+ requirementId: options.requirementId,
303
+ storyPoints: options.points ? parseInt(options.points) : 1,
304
+ priority: options.priority || PRIORITY.MEDIUM
305
+ }]);
306
+
307
+ return {
308
+ success: true,
309
+ message: 'Task added to sprint',
310
+ sprint
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Generate sprint report
316
+ * @param {string} sprintId - Sprint ID
317
+ * @returns {Promise<Object>} Sprint report
318
+ */
319
+ async generateSprintReport(sprintId) {
320
+ if (!sprintId) {
321
+ throw new Error('Sprint ID is required');
322
+ }
323
+
324
+ const sprint = await this.planner.getSprint(sprintId);
325
+ if (!sprint) {
326
+ throw new Error(`Sprint not found: ${sprintId}`);
327
+ }
328
+
329
+ const markdown = await this.reporter.generateMarkdownReport(sprint);
330
+
331
+ return {
332
+ success: true,
333
+ message: 'Report generated',
334
+ report: markdown
335
+ };
336
+ }
337
+
338
+ /**
339
+ * Scan for traceability
340
+ * @param {string} directory - Directory to scan
341
+ * @param {Object} options - Scan options
342
+ * @returns {Promise<Object>} Scan results
343
+ */
344
+ async scanTraceability(directory = '.', options = {}) {
345
+ const artifacts = await this.extractor.scanDirectory(directory, {
346
+ extensions: options.extensions ? options.extensions.split(',') : undefined
347
+ });
348
+
349
+ return {
350
+ success: true,
351
+ directory,
352
+ artifacts: artifacts.length,
353
+ requirements: new Set(artifacts.map(a => a.requirementId)).size,
354
+ results: artifacts
355
+ };
356
+ }
357
+
358
+ /**
359
+ * Detect traceability gaps
360
+ * @param {string} directory - Directory to analyze
361
+ * @returns {Promise<Object>} Gap detection results
362
+ */
363
+ async detectGaps(directory = '.') {
364
+ // Scan artifacts
365
+ const codeArtifacts = await this.extractor.scanDirectory(directory, {
366
+ extensions: ['.js', '.ts']
367
+ });
368
+
369
+ const testArtifacts = await this.extractor.scanDirectory(directory, {
370
+ extensions: ['.test.js', '.spec.js', '.test.ts', '.spec.ts']
371
+ });
372
+
373
+ // Build matrix in format expected by GapDetector
374
+ const matrix = {};
375
+ const allRequirements = new Set();
376
+
377
+ for (const artifact of codeArtifacts) {
378
+ allRequirements.add(artifact.requirementId);
379
+ if (!matrix[artifact.requirementId]) {
380
+ matrix[artifact.requirementId] = {
381
+ requirementId: artifact.requirementId,
382
+ code: [],
383
+ tests: [],
384
+ design: [],
385
+ commits: []
386
+ };
387
+ }
388
+ matrix[artifact.requirementId].code.push(artifact);
389
+ }
390
+
391
+ for (const artifact of testArtifacts) {
392
+ allRequirements.add(artifact.requirementId);
393
+ if (!matrix[artifact.requirementId]) {
394
+ matrix[artifact.requirementId] = {
395
+ requirementId: artifact.requirementId,
396
+ code: [],
397
+ tests: [],
398
+ design: [],
399
+ commits: []
400
+ };
401
+ }
402
+ matrix[artifact.requirementId].tests.push(artifact);
403
+ }
404
+
405
+ // Detect gaps using getGapReport which calls detectGaps internally
406
+ const links = Object.values(matrix);
407
+ const report = this.gapDetector.getGapReport(links);
408
+
409
+ return {
410
+ success: true,
411
+ requirements: allRequirements.size,
412
+ gaps: report.gaps.length,
413
+ report
414
+ };
415
+ }
416
+
417
+ /**
418
+ * Show traceability matrix
419
+ * @param {string} matrixId - Matrix ID to load
420
+ * @returns {Promise<Object>} Matrix data
421
+ */
422
+ async showMatrix(matrixId) {
423
+ if (matrixId) {
424
+ const matrix = await this.matrixStorage.load(matrixId);
425
+ if (!matrix) {
426
+ throw new Error(`Matrix not found: ${matrixId}`);
427
+ }
428
+ return {
429
+ success: true,
430
+ matrix
431
+ };
432
+ }
433
+
434
+ const matrices = await this.matrixStorage.list();
435
+ return {
436
+ success: true,
437
+ matrices
438
+ };
439
+ }
440
+
441
+ /**
442
+ * Save traceability matrix
443
+ * @param {string} directory - Directory to scan
444
+ * @param {Object} options - Save options
445
+ * @returns {Promise<Object>} Save result
446
+ */
447
+ async saveMatrix(directory = '.', options = {}) {
448
+ const codeArtifacts = await this.extractor.scanDirectory(directory);
449
+
450
+ const entries = this.extractor.groupByRequirement(codeArtifacts);
451
+
452
+ const featureId = options.id || `MATRIX-${Date.now()}`;
453
+ const matrix = {
454
+ name: options.name || 'Traceability Matrix',
455
+ entries,
456
+ createdAt: new Date().toISOString()
457
+ };
458
+
459
+ const filePath = await this.matrixStorage.save(featureId, matrix);
460
+
461
+ return {
462
+ success: true,
463
+ message: `Matrix saved: ${featureId}`,
464
+ path: filePath
465
+ };
466
+ }
467
+
468
+ /**
469
+ * Get overall summary
470
+ * @param {string} featureId - Feature ID
471
+ * @returns {Promise<Object>} Summary
472
+ */
473
+ async getSummary(featureId) {
474
+ const workflows = await this.dashboard.listWorkflows();
475
+ const filteredWorkflows = featureId
476
+ ? workflows.filter(w => w.featureId === featureId)
477
+ : workflows;
478
+
479
+ const summary = {
480
+ workflows: {
481
+ total: filteredWorkflows.length,
482
+ active: filteredWorkflows.filter(w => w.status === 'active').length,
483
+ completed: filteredWorkflows.filter(w => w.status === 'completed').length
484
+ }
485
+ };
486
+
487
+ return {
488
+ success: true,
489
+ summary
490
+ };
491
+ }
492
+
493
+ /**
494
+ * Show help
495
+ * @returns {Object} Help information
496
+ */
497
+ showHelp() {
498
+ return {
499
+ success: true,
500
+ commands: {
501
+ 'workflow:create <featureId>': 'Create a new workflow',
502
+ 'workflow:status <workflowId>': 'Get workflow status',
503
+ 'workflow:advance <workflowId>': 'Advance workflow to next stage',
504
+ 'workflow:list': 'List all workflows',
505
+ 'sprint:create': 'Create a new sprint',
506
+ 'sprint:start <sprintId>': 'Start a sprint',
507
+ 'sprint:complete <sprintId>': 'Complete a sprint',
508
+ 'sprint:status <sprintId>': 'Get sprint status',
509
+ 'sprint:add-task <sprintId>': 'Add task to sprint',
510
+ 'sprint:report <sprintId>': 'Generate sprint report',
511
+ 'trace:scan [directory]': 'Scan for traceability',
512
+ 'trace:gaps [directory]': 'Detect traceability gaps',
513
+ 'trace:matrix [matrixId]': 'Show traceability matrix',
514
+ 'trace:save [directory]': 'Save traceability matrix',
515
+ 'summary [featureId]': 'Get overall summary',
516
+ 'help': 'Show this help'
517
+ }
518
+ };
519
+ }
520
+
521
+ /**
522
+ * Get next stage in workflow
523
+ * @param {string} currentStage - Current stage
524
+ * @returns {string} Next stage
525
+ */
526
+ getNextStage(currentStage) {
527
+ const stageOrder = ['steering', 'requirements', 'design', 'implementation', 'validation'];
528
+ const currentIndex = stageOrder.indexOf(currentStage);
529
+ if (currentIndex === -1 || currentIndex === stageOrder.length - 1) {
530
+ return currentStage;
531
+ }
532
+ return stageOrder[currentIndex + 1];
533
+ }
534
+ }
535
+
536
+ module.exports = { DashboardCLI };