claude-autopm 1.27.0 → 1.29.0

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,689 @@
1
+ /**
2
+ * AnalyticsEngine - Comprehensive analytics and insights for PRDs, Epics, and Tasks
3
+ *
4
+ * Provides metrics, velocity tracking, completion rates, and team performance analytics.
5
+ *
6
+ * @example Epic Analysis
7
+ * ```javascript
8
+ * const AnalyticsEngine = require('./lib/analytics-engine');
9
+ * const engine = new AnalyticsEngine({ basePath: '.claude' });
10
+ *
11
+ * const analytics = await engine.analyzeEpic('epic-001');
12
+ * console.log(analytics.status); // Status breakdown
13
+ * console.log(analytics.velocity); // Velocity metrics
14
+ * console.log(analytics.progress); // Progress percentage
15
+ * console.log(analytics.blockers); // Blocked tasks
16
+ * ```
17
+ *
18
+ * @example Team Metrics
19
+ * ```javascript
20
+ * const metrics = await engine.getTeamMetrics({ period: 30 });
21
+ * console.log(metrics.completion); // Completion rate
22
+ * console.log(metrics.velocity); // Tasks per week
23
+ * console.log(metrics.duration); // Average duration
24
+ * ```
25
+ *
26
+ * @module AnalyticsEngine
27
+ * @version 1.0.0
28
+ * @since v1.29.0
29
+ */
30
+
31
+ const FilterEngine = require('./filter-engine');
32
+ const fs = require('fs').promises;
33
+ const path = require('path');
34
+
35
+ class AnalyticsEngine {
36
+ /**
37
+ * Create AnalyticsEngine instance
38
+ *
39
+ * @param {Object} options - Configuration options
40
+ * @param {string} options.basePath - Base path for file operations (default: '.claude')
41
+ */
42
+ constructor(options = {}) {
43
+ this.basePath = options.basePath || '.claude';
44
+ this.filterEngine = new FilterEngine({ basePath: this.basePath });
45
+ }
46
+
47
+ /**
48
+ * Analyze epic with comprehensive metrics
49
+ *
50
+ * @param {string} epicId - Epic ID to analyze
51
+ * @returns {Promise<Object|null>} - Analytics object or null if epic not found
52
+ *
53
+ * @example
54
+ * const analytics = await engine.analyzeEpic('epic-001');
55
+ * // Returns:
56
+ * // {
57
+ * // epicId: 'epic-001',
58
+ * // title: 'User Authentication',
59
+ * // status: { total: 25, completed: 15, in_progress: 7, pending: 3 },
60
+ * // velocity: { current: 3.5, average: 3.2, trend: 'increasing' },
61
+ * // progress: { percentage: 60, remainingTasks: 10, completedTasks: 15 },
62
+ * // timeline: { started: '2025-01-15', lastUpdate: '2025-10-06', daysActive: 265 },
63
+ * // blockers: [{ taskId: 'task-003', reason: 'Waiting for API' }],
64
+ * // dependencies: { blocked: 2, blocking: 3 }
65
+ * // }
66
+ */
67
+ async analyzeEpic(epicId) {
68
+ // Load epic and tasks
69
+ const epicDir = path.join(this.basePath, 'epics', epicId);
70
+
71
+ try {
72
+ await fs.access(epicDir);
73
+ } catch (error) {
74
+ return null; // Epic doesn't exist
75
+ }
76
+
77
+ // Load epic metadata
78
+ const epicFile = path.join(epicDir, 'epic.md');
79
+ let epicMetadata = {};
80
+ try {
81
+ const epicContent = await fs.readFile(epicFile, 'utf8');
82
+ const parsed = this.filterEngine.parseFrontmatter(epicContent);
83
+ epicMetadata = parsed.frontmatter;
84
+ } catch (error) {
85
+ epicMetadata = { id: epicId, title: epicId };
86
+ }
87
+
88
+ // Load all tasks for this epic
89
+ const tasks = await this.filterEngine.loadFiles(epicDir);
90
+ const taskFiles = tasks.filter(t => t.frontmatter.id && t.frontmatter.id !== epicId);
91
+
92
+ if (taskFiles.length === 0) {
93
+ return {
94
+ epicId,
95
+ title: epicMetadata.title || epicId,
96
+ status: { total: 0, completed: 0, in_progress: 0, pending: 0, blocked: 0 },
97
+ velocity: { current: 0, average: 0, trend: 'stable' },
98
+ progress: { percentage: 0, remainingTasks: 0, completedTasks: 0 },
99
+ timeline: {
100
+ started: epicMetadata.created || null,
101
+ lastUpdate: epicMetadata.updated || null,
102
+ daysActive: 0,
103
+ estimatedCompletion: null
104
+ },
105
+ blockers: [],
106
+ dependencies: { blocked: 0, blocking: 0 }
107
+ };
108
+ }
109
+
110
+ // Calculate status breakdown
111
+ const status = this._calculateStatusBreakdown(taskFiles);
112
+
113
+ // Calculate progress
114
+ const progress = this._calculateProgress(taskFiles);
115
+
116
+ // Calculate velocity
117
+ const velocity = await this._calculateVelocityForEpic(taskFiles);
118
+
119
+ // Calculate timeline
120
+ const timeline = this._calculateTimeline(epicMetadata, taskFiles, velocity.average);
121
+
122
+ // Find blockers
123
+ const blockers = this._findBlockersInTasks(taskFiles);
124
+
125
+ // Calculate dependencies
126
+ const dependencies = this._calculateDependencies(taskFiles);
127
+
128
+ return {
129
+ epicId,
130
+ title: epicMetadata.title || epicId,
131
+ status,
132
+ velocity,
133
+ progress,
134
+ timeline,
135
+ blockers,
136
+ dependencies
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Get team metrics for specified period
142
+ *
143
+ * @param {Object} options - Options
144
+ * @param {number} options.period - Number of days (default: 30)
145
+ * @param {string[]} options.types - Types to include (default: ['prd', 'epic', 'task'])
146
+ * @returns {Promise<Object>} - Team metrics
147
+ *
148
+ * @example
149
+ * const metrics = await engine.getTeamMetrics({ period: 30 });
150
+ * // Returns:
151
+ * // {
152
+ * // period: { start: '2025-09-06', end: '2025-10-06', days: 30 },
153
+ * // completion: { total: 150, completed: 120, rate: 0.80 },
154
+ * // velocity: { tasksPerWeek: 28, epicsPerMonth: 6 },
155
+ * // duration: { averageTaskDays: 2.5, averageEpicDays: 21 },
156
+ * // breakdown: { prd: {...}, epic: {...}, task: {...} }
157
+ * // }
158
+ */
159
+ async getTeamMetrics(options = {}) {
160
+ const period = options.period || 30;
161
+ const types = options.types || ['prd', 'epic', 'task'];
162
+
163
+ const endDate = new Date();
164
+ const startDate = new Date(endDate);
165
+ startDate.setDate(startDate.getDate() - period);
166
+
167
+ const startDateStr = this._formatDate(startDate);
168
+ const endDateStr = this._formatDate(endDate);
169
+
170
+ // Load all files
171
+ const allFiles = [];
172
+
173
+ for (const type of types) {
174
+ let typeFiles;
175
+ if (type === 'task') {
176
+ // Load tasks from all epics
177
+ typeFiles = await this._loadAllTasks();
178
+ } else if (type === 'epic') {
179
+ // Load epics from epic directories
180
+ typeFiles = await this._loadAllEpics();
181
+ } else {
182
+ const dir = path.join(this.basePath, `${type}s`);
183
+ typeFiles = await this.filterEngine.loadFiles(dir);
184
+ }
185
+
186
+ // Filter by date range
187
+ const filteredFiles = typeFiles.filter(f => {
188
+ const created = f.frontmatter.created;
189
+ if (!created) return false;
190
+ return created >= startDateStr && created <= endDateStr;
191
+ });
192
+
193
+ allFiles.push(...filteredFiles.map(f => ({ ...f, type })));
194
+ }
195
+
196
+ // Calculate completion metrics
197
+ const completion = this._calculateCompletionMetrics(allFiles);
198
+
199
+ // Calculate velocity
200
+ const velocity = this._calculateVelocityMetrics(allFiles, period);
201
+
202
+ // Calculate duration
203
+ const duration = this._calculateDurationMetrics(allFiles);
204
+
205
+ // Calculate breakdown by type
206
+ const breakdown = this._calculateBreakdown(allFiles);
207
+
208
+ return {
209
+ period: {
210
+ start: startDateStr,
211
+ end: endDateStr,
212
+ days: period
213
+ },
214
+ completion,
215
+ velocity,
216
+ duration,
217
+ breakdown
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Calculate velocity for an epic
223
+ *
224
+ * @param {string} epicId - Epic ID
225
+ * @param {number} periodDays - Period in days (default: 7)
226
+ * @returns {Promise<Object>} - Velocity metrics
227
+ *
228
+ * @example
229
+ * const velocity = await engine.calculateVelocity('epic-001', 7);
230
+ * // Returns: { tasksPerWeek: 3.5, completedInPeriod: 7, periodDays: 7 }
231
+ */
232
+ async calculateVelocity(epicId, periodDays = 7) {
233
+ const epicDir = path.join(this.basePath, 'epics', epicId);
234
+ const tasks = await this.filterEngine.loadFiles(epicDir);
235
+ const taskFiles = tasks.filter(t => t.frontmatter.id && t.frontmatter.id !== epicId);
236
+
237
+ return this._calculateVelocityForPeriod(taskFiles, periodDays);
238
+ }
239
+
240
+ /**
241
+ * Get completion rate for a type
242
+ *
243
+ * @param {string} type - Type (task/epic/prd)
244
+ * @param {number} periodDays - Period in days (default: 30)
245
+ * @returns {Promise<Object>} - Completion rate
246
+ *
247
+ * @example
248
+ * const rate = await engine.getCompletionRate('task', 30);
249
+ * // Returns: { total: 150, completed: 120, rate: 0.80 }
250
+ */
251
+ async getCompletionRate(type, periodDays = 30) {
252
+ const endDate = new Date();
253
+ const startDate = new Date(endDate);
254
+ startDate.setDate(startDate.getDate() - periodDays);
255
+ const startDateStr = this._formatDate(startDate);
256
+
257
+ let files;
258
+ if (type === 'task') {
259
+ files = await this._loadAllTasks();
260
+ } else if (type === 'epic') {
261
+ files = await this._loadAllEpics();
262
+ } else {
263
+ const dir = path.join(this.basePath, `${type}s`);
264
+ files = await this.filterEngine.loadFiles(dir);
265
+ }
266
+
267
+ // Filter by date range
268
+ const filteredFiles = files.filter(f => {
269
+ const created = f.frontmatter.created;
270
+ if (!created) return false;
271
+ return created >= startDateStr;
272
+ });
273
+
274
+ const total = filteredFiles.length;
275
+ const completed = filteredFiles.filter(f => f.frontmatter.status === 'completed').length;
276
+ const rate = total > 0 ? completed / total : 0;
277
+
278
+ return { total, completed, rate };
279
+ }
280
+
281
+ /**
282
+ * Find blockers for an epic
283
+ *
284
+ * @param {string} epicId - Epic ID
285
+ * @returns {Promise<Array>} - Array of blocked tasks
286
+ *
287
+ * @example
288
+ * const blockers = await engine.findBlockers('epic-001');
289
+ * // Returns: [
290
+ * // { taskId: 'task-003', reason: 'Waiting for API access' },
291
+ * // { taskId: 'task-012', reason: 'Depends on task-003' }
292
+ * // ]
293
+ */
294
+ async findBlockers(epicId) {
295
+ const epicDir = path.join(this.basePath, 'epics', epicId);
296
+ const tasks = await this.filterEngine.loadFiles(epicDir);
297
+ const taskFiles = tasks.filter(t => t.frontmatter.id && t.frontmatter.id !== epicId);
298
+
299
+ return this._findBlockersInTasks(taskFiles);
300
+ }
301
+
302
+ /**
303
+ * Export analytics to JSON or CSV
304
+ *
305
+ * @param {Object} analytics - Analytics object
306
+ * @param {string} format - Format (json/csv, default: json)
307
+ * @returns {Promise<string>} - Exported data
308
+ *
309
+ * @example
310
+ * const analytics = await engine.analyzeEpic('epic-001');
311
+ * const json = await engine.export(analytics, 'json');
312
+ * const csv = await engine.export(analytics, 'csv');
313
+ */
314
+ async export(analytics, format = 'json') {
315
+ if (format === 'csv') {
316
+ return this._exportToCSV(analytics);
317
+ }
318
+
319
+ return JSON.stringify(analytics, null, 2);
320
+ }
321
+
322
+ // ============================================================================
323
+ // Private Helper Methods
324
+ // ============================================================================
325
+
326
+ _calculateStatusBreakdown(taskFiles) {
327
+ const status = {
328
+ total: taskFiles.length,
329
+ completed: 0,
330
+ in_progress: 0,
331
+ pending: 0,
332
+ blocked: 0
333
+ };
334
+
335
+ for (const task of taskFiles) {
336
+ const taskStatus = task.frontmatter.status;
337
+ if (taskStatus === 'completed') {
338
+ status.completed++;
339
+ } else if (taskStatus === 'in_progress') {
340
+ status.in_progress++;
341
+ } else if (taskStatus === 'blocked') {
342
+ status.blocked++;
343
+ } else if (taskStatus === 'pending') {
344
+ status.pending++;
345
+ }
346
+ }
347
+
348
+ return status;
349
+ }
350
+
351
+ _calculateProgress(taskFiles) {
352
+ const total = taskFiles.length;
353
+ const completed = taskFiles.filter(t => t.frontmatter.status === 'completed').length;
354
+ const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
355
+
356
+ return {
357
+ percentage,
358
+ completedTasks: completed,
359
+ remainingTasks: total - completed
360
+ };
361
+ }
362
+
363
+ async _calculateVelocityForEpic(taskFiles) {
364
+ // Calculate current velocity (last 7 days)
365
+ const current = await this._calculateVelocityForPeriod(taskFiles, 7);
366
+
367
+ // Calculate average velocity (all time)
368
+ const completedTasks = taskFiles.filter(t =>
369
+ t.frontmatter.status === 'completed' && t.frontmatter.completed
370
+ );
371
+
372
+ if (completedTasks.length === 0) {
373
+ return {
374
+ current: 0,
375
+ average: 0,
376
+ trend: 'stable'
377
+ };
378
+ }
379
+
380
+ // Get date range
381
+ const dates = completedTasks
382
+ .map(t => new Date(t.frontmatter.completed))
383
+ .sort((a, b) => a - b);
384
+
385
+ const firstDate = dates[0];
386
+ const lastDate = dates[dates.length - 1];
387
+ const totalDays = Math.max(1, Math.ceil((lastDate - firstDate) / (1000 * 60 * 60 * 24)));
388
+ const totalWeeks = totalDays / 7;
389
+
390
+ const average = totalWeeks > 0 ? completedTasks.length / totalWeeks : 0;
391
+
392
+ // Determine trend
393
+ let trend = 'stable';
394
+ if (current.tasksPerWeek > average * 1.1) {
395
+ trend = 'increasing';
396
+ } else if (current.tasksPerWeek < average * 0.9) {
397
+ trend = 'decreasing';
398
+ }
399
+
400
+ return {
401
+ current: Math.round(current.tasksPerWeek * 10) / 10,
402
+ average: Math.round(average * 10) / 10,
403
+ trend
404
+ };
405
+ }
406
+
407
+ _calculateVelocityForPeriod(taskFiles, periodDays) {
408
+ const endDate = new Date();
409
+ const startDate = new Date(endDate);
410
+ startDate.setDate(startDate.getDate() - periodDays);
411
+
412
+ const completedInPeriod = taskFiles.filter(t => {
413
+ if (t.frontmatter.status !== 'completed' || !t.frontmatter.completed) {
414
+ return false;
415
+ }
416
+ const completedDate = new Date(t.frontmatter.completed);
417
+ return completedDate >= startDate && completedDate <= endDate;
418
+ }).length;
419
+
420
+ const weeks = periodDays / 7;
421
+ const tasksPerWeek = weeks > 0 ? completedInPeriod / weeks : 0;
422
+
423
+ return {
424
+ tasksPerWeek: Math.round(tasksPerWeek * 10) / 10,
425
+ completedInPeriod,
426
+ periodDays
427
+ };
428
+ }
429
+
430
+ _calculateTimeline(epicMetadata, taskFiles, averageVelocity) {
431
+ const started = epicMetadata.created || null;
432
+ const lastUpdate = epicMetadata.updated || this._findLastUpdateDate(taskFiles);
433
+
434
+ let daysActive = 0;
435
+ if (started) {
436
+ const startDate = new Date(started);
437
+ const endDate = lastUpdate ? new Date(lastUpdate) : new Date();
438
+ daysActive = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));
439
+ }
440
+
441
+ // Estimate completion
442
+ let estimatedCompletion = null;
443
+ if (averageVelocity > 0) {
444
+ const remainingTasks = taskFiles.filter(t => t.frontmatter.status !== 'completed').length;
445
+ const weeksRemaining = remainingTasks / averageVelocity;
446
+ const daysRemaining = Math.ceil(weeksRemaining * 7);
447
+
448
+ const completionDate = new Date();
449
+ completionDate.setDate(completionDate.getDate() + daysRemaining);
450
+ estimatedCompletion = this._formatDate(completionDate);
451
+ }
452
+
453
+ return {
454
+ started,
455
+ lastUpdate,
456
+ daysActive,
457
+ estimatedCompletion
458
+ };
459
+ }
460
+
461
+ _findLastUpdateDate(taskFiles) {
462
+ const dates = taskFiles
463
+ .map(t => t.frontmatter.updated || t.frontmatter.completed || t.frontmatter.created)
464
+ .filter(d => d)
465
+ .sort()
466
+ .reverse();
467
+
468
+ return dates.length > 0 ? dates[0] : null;
469
+ }
470
+
471
+ _findBlockersInTasks(taskFiles) {
472
+ const blockers = [];
473
+
474
+ for (const task of taskFiles) {
475
+ if (task.frontmatter.status === 'blocked') {
476
+ blockers.push({
477
+ taskId: task.frontmatter.id,
478
+ reason: task.frontmatter.blocker_reason || 'Unknown reason'
479
+ });
480
+ }
481
+ }
482
+
483
+ return blockers;
484
+ }
485
+
486
+ _calculateDependencies(taskFiles) {
487
+ let blocked = 0;
488
+ let blocking = 0;
489
+
490
+ for (const task of taskFiles) {
491
+ if (task.frontmatter.depends_on && task.frontmatter.depends_on.length > 0) {
492
+ blocked++;
493
+ }
494
+ if (task.frontmatter.blocks && task.frontmatter.blocks.length > 0) {
495
+ blocking += task.frontmatter.blocks.length;
496
+ }
497
+ }
498
+
499
+ return { blocked, blocking };
500
+ }
501
+
502
+ async _loadAllTasks() {
503
+ const epicsDir = path.join(this.basePath, 'epics');
504
+
505
+ try {
506
+ await fs.access(epicsDir);
507
+ } catch (error) {
508
+ return [];
509
+ }
510
+
511
+ const epicDirs = await fs.readdir(epicsDir);
512
+ const allTasks = [];
513
+
514
+ for (const epicDir of epicDirs) {
515
+ const epicPath = path.join(epicsDir, epicDir);
516
+ const stat = await fs.stat(epicPath);
517
+
518
+ if (stat.isDirectory()) {
519
+ const tasks = await this.filterEngine.loadFiles(epicPath);
520
+ const taskFiles = tasks.filter(t => t.frontmatter.id && !t.path.endsWith('epic.md'));
521
+ allTasks.push(...taskFiles);
522
+ }
523
+ }
524
+
525
+ return allTasks;
526
+ }
527
+
528
+ async _loadAllEpics() {
529
+ const epicsDir = path.join(this.basePath, 'epics');
530
+
531
+ try {
532
+ await fs.access(epicsDir);
533
+ } catch (error) {
534
+ return [];
535
+ }
536
+
537
+ const epicDirs = await fs.readdir(epicsDir);
538
+ const allEpics = [];
539
+
540
+ for (const epicDir of epicDirs) {
541
+ const epicPath = path.join(epicsDir, epicDir);
542
+ const stat = await fs.stat(epicPath);
543
+
544
+ if (stat.isDirectory()) {
545
+ const files = await this.filterEngine.loadFiles(epicPath);
546
+ const epicFile = files.find(f => f.path.endsWith('epic.md'));
547
+ if (epicFile) {
548
+ allEpics.push(epicFile);
549
+ }
550
+ }
551
+ }
552
+
553
+ return allEpics;
554
+ }
555
+
556
+ _calculateCompletionMetrics(allFiles) {
557
+ const total = allFiles.length;
558
+ const completed = allFiles.filter(f => f.frontmatter.status === 'completed').length;
559
+ const rate = total > 0 ? completed / total : 0;
560
+
561
+ return { total, completed, rate: Math.round(rate * 100) / 100 };
562
+ }
563
+
564
+ _calculateVelocityMetrics(allFiles, periodDays) {
565
+ const tasks = allFiles.filter(f => f.type === 'task');
566
+ const completedTasks = tasks.filter(t =>
567
+ t.frontmatter.status === 'completed' && t.frontmatter.completed
568
+ );
569
+
570
+ const weeks = periodDays / 7;
571
+ const tasksPerWeek = weeks > 0 ? completedTasks.length / weeks : 0;
572
+
573
+ const epics = allFiles.filter(f => f.type === 'epic');
574
+ const completedEpics = epics.filter(e => e.frontmatter.status === 'completed');
575
+ const months = periodDays / 30;
576
+ const epicsPerMonth = months > 0 ? completedEpics.length / months : 0;
577
+
578
+ return {
579
+ tasksPerWeek: Math.round(tasksPerWeek * 10) / 10,
580
+ epicsPerMonth: Math.round(epicsPerMonth * 10) / 10
581
+ };
582
+ }
583
+
584
+ _calculateDurationMetrics(allFiles) {
585
+ const tasks = allFiles.filter(f =>
586
+ f.type === 'task' &&
587
+ f.frontmatter.status === 'completed' &&
588
+ f.frontmatter.created &&
589
+ f.frontmatter.completed
590
+ );
591
+
592
+ let totalTaskDays = 0;
593
+ for (const task of tasks) {
594
+ const created = new Date(task.frontmatter.created);
595
+ const completed = new Date(task.frontmatter.completed);
596
+ const days = Math.ceil((completed - created) / (1000 * 60 * 60 * 24));
597
+ totalTaskDays += days;
598
+ }
599
+
600
+ const averageTaskDays = tasks.length > 0 ? totalTaskDays / tasks.length : 0;
601
+
602
+ const epics = allFiles.filter(f =>
603
+ f.type === 'epic' &&
604
+ f.frontmatter.status === 'completed' &&
605
+ f.frontmatter.created &&
606
+ f.frontmatter.completed
607
+ );
608
+
609
+ let totalEpicDays = 0;
610
+ for (const epic of epics) {
611
+ const created = new Date(epic.frontmatter.created);
612
+ const completed = new Date(epic.frontmatter.completed);
613
+ const days = Math.ceil((completed - created) / (1000 * 60 * 60 * 24));
614
+ totalEpicDays += days;
615
+ }
616
+
617
+ const averageEpicDays = epics.length > 0 ? totalEpicDays / epics.length : 0;
618
+
619
+ return {
620
+ averageTaskDays: Math.round(averageTaskDays * 10) / 10,
621
+ averageEpicDays: Math.round(averageEpicDays * 10) / 10
622
+ };
623
+ }
624
+
625
+ _calculateBreakdown(allFiles) {
626
+ const breakdown = {};
627
+
628
+ const types = ['prd', 'epic', 'task'];
629
+ for (const type of types) {
630
+ const typeFiles = allFiles.filter(f => f.type === type);
631
+ const total = typeFiles.length;
632
+ const completed = typeFiles.filter(f => f.frontmatter.status === 'completed').length;
633
+
634
+ breakdown[type] = { total, completed };
635
+ }
636
+
637
+ return breakdown;
638
+ }
639
+
640
+ _formatDate(date) {
641
+ const year = date.getFullYear();
642
+ const month = String(date.getMonth() + 1).padStart(2, '0');
643
+ const day = String(date.getDate()).padStart(2, '0');
644
+ return `${year}-${month}-${day}`;
645
+ }
646
+
647
+ _exportToCSV(analytics) {
648
+ const lines = [];
649
+
650
+ // Header
651
+ lines.push('Metric,Value');
652
+
653
+ // Epic info
654
+ lines.push(`Epic ID,${analytics.epicId}`);
655
+ lines.push(`Title,${analytics.title}`);
656
+
657
+ // Status
658
+ lines.push(`Total Tasks,${analytics.status.total}`);
659
+ lines.push(`Completed,${analytics.status.completed}`);
660
+ lines.push(`In Progress,${analytics.status.in_progress}`);
661
+ lines.push(`Pending,${analytics.status.pending}`);
662
+ lines.push(`Blocked,${analytics.status.blocked}`);
663
+
664
+ // Progress
665
+ lines.push(`Progress %,${analytics.progress.percentage}`);
666
+
667
+ // Velocity
668
+ lines.push(`Current Velocity,${analytics.velocity.current}`);
669
+ lines.push(`Average Velocity,${analytics.velocity.average}`);
670
+ lines.push(`Trend,${analytics.velocity.trend}`);
671
+
672
+ // Timeline
673
+ lines.push(`Started,${analytics.timeline.started || 'N/A'}`);
674
+ lines.push(`Last Update,${analytics.timeline.lastUpdate || 'N/A'}`);
675
+ lines.push(`Days Active,${analytics.timeline.daysActive}`);
676
+ lines.push(`Est. Completion,${analytics.timeline.estimatedCompletion || 'N/A'}`);
677
+
678
+ // Dependencies
679
+ lines.push(`Blocked Tasks,${analytics.dependencies.blocked}`);
680
+ lines.push(`Blocking Tasks,${analytics.dependencies.blocking}`);
681
+
682
+ // Blockers
683
+ lines.push(`Total Blockers,${analytics.blockers.length}`);
684
+
685
+ return lines.join('\n');
686
+ }
687
+ }
688
+
689
+ module.exports = AnalyticsEngine;