repo-wrapped 0.0.5 → 0.0.7

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,189 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildVelocitySection = buildVelocitySection;
4
+ const date_fns_1 = require("date-fns");
5
+ function buildVelocitySection(velocity) {
6
+ if (velocity.timeline.length === 0) {
7
+ return `
8
+ <div class="velocity-section">
9
+ <h2>📈 Velocity Analysis</h2>
10
+ <div class="empty-state">No velocity data available</div>
11
+ </div>
12
+ `;
13
+ }
14
+ const chartHtml = buildVelocityChart(velocity);
15
+ const statsHtml = buildVelocityStats(velocity);
16
+ const anomaliesHtml = buildAnomaliesList(velocity.anomalies);
17
+ return `
18
+ <div class="velocity-section">
19
+ <h2>📈 Velocity Analysis</h2>
20
+
21
+ ${statsHtml}
22
+
23
+ <div class="velocity-chart-container">
24
+ <h3>Commits Per Week</h3>
25
+ ${chartHtml}
26
+ </div>
27
+
28
+ ${anomaliesHtml}
29
+ </div>
30
+ `;
31
+ }
32
+ function buildVelocityStats(velocity) {
33
+ const trendEmoji = velocity.overallTrend === 'increasing' ? '📈' :
34
+ velocity.overallTrend === 'decreasing' ? '📉' :
35
+ velocity.overallTrend === 'volatile' ? '📊' : '➡️';
36
+ const trendClass = velocity.overallTrend === 'increasing' ? 'positive' :
37
+ velocity.overallTrend === 'decreasing' ? 'negative' : 'neutral';
38
+ const trendSign = velocity.trendPercentage >= 0 ? '+' : '';
39
+ return `
40
+ <div class="velocity-stats">
41
+ <div class="velocity-stat-card">
42
+ <div class="stat-icon">${trendEmoji}</div>
43
+ <div class="stat-info">
44
+ <div class="stat-label">Overall Trend</div>
45
+ <div class="stat-value ${trendClass}">${velocity.overallTrend}</div>
46
+ <div class="stat-detail">${trendSign}${velocity.trendPercentage}% over period</div>
47
+ </div>
48
+ </div>
49
+ <div class="velocity-stat-card">
50
+ <div class="stat-icon">📊</div>
51
+ <div class="stat-info">
52
+ <div class="stat-label">Average</div>
53
+ <div class="stat-value">${velocity.averageCommitsPerWeek}</div>
54
+ <div class="stat-detail">commits/week</div>
55
+ </div>
56
+ </div>
57
+ <div class="velocity-stat-card">
58
+ <div class="stat-icon">🚀</div>
59
+ <div class="stat-info">
60
+ <div class="stat-label">Peak Week</div>
61
+ <div class="stat-value">${velocity.peakWeek.commits}</div>
62
+ <div class="stat-detail">${(0, date_fns_1.format)(velocity.peakWeek.weekStart, 'MMM d, yyyy')}</div>
63
+ </div>
64
+ </div>
65
+ <div class="velocity-stat-card">
66
+ <div class="stat-icon">🐢</div>
67
+ <div class="stat-info">
68
+ <div class="stat-label">Lowest Week</div>
69
+ <div class="stat-value">${velocity.lowestWeek.commits}</div>
70
+ <div class="stat-detail">${(0, date_fns_1.format)(velocity.lowestWeek.weekStart, 'MMM d, yyyy')}</div>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ `;
75
+ }
76
+ function buildVelocityChart(velocity) {
77
+ const timeline = velocity.timeline;
78
+ const maxCommits = Math.max(...timeline.map(d => d.commits), 1);
79
+ const chartWidth = 800;
80
+ const chartHeight = 200;
81
+ const padding = { top: 20, right: 20, bottom: 40, left: 50 };
82
+ const innerWidth = chartWidth - padding.left - padding.right;
83
+ const innerHeight = chartHeight - padding.top - padding.bottom;
84
+ // Build SVG path for commits
85
+ const xScale = (i) => padding.left + (i / (timeline.length - 1 || 1)) * innerWidth;
86
+ const yScale = (v) => padding.top + innerHeight - (v / maxCommits) * innerHeight;
87
+ // Commits line
88
+ const commitsPath = timeline.map((d, i) => `${i === 0 ? 'M' : 'L'} ${xScale(i)} ${yScale(d.commits)}`).join(' ');
89
+ // Rolling average line
90
+ const avgPath = timeline.map((d, i) => `${i === 0 ? 'M' : 'L'} ${xScale(i)} ${yScale(d.rollingAverage)}`).join(' ');
91
+ // Area fill under commits line
92
+ const areaPath = `${commitsPath} L ${xScale(timeline.length - 1)} ${yScale(0)} L ${xScale(0)} ${yScale(0)} Z`;
93
+ // Find anomaly positions
94
+ const anomalyMarkers = velocity.anomalies.map(anomaly => {
95
+ const idx = timeline.findIndex(d => (0, date_fns_1.format)(d.weekStart, 'yyyy-MM-dd') === (0, date_fns_1.format)(anomaly.weekStart, 'yyyy-MM-dd'));
96
+ if (idx === -1)
97
+ return '';
98
+ const x = xScale(idx);
99
+ const y = yScale(timeline[idx].commits);
100
+ const color = anomaly.type === 'drop' ? '#ff6b6b' : anomaly.type === 'spike' ? '#ffa94d' : '#868e96';
101
+ return `<circle cx="${x}" cy="${y}" r="6" fill="${color}" class="anomaly-marker" data-type="${anomaly.type}" data-severity="${anomaly.severity}"/>`;
102
+ }).join('');
103
+ // X-axis labels (show every nth label to avoid crowding)
104
+ const labelInterval = Math.max(1, Math.floor(timeline.length / 8));
105
+ const xLabels = timeline.map((d, i) => {
106
+ if (i % labelInterval !== 0 && i !== timeline.length - 1)
107
+ return '';
108
+ return `<text x="${xScale(i)}" y="${chartHeight - 10}" text-anchor="middle" class="axis-label">${(0, date_fns_1.format)(d.weekStart, 'MMM d')}</text>`;
109
+ }).join('');
110
+ // Y-axis labels
111
+ const yTicks = [0, Math.round(maxCommits / 2), maxCommits];
112
+ const yLabels = yTicks.map(v => `<text x="${padding.left - 10}" y="${yScale(v) + 4}" text-anchor="end" class="axis-label">${v}</text>`).join('');
113
+ // Grid lines
114
+ const gridLines = yTicks.map(v => `<line x1="${padding.left}" y1="${yScale(v)}" x2="${chartWidth - padding.right}" y2="${yScale(v)}" class="grid-line"/>`).join('');
115
+ return `
116
+ <svg class="velocity-chart" viewBox="0 0 ${chartWidth} ${chartHeight}" preserveAspectRatio="xMidYMid meet">
117
+ <defs>
118
+ <linearGradient id="areaGradient" x1="0%" y1="0%" x2="0%" y2="100%">
119
+ <stop offset="0%" style="stop-color:var(--accent-color);stop-opacity:0.3"/>
120
+ <stop offset="100%" style="stop-color:var(--accent-color);stop-opacity:0.05"/>
121
+ </linearGradient>
122
+ </defs>
123
+
124
+ <!-- Grid -->
125
+ ${gridLines}
126
+
127
+ <!-- Area fill -->
128
+ <path d="${areaPath}" fill="url(#areaGradient)" class="area-fill"/>
129
+
130
+ <!-- Commits line -->
131
+ <path d="${commitsPath}" fill="none" stroke="var(--accent-color)" stroke-width="2" class="commits-line"/>
132
+
133
+ <!-- Rolling average line -->
134
+ <path d="${avgPath}" fill="none" stroke="#ffa94d" stroke-width="2" stroke-dasharray="5,5" class="avg-line"/>
135
+
136
+ <!-- Anomaly markers -->
137
+ ${anomalyMarkers}
138
+
139
+ <!-- Axes labels -->
140
+ ${xLabels}
141
+ ${yLabels}
142
+
143
+ <!-- Legend -->
144
+ <g transform="translate(${chartWidth - 150}, 10)">
145
+ <line x1="0" y1="5" x2="20" y2="5" stroke="var(--accent-color)" stroke-width="2"/>
146
+ <text x="25" y="9" class="legend-label">Commits</text>
147
+ <line x1="0" y1="20" x2="20" y2="20" stroke="#ffa94d" stroke-width="2" stroke-dasharray="5,5"/>
148
+ <text x="25" y="24" class="legend-label">Rolling Avg</text>
149
+ </g>
150
+ </svg>
151
+ `;
152
+ }
153
+ function buildAnomaliesList(anomalies) {
154
+ if (anomalies.length === 0) {
155
+ return '';
156
+ }
157
+ const criticalAnomalies = anomalies.filter(a => a.severity === 'critical' || a.severity === 'significant');
158
+ if (criticalAnomalies.length === 0) {
159
+ return '';
160
+ }
161
+ const anomalyItems = criticalAnomalies.slice(0, 5).map(anomaly => {
162
+ const typeEmoji = anomaly.type === 'drop' ? '📉' : anomaly.type === 'spike' ? '📈' : '🚫';
163
+ const severityClass = anomaly.severity === 'critical' ? 'critical' : 'warning';
164
+ const dateStr = (0, date_fns_1.format)(anomaly.weekStart, 'MMM d, yyyy');
165
+ return `
166
+ <div class="anomaly-item ${severityClass}">
167
+ <span class="anomaly-icon">${typeEmoji}</span>
168
+ <div class="anomaly-info">
169
+ <div class="anomaly-header">
170
+ <span class="anomaly-type">${anomaly.type}</span>
171
+ <span class="anomaly-date">${dateStr}</span>
172
+ <span class="anomaly-change">${anomaly.percentageChange >= 0 ? '+' : ''}${anomaly.percentageChange}%</span>
173
+ </div>
174
+ <div class="anomaly-causes">
175
+ Possible: ${anomaly.possibleCauses.slice(0, 2).join(', ')}
176
+ </div>
177
+ </div>
178
+ </div>
179
+ `;
180
+ }).join('');
181
+ return `
182
+ <div class="anomalies-section">
183
+ <h3>⚠️ Detected Anomalies</h3>
184
+ <div class="anomalies-list">
185
+ ${anomalyItems}
186
+ </div>
187
+ </div>
188
+ `;
189
+ }
@@ -13,5 +13,6 @@ function loadStyles() {
13
13
  const componentsCSS = (0, fs_1.readFileSync)((0, path_1.join)(stylesDir, 'components.css'), 'utf-8');
14
14
  const achievementsCSS = (0, fs_1.readFileSync)((0, path_1.join)(stylesDir, 'achievements.css'), 'utf-8');
15
15
  const knowledgeCSS = (0, fs_1.readFileSync)((0, path_1.join)(stylesDir, 'knowledge.css'), 'utf-8');
16
- return `${baseCSS}\n\n${componentsCSS}\n\n${achievementsCSS}\n\n${knowledgeCSS}`;
16
+ const leaddevCSS = (0, fs_1.readFileSync)((0, path_1.join)(stylesDir, 'leaddev.css'), 'utf-8');
17
+ return `${baseCSS}\n\n${componentsCSS}\n\n${achievementsCSS}\n\n${knowledgeCSS}\n\n${leaddevCSS}`;
17
18
  }
package/dist/index.js CHANGED
@@ -31,6 +31,21 @@ Generate Options:
31
31
  --body-check Include commit body in quality scoring
32
32
  --deep-analysis Enable file-level knowledge distribution analysis
33
33
 
34
+ Strategic Insights:
35
+ --velocity Enable velocity timeline analysis
36
+ --velocity-window <w> Rolling average window in weeks (default: 4)
37
+ --gap-analysis Enable gap detection and blocker analysis
38
+ --gap-threshold <days> Minimum gap days to report (default: 3)
39
+ --compare <r1> <r2> Compare two date ranges (format: YYYY-MM-DD..YYYY-MM-DD)
40
+ --compare-labels <l> Labels for comparison (format: "Label1,Label2")
41
+ --author <name> Filter to specific author (partial match)
42
+ --authors <names> Filter to multiple authors (comma-separated)
43
+ --team <file> Path to team members file (one name per line)
44
+ --exclude-author <name> Exclude specific author (e.g., bots)
45
+ --executive-summary Generate executive summary with KPIs
46
+ --summary-only Output only the executive summary (terminal or HTML)
47
+ --events <file> Path to events annotation JSON file
48
+
34
49
  Wrapped Options:
35
50
  --html Generate HTML slideshow and open in browser
36
51
  --compare Compare with previous year
@@ -38,6 +53,9 @@ Wrapped Options:
38
53
  Examples:
39
54
  repo-wrapped generate . --html
40
55
  repo-wrapped generate . --all --html --deep-analysis
56
+ repo-wrapped generate . --velocity --gap-analysis --executive-summary
57
+ repo-wrapped generate . --compare "2025-01-01..2025-06-30" "2025-07-01..2025-12-31"
58
+ repo-wrapped generate . --author "John" --html
41
59
  repo-wrapped wrapped 2025 . --html
42
60
  `);
43
61
  program
@@ -50,6 +68,21 @@ program
50
68
  .option('--html', 'Generate HTML output and open in browser')
51
69
  .option('--body-check', 'Include commit body in quality scoring')
52
70
  .option('--deep-analysis', 'Enable file-level analysis for detailed knowledge distribution')
71
+ // Lead Developer Insights options
72
+ .option('--velocity', 'Enable velocity timeline analysis')
73
+ .option('--velocity-window <weeks>', 'Rolling average window in weeks', '4')
74
+ .option('--gap-analysis', 'Enable gap detection and blocker analysis')
75
+ .option('--gap-threshold <days>', 'Minimum gap days to report', '3')
76
+ .option('--compare <ranges...>', 'Compare two date ranges (format: YYYY-MM-DD..YYYY-MM-DD)')
77
+ .option('--compare-labels <labels>', 'Labels for comparison (comma-separated)')
78
+ .option('--author <name>', 'Filter to specific author (partial match)')
79
+ .option('--authors <names>', 'Filter to multiple authors (comma-separated)')
80
+ .option('--team <file>', 'Path to team members file (one name per line)')
81
+ .option('--exclude-author <name>', 'Exclude specific author')
82
+ .option('--exclude-authors <names>', 'Exclude multiple authors (comma-separated)')
83
+ .option('--executive-summary', 'Generate executive summary with KPIs')
84
+ .option('--summary-only', 'Output only the executive summary')
85
+ .option('--events <file>', 'Path to events annotation JSON file')
53
86
  .action(generate_js_1.generateMatrix);
54
87
  program
55
88
  .command('wrapped')
@@ -0,0 +1,253 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadEventsFile = loadEventsFile;
37
+ exports.parseEventsInline = parseEventsInline;
38
+ exports.correlateEventsWithMetrics = correlateEventsWithMetrics;
39
+ exports.getEventTypeEmoji = getEventTypeEmoji;
40
+ exports.getEventTypeColor = getEventTypeColor;
41
+ exports.formatEventsInsights = formatEventsInsights;
42
+ const fs = __importStar(require("fs"));
43
+ const date_fns_1 = require("date-fns");
44
+ const commitQualityAnalyzer_1 = require("./commitQualityAnalyzer");
45
+ /**
46
+ * Load events from a JSON file
47
+ */
48
+ function loadEventsFile(filePath) {
49
+ try {
50
+ const content = fs.readFileSync(filePath, 'utf-8');
51
+ const parsed = JSON.parse(content);
52
+ // Validate and transform
53
+ const events = (parsed.events || []).map((event) => ({
54
+ date: new Date(event.date),
55
+ label: event.label,
56
+ type: validateEventType(event.type),
57
+ description: event.description,
58
+ }));
59
+ return { events };
60
+ }
61
+ catch (error) {
62
+ if (error.code === 'ENOENT') {
63
+ throw new Error(`Events file not found: ${filePath}`);
64
+ }
65
+ throw new Error(`Failed to parse events file: ${error.message}`);
66
+ }
67
+ }
68
+ /**
69
+ * Parse inline events JSON
70
+ */
71
+ function parseEventsInline(jsonStr) {
72
+ try {
73
+ const parsed = JSON.parse(jsonStr);
74
+ const events = (parsed.events || [parsed]).map((event) => ({
75
+ date: new Date(event.date),
76
+ label: event.label,
77
+ type: validateEventType(event.type),
78
+ description: event.description,
79
+ }));
80
+ return { events };
81
+ }
82
+ catch (error) {
83
+ throw new Error(`Failed to parse inline events JSON: ${error.message}`);
84
+ }
85
+ }
86
+ function validateEventType(type) {
87
+ const validTypes = ['disruption', 'attrition', 'growth', 'release', 'incident', 'external', 'custom'];
88
+ if (validTypes.includes(type)) {
89
+ return type;
90
+ }
91
+ return 'custom';
92
+ }
93
+ /**
94
+ * Calculate correlation between events and metrics changes
95
+ */
96
+ function correlateEventsWithMetrics(events, commits, windowDays = 14, skipBodyCheck = true) {
97
+ return events.map(event => ({
98
+ ...event,
99
+ correlation: calculateEventCorrelation(event, commits, windowDays, skipBodyCheck),
100
+ }));
101
+ }
102
+ function calculateEventCorrelation(event, commits, windowDays, skipBodyCheck) {
103
+ const eventDate = event.date;
104
+ // Before period
105
+ const beforeStart = (0, date_fns_1.subDays)(eventDate, windowDays);
106
+ const beforeEnd = (0, date_fns_1.subDays)(eventDate, 1);
107
+ const beforeCommits = commits.filter(c => {
108
+ const d = new Date(c.date);
109
+ return d >= beforeStart && d <= beforeEnd;
110
+ });
111
+ // After period
112
+ const afterStart = (0, date_fns_1.addDays)(eventDate, 1);
113
+ const afterEnd = (0, date_fns_1.addDays)(eventDate, windowDays);
114
+ const afterCommits = commits.filter(c => {
115
+ const d = new Date(c.date);
116
+ return d >= afterStart && d <= afterEnd;
117
+ });
118
+ const metricsBefore = calculatePeriodMetrics(beforeCommits, beforeStart, beforeEnd, skipBodyCheck);
119
+ const metricsAfter = calculatePeriodMetrics(afterCommits, afterStart, afterEnd, skipBodyCheck);
120
+ // Calculate changes
121
+ const velocityChange = calculatePercentageChange(metricsBefore.commitsPerWeek ?? 0, metricsAfter.commitsPerWeek ?? 0);
122
+ const qualityChange = calculatePercentageChange(metricsBefore.qualityScore ?? 0, metricsAfter.qualityScore ?? 0);
123
+ const activeAuthorsChange = (metricsAfter.totalAuthors ?? 0) - (metricsBefore.totalAuthors ?? 0);
124
+ // Generate assessment
125
+ const assessment = generateAssessment(event, velocityChange, qualityChange, activeAuthorsChange);
126
+ return {
127
+ metricsBefore,
128
+ metricsAfter,
129
+ impact: {
130
+ velocityChange,
131
+ qualityChange,
132
+ activeAuthorsChange,
133
+ },
134
+ assessment,
135
+ };
136
+ }
137
+ function calculatePeriodMetrics(commits, startDate, endDate, skipBodyCheck) {
138
+ const days = (0, date_fns_1.differenceInDays)(endDate, startDate) + 1;
139
+ const weeks = Math.max(1, days / 7);
140
+ const totalCommits = commits.length;
141
+ const commitsPerWeek = Math.round((totalCommits / weeks) * 10) / 10;
142
+ const totalAuthors = new Set(commits.map(c => c.author)).size;
143
+ const qualityResult = commits.length > 0
144
+ ? (0, commitQualityAnalyzer_1.analyzeCommitQuality)(commits, { skipBodyCheck })
145
+ : { overallScore: 0 };
146
+ return {
147
+ totalCommits,
148
+ commitsPerWeek,
149
+ totalAuthors,
150
+ qualityScore: Math.round(qualityResult.overallScore * 10) / 10,
151
+ };
152
+ }
153
+ function calculatePercentageChange(before, after) {
154
+ if (before === 0) {
155
+ return after > 0 ? 100 : 0;
156
+ }
157
+ return Math.round(((after - before) / before) * 1000) / 10;
158
+ }
159
+ function generateAssessment(event, velocityChange, qualityChange, authorChange) {
160
+ const parts = [];
161
+ const eventLabel = `"${event.label}"`;
162
+ if (velocityChange <= -30) {
163
+ parts.push(`velocity dropped ${Math.abs(velocityChange)}%`);
164
+ }
165
+ else if (velocityChange >= 30) {
166
+ parts.push(`velocity increased ${velocityChange}%`);
167
+ }
168
+ if (qualityChange <= -20) {
169
+ parts.push(`quality decreased ${Math.abs(qualityChange)}%`);
170
+ }
171
+ else if (qualityChange >= 20) {
172
+ parts.push(`quality improved ${qualityChange}%`);
173
+ }
174
+ if (authorChange < 0) {
175
+ parts.push(`${Math.abs(authorChange)} fewer active contributor(s)`);
176
+ }
177
+ else if (authorChange > 0) {
178
+ parts.push(`${authorChange} more active contributor(s)`);
179
+ }
180
+ if (parts.length === 0) {
181
+ return `No significant metric changes detected after ${eventLabel}`;
182
+ }
183
+ return `After ${eventLabel}: ${parts.join(', ')}`;
184
+ }
185
+ /**
186
+ * Get emoji for event type
187
+ */
188
+ function getEventTypeEmoji(type) {
189
+ const emojis = {
190
+ disruption: '⚡',
191
+ attrition: '👋',
192
+ growth: '🌱',
193
+ release: '🚀',
194
+ incident: '🔥',
195
+ external: '📅',
196
+ custom: '📌',
197
+ };
198
+ return emojis[type] || '📌';
199
+ }
200
+ /**
201
+ * Get color for event type (for HTML)
202
+ */
203
+ function getEventTypeColor(type) {
204
+ const colors = {
205
+ disruption: '#ff6b6b',
206
+ attrition: '#ffa94d',
207
+ growth: '#69db7c',
208
+ release: '#4dabf7',
209
+ incident: '#ff8787',
210
+ external: '#868e96',
211
+ custom: '#9775fa',
212
+ };
213
+ return colors[type] || '#9775fa';
214
+ }
215
+ /**
216
+ * Format events for display
217
+ */
218
+ function formatEventsInsights(events) {
219
+ const insights = [];
220
+ if (events.length === 0) {
221
+ return ['No events annotated'];
222
+ }
223
+ insights.push(`📅 ${events.length} Event(s) Annotated`);
224
+ insights.push('');
225
+ // Sort by date
226
+ const sortedEvents = [...events].sort((a, b) => a.date.getTime() - b.date.getTime());
227
+ for (const event of sortedEvents) {
228
+ const emoji = getEventTypeEmoji(event.type);
229
+ const dateStr = (0, date_fns_1.format)(event.date, 'MMM d, yyyy');
230
+ insights.push(`${emoji} ${dateStr}: ${event.label}`);
231
+ if (event.description) {
232
+ insights.push(` ${event.description}`);
233
+ }
234
+ if (event.correlation) {
235
+ const { impact } = event.correlation;
236
+ const impactParts = [];
237
+ if (Math.abs(impact.velocityChange) >= 10) {
238
+ impactParts.push(`velocity ${impact.velocityChange >= 0 ? '+' : ''}${impact.velocityChange}%`);
239
+ }
240
+ if (Math.abs(impact.qualityChange) >= 10) {
241
+ impactParts.push(`quality ${impact.qualityChange >= 0 ? '+' : ''}${impact.qualityChange}%`);
242
+ }
243
+ if (impact.activeAuthorsChange !== 0) {
244
+ impactParts.push(`${impact.activeAuthorsChange >= 0 ? '+' : ''}${impact.activeAuthorsChange} contributors`);
245
+ }
246
+ if (impactParts.length > 0) {
247
+ insights.push(` 📊 Impact: ${impactParts.join(', ')}`);
248
+ }
249
+ }
250
+ insights.push('');
251
+ }
252
+ return insights;
253
+ }