repo-wrapped 0.0.6 → 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.
- package/SPECS.md +490 -0
- package/dist/commands/generate.js +262 -5
- package/dist/generators/html/styles/base.css +2 -3
- package/dist/generators/html/styles/leaddev.css +1275 -0
- package/dist/generators/html/templates/comparisonSection.js +119 -0
- package/dist/generators/html/templates/eventsSection.js +113 -0
- package/dist/generators/html/templates/executiveSummarySection.js +84 -0
- package/dist/generators/html/templates/gapSection.js +190 -0
- package/dist/generators/html/templates/teamSection.js +146 -0
- package/dist/generators/html/templates/velocitySection.js +189 -0
- package/dist/generators/html/utils/styleLoader.js +2 -1
- package/dist/index.js +33 -0
- package/dist/utils/eventAnnotationParser.js +253 -0
- package/dist/utils/executiveSummaryGenerator.js +275 -0
- package/dist/utils/gapAnalyzer.js +300 -0
- package/dist/utils/htmlGenerator.js +103 -23
- package/dist/utils/rangeComparisonAnalyzer.js +222 -0
- package/dist/utils/teamAnalyzer.js +297 -0
- package/dist/utils/velocityAnalyzer.js +252 -0
- package/package.json +11 -2
- package/test-team.txt +2 -0
|
@@ -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
|
-
|
|
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
|
+
}
|