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,361 @@
1
+ /**
2
+ * SprintPlanner Implementation
3
+ *
4
+ * Generates sprint planning templates based on tasks.
5
+ *
6
+ * Requirement: IMP-6.2-003-03
7
+ * Design: Section 4.3
8
+ */
9
+
10
+ const fs = require('fs').promises;
11
+ const path = require('path');
12
+
13
+ /**
14
+ * Default configuration
15
+ */
16
+ const DEFAULT_CONFIG = {
17
+ storageDir: 'storage/sprints',
18
+ defaultSprintDuration: 14, // days
19
+ defaultVelocity: 20 // story points
20
+ };
21
+
22
+ /**
23
+ * Task priority levels
24
+ */
25
+ const PRIORITY = {
26
+ CRITICAL: 'critical',
27
+ HIGH: 'high',
28
+ MEDIUM: 'medium',
29
+ LOW: 'low'
30
+ };
31
+
32
+ /**
33
+ * SprintPlanner
34
+ *
35
+ * Creates and manages sprint plans.
36
+ */
37
+ class SprintPlanner {
38
+ /**
39
+ * @param {Object} config - Configuration options
40
+ */
41
+ constructor(config = {}) {
42
+ this.config = { ...DEFAULT_CONFIG, ...config };
43
+ }
44
+
45
+ /**
46
+ * Create a new sprint plan
47
+ * @param {Object} options - Sprint options
48
+ * @returns {Promise<Object>} Created sprint plan
49
+ */
50
+ async createSprint(options) {
51
+ const sprintId = options.sprintId || `SPRINT-${Date.now()}`;
52
+
53
+ const sprint = {
54
+ id: sprintId,
55
+ name: options.name || `Sprint ${sprintId}`,
56
+ featureId: options.featureId,
57
+ goal: options.goal || '',
58
+ startDate: options.startDate || new Date().toISOString().slice(0, 10),
59
+ endDate: options.endDate || this.calculateEndDate(options.startDate),
60
+ duration: options.duration || this.config.defaultSprintDuration,
61
+ velocity: options.velocity || this.config.defaultVelocity,
62
+ tasks: [],
63
+ status: 'planning',
64
+ createdAt: new Date().toISOString(),
65
+ updatedAt: new Date().toISOString()
66
+ };
67
+
68
+ await this.saveSprint(sprint);
69
+
70
+ return sprint;
71
+ }
72
+
73
+ /**
74
+ * Add tasks to sprint
75
+ * @param {string} sprintId - Sprint ID
76
+ * @param {Array} tasks - Tasks to add
77
+ * @returns {Promise<Object>} Updated sprint
78
+ */
79
+ async addTasks(sprintId, tasks) {
80
+ const sprint = await this.loadSprint(sprintId);
81
+ if (!sprint) {
82
+ throw new Error(`Sprint not found: ${sprintId}`);
83
+ }
84
+
85
+ for (const task of tasks) {
86
+ const sprintTask = {
87
+ id: task.id || `T-${Date.now()}-${Math.random().toString(36).substr(2, 4)}`,
88
+ title: task.title,
89
+ description: task.description || '',
90
+ requirementId: task.requirementId || null,
91
+ storyPoints: task.storyPoints || 1,
92
+ priority: task.priority || PRIORITY.MEDIUM,
93
+ assignee: task.assignee || null,
94
+ status: 'todo',
95
+ dependencies: task.dependencies || [],
96
+ acceptanceCriteria: task.acceptanceCriteria || [],
97
+ addedAt: new Date().toISOString()
98
+ };
99
+
100
+ sprint.tasks.push(sprintTask);
101
+ }
102
+
103
+ sprint.updatedAt = new Date().toISOString();
104
+ await this.saveSprint(sprint);
105
+
106
+ return sprint;
107
+ }
108
+
109
+ /**
110
+ * Update task status
111
+ * @param {string} sprintId - Sprint ID
112
+ * @param {string} taskId - Task ID
113
+ * @param {string} status - New status
114
+ * @returns {Promise<Object>} Updated sprint
115
+ */
116
+ async updateTaskStatus(sprintId, taskId, status) {
117
+ const sprint = await this.loadSprint(sprintId);
118
+ if (!sprint) {
119
+ throw new Error(`Sprint not found: ${sprintId}`);
120
+ }
121
+
122
+ const task = sprint.tasks.find(t => t.id === taskId);
123
+ if (!task) {
124
+ throw new Error(`Task not found: ${taskId}`);
125
+ }
126
+
127
+ task.status = status;
128
+ if (status === 'done') {
129
+ task.completedAt = new Date().toISOString();
130
+ }
131
+
132
+ sprint.updatedAt = new Date().toISOString();
133
+ await this.saveSprint(sprint);
134
+
135
+ return sprint;
136
+ }
137
+
138
+ /**
139
+ * Start sprint
140
+ * @param {string} sprintId - Sprint ID
141
+ * @returns {Promise<Object>} Updated sprint
142
+ */
143
+ async startSprint(sprintId) {
144
+ const sprint = await this.loadSprint(sprintId);
145
+ if (!sprint) {
146
+ throw new Error(`Sprint not found: ${sprintId}`);
147
+ }
148
+
149
+ sprint.status = 'active';
150
+ sprint.startedAt = new Date().toISOString();
151
+ sprint.updatedAt = new Date().toISOString();
152
+
153
+ await this.saveSprint(sprint);
154
+
155
+ return sprint;
156
+ }
157
+
158
+ /**
159
+ * Complete sprint
160
+ * @param {string} sprintId - Sprint ID
161
+ * @returns {Promise<Object>} Updated sprint
162
+ */
163
+ async completeSprint(sprintId) {
164
+ const sprint = await this.loadSprint(sprintId);
165
+ if (!sprint) {
166
+ throw new Error(`Sprint not found: ${sprintId}`);
167
+ }
168
+
169
+ sprint.status = 'completed';
170
+ sprint.completedAt = new Date().toISOString();
171
+ sprint.updatedAt = new Date().toISOString();
172
+
173
+ await this.saveSprint(sprint);
174
+
175
+ return sprint;
176
+ }
177
+
178
+ /**
179
+ * Get sprint by ID
180
+ * @param {string} sprintId - Sprint ID
181
+ * @returns {Promise<Object|null>} Sprint
182
+ */
183
+ async getSprint(sprintId) {
184
+ return await this.loadSprint(sprintId);
185
+ }
186
+
187
+ /**
188
+ * Calculate sprint metrics
189
+ * @param {string} sprintId - Sprint ID
190
+ * @returns {Promise<Object>} Sprint metrics
191
+ */
192
+ async getMetrics(sprintId) {
193
+ const sprint = await this.loadSprint(sprintId);
194
+ if (!sprint) {
195
+ throw new Error(`Sprint not found: ${sprintId}`);
196
+ }
197
+
198
+ const tasks = sprint.tasks;
199
+ const totalPoints = tasks.reduce((sum, t) => sum + t.storyPoints, 0);
200
+ const completedPoints = tasks
201
+ .filter(t => t.status === 'done')
202
+ .reduce((sum, t) => sum + t.storyPoints, 0);
203
+ const inProgressPoints = tasks
204
+ .filter(t => t.status === 'in-progress')
205
+ .reduce((sum, t) => sum + t.storyPoints, 0);
206
+
207
+ const todoTasks = tasks.filter(t => t.status === 'todo').length;
208
+ const inProgressTasks = tasks.filter(t => t.status === 'in-progress').length;
209
+ const doneTasks = tasks.filter(t => t.status === 'done').length;
210
+
211
+ return {
212
+ sprintId,
213
+ totalTasks: tasks.length,
214
+ todoTasks,
215
+ inProgressTasks,
216
+ doneTasks,
217
+ totalPoints,
218
+ completedPoints,
219
+ inProgressPoints,
220
+ remainingPoints: totalPoints - completedPoints,
221
+ completionPercentage: totalPoints > 0
222
+ ? Math.round((completedPoints / totalPoints) * 100)
223
+ : 0,
224
+ velocity: sprint.velocity,
225
+ overCapacity: totalPoints > sprint.velocity
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Generate sprint backlog template
231
+ * @param {string} sprintId - Sprint ID
232
+ * @returns {Promise<string>} Markdown template
233
+ */
234
+ async generateBacklogTemplate(sprintId) {
235
+ const sprint = await this.loadSprint(sprintId);
236
+ if (!sprint) {
237
+ throw new Error(`Sprint not found: ${sprintId}`);
238
+ }
239
+
240
+ const metrics = await this.getMetrics(sprintId);
241
+ const lines = [];
242
+
243
+ lines.push(`# ${sprint.name}`);
244
+ lines.push('');
245
+ lines.push(`**Feature:** ${sprint.featureId || 'N/A'}`);
246
+ lines.push(`**Goal:** ${sprint.goal || 'N/A'}`);
247
+ lines.push(`**Period:** ${sprint.startDate} - ${sprint.endDate}`);
248
+ lines.push(`**Velocity:** ${sprint.velocity} points`);
249
+ lines.push('');
250
+
251
+ // Summary
252
+ lines.push('## Summary');
253
+ lines.push('');
254
+ lines.push('| Metric | Value |');
255
+ lines.push('|--------|-------|');
256
+ lines.push(`| Total Tasks | ${metrics.totalTasks} |`);
257
+ lines.push(`| Total Points | ${metrics.totalPoints} |`);
258
+ lines.push(`| Completed Points | ${metrics.completedPoints} |`);
259
+ lines.push(`| Completion | ${metrics.completionPercentage}% |`);
260
+ lines.push('');
261
+
262
+ // Tasks by priority
263
+ lines.push('## Tasks');
264
+ lines.push('');
265
+
266
+ const priorityOrder = ['critical', 'high', 'medium', 'low'];
267
+
268
+ for (const priority of priorityOrder) {
269
+ const priorityTasks = sprint.tasks.filter(t => t.priority === priority);
270
+ if (priorityTasks.length > 0) {
271
+ lines.push(`### ${priority.charAt(0).toUpperCase() + priority.slice(1)} Priority`);
272
+ lines.push('');
273
+
274
+ for (const task of priorityTasks) {
275
+ const status = task.status === 'done' ? '✅' :
276
+ task.status === 'in-progress' ? '🔄' : '⬜';
277
+ lines.push(`- ${status} **${task.id}**: ${task.title} (${task.storyPoints}pt)`);
278
+ if (task.requirementId) {
279
+ lines.push(` - Requirement: ${task.requirementId}`);
280
+ }
281
+ }
282
+ lines.push('');
283
+ }
284
+ }
285
+
286
+ return lines.join('\n');
287
+ }
288
+
289
+ /**
290
+ * Prioritize tasks by dependencies and priority
291
+ * @param {Array} tasks - Tasks to prioritize
292
+ * @returns {Array} Prioritized tasks
293
+ */
294
+ prioritizeTasks(tasks) {
295
+ const priorityWeight = {
296
+ critical: 4,
297
+ high: 3,
298
+ medium: 2,
299
+ low: 1
300
+ };
301
+
302
+ // Sort by priority weight (descending) then by dependency count (ascending)
303
+ return [...tasks].sort((a, b) => {
304
+ const priorityDiff = priorityWeight[b.priority] - priorityWeight[a.priority];
305
+ if (priorityDiff !== 0) return priorityDiff;
306
+
307
+ return (a.dependencies?.length || 0) - (b.dependencies?.length || 0);
308
+ });
309
+ }
310
+
311
+ /**
312
+ * Calculate end date based on duration
313
+ * @param {string} startDate - Start date
314
+ * @returns {string} End date
315
+ */
316
+ calculateEndDate(startDate) {
317
+ const start = startDate ? new Date(startDate) : new Date();
318
+ const end = new Date(start);
319
+ end.setDate(end.getDate() + this.config.defaultSprintDuration);
320
+ return end.toISOString().slice(0, 10);
321
+ }
322
+
323
+ /**
324
+ * Save sprint to storage
325
+ * @param {Object} sprint - Sprint to save
326
+ */
327
+ async saveSprint(sprint) {
328
+ await this.ensureStorageDir();
329
+
330
+ const filePath = path.join(this.config.storageDir, `${sprint.id}.json`);
331
+ await fs.writeFile(filePath, JSON.stringify(sprint, null, 2), 'utf-8');
332
+ }
333
+
334
+ /**
335
+ * Load sprint from storage
336
+ * @param {string} sprintId - Sprint ID
337
+ * @returns {Promise<Object|null>} Sprint
338
+ */
339
+ async loadSprint(sprintId) {
340
+ try {
341
+ const filePath = path.join(this.config.storageDir, `${sprintId}.json`);
342
+ const content = await fs.readFile(filePath, 'utf-8');
343
+ return JSON.parse(content);
344
+ } catch {
345
+ return null;
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Ensure storage directory exists
351
+ */
352
+ async ensureStorageDir() {
353
+ try {
354
+ await fs.access(this.config.storageDir);
355
+ } catch {
356
+ await fs.mkdir(this.config.storageDir, { recursive: true });
357
+ }
358
+ }
359
+ }
360
+
361
+ module.exports = { SprintPlanner, PRIORITY };