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.
- 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 +117 -32
- 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,297 @@
|
|
|
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.filterCommitsByAuthor = filterCommitsByAuthor;
|
|
37
|
+
exports.parseAuthorFilter = parseAuthorFilter;
|
|
38
|
+
exports.loadTeamFile = loadTeamFile;
|
|
39
|
+
exports.analyzeTeam = analyzeTeam;
|
|
40
|
+
exports.formatTeamInsights = formatTeamInsights;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const commitQualityAnalyzer_1 = require("./commitQualityAnalyzer");
|
|
43
|
+
/**
|
|
44
|
+
* Filters commits by author(s) based on the provided filter
|
|
45
|
+
*/
|
|
46
|
+
function filterCommitsByAuthor(commits, filter) {
|
|
47
|
+
return commits.filter(commit => {
|
|
48
|
+
const authorLower = commit.author.toLowerCase();
|
|
49
|
+
const matches = filter.authors.some(filterAuthor => {
|
|
50
|
+
const filterLower = filterAuthor.toLowerCase();
|
|
51
|
+
if (filter.matchType === 'exact') {
|
|
52
|
+
return authorLower === filterLower;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
return authorLower.includes(filterLower);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return filter.mode === 'include' ? matches : !matches;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Parse author filter from CLI options
|
|
63
|
+
*/
|
|
64
|
+
function parseAuthorFilter(author, authors, excludeAuthor, excludeAuthors) {
|
|
65
|
+
// Include filters take precedence
|
|
66
|
+
if (author) {
|
|
67
|
+
return {
|
|
68
|
+
mode: 'include',
|
|
69
|
+
authors: [author],
|
|
70
|
+
matchType: 'partial',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (authors) {
|
|
74
|
+
return {
|
|
75
|
+
mode: 'include',
|
|
76
|
+
authors: authors.split(',').map(a => a.trim()).filter(a => a.length > 0),
|
|
77
|
+
matchType: 'partial',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Exclude filters
|
|
81
|
+
if (excludeAuthor) {
|
|
82
|
+
return {
|
|
83
|
+
mode: 'exclude',
|
|
84
|
+
authors: [excludeAuthor],
|
|
85
|
+
matchType: 'partial',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (excludeAuthors) {
|
|
89
|
+
return {
|
|
90
|
+
mode: 'exclude',
|
|
91
|
+
authors: excludeAuthors.split(',').map(a => a.trim()).filter(a => a.length > 0),
|
|
92
|
+
matchType: 'partial',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Load team members from a file (one name per line)
|
|
99
|
+
*/
|
|
100
|
+
function loadTeamFile(filePath) {
|
|
101
|
+
try {
|
|
102
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
103
|
+
return content
|
|
104
|
+
.split('\n')
|
|
105
|
+
.map(line => line.trim())
|
|
106
|
+
.filter(line => line.length > 0 && !line.startsWith('#'));
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
throw new Error(`Failed to load team file: ${error.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Analyzes commits for a team of contributors
|
|
114
|
+
*/
|
|
115
|
+
function analyzeTeam(commits, teamMembers, teamName, skipBodyCheck = true) {
|
|
116
|
+
// Filter commits to team members only
|
|
117
|
+
const teamFilter = {
|
|
118
|
+
mode: 'include',
|
|
119
|
+
authors: teamMembers,
|
|
120
|
+
matchType: 'partial',
|
|
121
|
+
};
|
|
122
|
+
const teamCommits = filterCommitsByAuthor(commits, teamFilter);
|
|
123
|
+
// Calculate team metrics
|
|
124
|
+
const totalCommits = teamCommits.length;
|
|
125
|
+
const commitsPerMember = teamMembers.length > 0
|
|
126
|
+
? Math.round((totalCommits / teamMembers.length) * 10) / 10
|
|
127
|
+
: 0;
|
|
128
|
+
// Active days
|
|
129
|
+
const activeDatesSet = new Set(teamCommits.map(c => {
|
|
130
|
+
const d = new Date(c.date);
|
|
131
|
+
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
132
|
+
}));
|
|
133
|
+
const activeDays = activeDatesSet.size;
|
|
134
|
+
// Quality score
|
|
135
|
+
const qualityResult = (0, commitQualityAnalyzer_1.analyzeCommitQuality)(teamCommits, { skipBodyCheck });
|
|
136
|
+
const qualityScore = Math.round(qualityResult.overallScore * 10) / 10;
|
|
137
|
+
// Member breakdown
|
|
138
|
+
const memberBreakdown = calculateMemberBreakdown(teamCommits, teamMembers, skipBodyCheck);
|
|
139
|
+
// Load distribution
|
|
140
|
+
const loadDistribution = calculateLoadDistribution(memberBreakdown, totalCommits);
|
|
141
|
+
return {
|
|
142
|
+
teamName,
|
|
143
|
+
members: teamMembers,
|
|
144
|
+
teamMetrics: {
|
|
145
|
+
totalCommits,
|
|
146
|
+
commitsPerMember,
|
|
147
|
+
activeDays,
|
|
148
|
+
qualityScore,
|
|
149
|
+
},
|
|
150
|
+
memberBreakdown,
|
|
151
|
+
loadDistribution,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function calculateMemberBreakdown(commits, teamMembers, skipBodyCheck) {
|
|
155
|
+
const breakdown = [];
|
|
156
|
+
const totalCommits = commits.length;
|
|
157
|
+
// Build a map of author -> commits
|
|
158
|
+
const commitsByAuthor = new Map();
|
|
159
|
+
for (const commit of commits) {
|
|
160
|
+
const authorLower = commit.author.toLowerCase();
|
|
161
|
+
// Find the matching team member
|
|
162
|
+
const matchedMember = teamMembers.find(member => authorLower.includes(member.toLowerCase()));
|
|
163
|
+
if (matchedMember) {
|
|
164
|
+
if (!commitsByAuthor.has(matchedMember)) {
|
|
165
|
+
commitsByAuthor.set(matchedMember, []);
|
|
166
|
+
}
|
|
167
|
+
commitsByAuthor.get(matchedMember).push(commit);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Calculate stats for each team member
|
|
171
|
+
for (const member of teamMembers) {
|
|
172
|
+
const memberCommits = commitsByAuthor.get(member) || [];
|
|
173
|
+
const commitCount = memberCommits.length;
|
|
174
|
+
const percentage = totalCommits > 0
|
|
175
|
+
? Math.round((commitCount / totalCommits) * 1000) / 10
|
|
176
|
+
: 0;
|
|
177
|
+
// Active days for this member
|
|
178
|
+
const memberActiveDates = new Set(memberCommits.map(c => {
|
|
179
|
+
const d = new Date(c.date);
|
|
180
|
+
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
181
|
+
}));
|
|
182
|
+
// Quality score for this member
|
|
183
|
+
const qualityResult = memberCommits.length > 0
|
|
184
|
+
? (0, commitQualityAnalyzer_1.analyzeCommitQuality)(memberCommits, { skipBodyCheck })
|
|
185
|
+
: { overallScore: 0 };
|
|
186
|
+
// Top areas (directories)
|
|
187
|
+
const areaCounts = new Map();
|
|
188
|
+
for (const commit of memberCommits) {
|
|
189
|
+
// Extract directory from commit message if available (heuristic)
|
|
190
|
+
// In a real implementation, this would use file change data
|
|
191
|
+
const match = commit.message.match(/^(?:feat|fix|docs|refactor|test|chore)\(([^)]+)\)/i);
|
|
192
|
+
if (match) {
|
|
193
|
+
const area = match[1];
|
|
194
|
+
areaCounts.set(area, (areaCounts.get(area) || 0) + 1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const topAreas = [...areaCounts.entries()]
|
|
198
|
+
.sort((a, b) => b[1] - a[1])
|
|
199
|
+
.slice(0, 3)
|
|
200
|
+
.map(([area]) => area);
|
|
201
|
+
breakdown.push({
|
|
202
|
+
author: member,
|
|
203
|
+
commits: commitCount,
|
|
204
|
+
percentage,
|
|
205
|
+
activeDays: memberActiveDates.size,
|
|
206
|
+
qualityScore: Math.round(qualityResult.overallScore * 10) / 10,
|
|
207
|
+
topAreas,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// Sort by commits descending
|
|
211
|
+
breakdown.sort((a, b) => b.commits - a.commits);
|
|
212
|
+
return breakdown;
|
|
213
|
+
}
|
|
214
|
+
function calculateLoadDistribution(memberBreakdown, totalCommits) {
|
|
215
|
+
const insights = [];
|
|
216
|
+
if (memberBreakdown.length === 0 || totalCommits === 0) {
|
|
217
|
+
return {
|
|
218
|
+
giniCoefficient: 0,
|
|
219
|
+
assessment: 'balanced',
|
|
220
|
+
insights: ['No team data available'],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// Calculate Gini coefficient for commit distribution
|
|
224
|
+
const commitCounts = memberBreakdown.map(m => m.commits).sort((a, b) => a - b);
|
|
225
|
+
const n = commitCounts.length;
|
|
226
|
+
let giniNumerator = 0;
|
|
227
|
+
for (let i = 0; i < n; i++) {
|
|
228
|
+
giniNumerator += (2 * (i + 1) - n - 1) * commitCounts[i];
|
|
229
|
+
}
|
|
230
|
+
const giniCoefficient = n > 1
|
|
231
|
+
? Math.round((giniNumerator / (n * totalCommits)) * 100) / 100
|
|
232
|
+
: 0;
|
|
233
|
+
// Determine assessment
|
|
234
|
+
let assessment;
|
|
235
|
+
if (giniCoefficient <= 0.3) {
|
|
236
|
+
assessment = 'balanced';
|
|
237
|
+
insights.push('Work is distributed fairly evenly across team members');
|
|
238
|
+
}
|
|
239
|
+
else if (giniCoefficient <= 0.5) {
|
|
240
|
+
assessment = 'moderate-imbalance';
|
|
241
|
+
insights.push('Some team members are contributing significantly more than others');
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
assessment = 'high-imbalance';
|
|
245
|
+
insights.push('Work is heavily concentrated among a few team members');
|
|
246
|
+
}
|
|
247
|
+
// Additional insights
|
|
248
|
+
const topContributor = memberBreakdown[0];
|
|
249
|
+
if (topContributor && topContributor.percentage > 50) {
|
|
250
|
+
insights.push(`${topContributor.author} accounts for ${topContributor.percentage}% of all commits`);
|
|
251
|
+
}
|
|
252
|
+
const inactiveMembers = memberBreakdown.filter(m => m.commits === 0);
|
|
253
|
+
if (inactiveMembers.length > 0) {
|
|
254
|
+
insights.push(`${inactiveMembers.length} team member(s) have no commits in this period`);
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
giniCoefficient,
|
|
258
|
+
assessment,
|
|
259
|
+
insights,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Format team analysis for display
|
|
264
|
+
*/
|
|
265
|
+
function formatTeamInsights(analysis) {
|
|
266
|
+
const insights = [];
|
|
267
|
+
const teamLabel = analysis.teamName || 'Team';
|
|
268
|
+
insights.push(`👥 ${teamLabel} Analysis (${analysis.members.length} members)`);
|
|
269
|
+
insights.push('');
|
|
270
|
+
// Team metrics
|
|
271
|
+
insights.push(`📊 Total commits: ${analysis.teamMetrics.totalCommits}`);
|
|
272
|
+
insights.push(`📅 Active days: ${analysis.teamMetrics.activeDays}`);
|
|
273
|
+
insights.push(`⚡ Avg commits/member: ${analysis.teamMetrics.commitsPerMember}`);
|
|
274
|
+
insights.push(`✨ Quality score: ${analysis.teamMetrics.qualityScore}/10`);
|
|
275
|
+
insights.push('');
|
|
276
|
+
// Load distribution
|
|
277
|
+
const loadEmoji = {
|
|
278
|
+
'balanced': '🟢',
|
|
279
|
+
'moderate-imbalance': '🟡',
|
|
280
|
+
'high-imbalance': '🔴',
|
|
281
|
+
}[analysis.loadDistribution.assessment];
|
|
282
|
+
insights.push(`${loadEmoji} Load distribution: ${analysis.loadDistribution.assessment} (Gini: ${analysis.loadDistribution.giniCoefficient})`);
|
|
283
|
+
for (const insight of analysis.loadDistribution.insights) {
|
|
284
|
+
insights.push(` • ${insight}`);
|
|
285
|
+
}
|
|
286
|
+
insights.push('');
|
|
287
|
+
// Member breakdown (top 5)
|
|
288
|
+
insights.push('👤 Member breakdown:');
|
|
289
|
+
for (const member of analysis.memberBreakdown.slice(0, 5)) {
|
|
290
|
+
const areas = member.topAreas.length > 0 ? ` [${member.topAreas.join(', ')}]` : '';
|
|
291
|
+
insights.push(` • ${member.author}: ${member.commits} commits (${member.percentage}%)${areas}`);
|
|
292
|
+
}
|
|
293
|
+
if (analysis.memberBreakdown.length > 5) {
|
|
294
|
+
insights.push(` ... and ${analysis.memberBreakdown.length - 5} more`);
|
|
295
|
+
}
|
|
296
|
+
return insights;
|
|
297
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyzeVelocity = analyzeVelocity;
|
|
4
|
+
exports.formatVelocityInsights = formatVelocityInsights;
|
|
5
|
+
const date_fns_1 = require("date-fns");
|
|
6
|
+
/**
|
|
7
|
+
* Analyzes commit velocity over time to show productivity trends
|
|
8
|
+
*/
|
|
9
|
+
function analyzeVelocity(commits, startDate, endDate, rollingWindowWeeks = 4) {
|
|
10
|
+
if (commits.length === 0) {
|
|
11
|
+
return getEmptyVelocityAnalysis();
|
|
12
|
+
}
|
|
13
|
+
// Build weekly data points
|
|
14
|
+
const timeline = buildWeeklyTimeline(commits, startDate, endDate);
|
|
15
|
+
// Calculate rolling averages
|
|
16
|
+
calculateRollingAverages(timeline, rollingWindowWeeks);
|
|
17
|
+
// Detect anomalies
|
|
18
|
+
const anomalies = detectAnomalies(timeline);
|
|
19
|
+
// Calculate overall trend
|
|
20
|
+
const { trend, trendPercentage } = calculateTrend(timeline);
|
|
21
|
+
// Find peak and lowest weeks
|
|
22
|
+
const peakWeek = findPeakWeek(timeline);
|
|
23
|
+
const lowestWeek = findLowestWeek(timeline);
|
|
24
|
+
// Calculate average
|
|
25
|
+
const totalCommits = timeline.reduce((sum, dp) => sum + dp.commits, 0);
|
|
26
|
+
const averageCommitsPerWeek = timeline.length > 0 ? totalCommits / timeline.length : 0;
|
|
27
|
+
return {
|
|
28
|
+
timeline,
|
|
29
|
+
overallTrend: trend,
|
|
30
|
+
trendPercentage,
|
|
31
|
+
averageCommitsPerWeek: Math.round(averageCommitsPerWeek * 10) / 10,
|
|
32
|
+
peakWeek,
|
|
33
|
+
lowestWeek,
|
|
34
|
+
anomalies,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function buildWeeklyTimeline(commits, startDate, endDate) {
|
|
38
|
+
const timeline = [];
|
|
39
|
+
const weekCount = (0, date_fns_1.differenceInWeeks)(endDate, startDate) + 1;
|
|
40
|
+
// Create a map for quick commit lookup by week
|
|
41
|
+
const commitsByWeek = new Map();
|
|
42
|
+
for (const commit of commits) {
|
|
43
|
+
const commitDate = new Date(commit.date);
|
|
44
|
+
const weekStart = (0, date_fns_1.startOfWeek)(commitDate, { weekStartsOn: 1 });
|
|
45
|
+
const weekKey = (0, date_fns_1.format)(weekStart, 'yyyy-MM-dd');
|
|
46
|
+
if (!commitsByWeek.has(weekKey)) {
|
|
47
|
+
commitsByWeek.set(weekKey, []);
|
|
48
|
+
}
|
|
49
|
+
commitsByWeek.get(weekKey).push(commit);
|
|
50
|
+
}
|
|
51
|
+
// Build timeline for each week
|
|
52
|
+
let currentWeekStart = (0, date_fns_1.startOfWeek)(startDate, { weekStartsOn: 1 });
|
|
53
|
+
for (let i = 0; i < weekCount && currentWeekStart <= endDate; i++) {
|
|
54
|
+
const weekEnd = (0, date_fns_1.endOfWeek)(currentWeekStart, { weekStartsOn: 1 });
|
|
55
|
+
const weekKey = (0, date_fns_1.format)(currentWeekStart, 'yyyy-MM-dd');
|
|
56
|
+
const weekCommits = commitsByWeek.get(weekKey) || [];
|
|
57
|
+
const authors = [...new Set(weekCommits.map(c => c.author))];
|
|
58
|
+
// For now, we don't have line stats in CommitData, so we'll use 0
|
|
59
|
+
// This could be enhanced later by parsing git diff stats
|
|
60
|
+
timeline.push({
|
|
61
|
+
weekStart: new Date(currentWeekStart),
|
|
62
|
+
weekEnd: new Date(weekEnd),
|
|
63
|
+
commits: weekCommits.length,
|
|
64
|
+
linesAdded: 0,
|
|
65
|
+
linesDeleted: 0,
|
|
66
|
+
filesChanged: 0,
|
|
67
|
+
authors,
|
|
68
|
+
rollingAverage: 0, // Will be calculated later
|
|
69
|
+
});
|
|
70
|
+
currentWeekStart = (0, date_fns_1.addWeeks)(currentWeekStart, 1);
|
|
71
|
+
}
|
|
72
|
+
return timeline;
|
|
73
|
+
}
|
|
74
|
+
function calculateRollingAverages(timeline, windowWeeks) {
|
|
75
|
+
for (let i = 0; i < timeline.length; i++) {
|
|
76
|
+
const windowStart = Math.max(0, i - windowWeeks + 1);
|
|
77
|
+
const windowEnd = i + 1;
|
|
78
|
+
const window = timeline.slice(windowStart, windowEnd);
|
|
79
|
+
const sum = window.reduce((acc, dp) => acc + dp.commits, 0);
|
|
80
|
+
timeline[i].rollingAverage = Math.round((sum / window.length) * 10) / 10;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function detectAnomalies(timeline) {
|
|
84
|
+
const anomalies = [];
|
|
85
|
+
if (timeline.length < 3) {
|
|
86
|
+
return anomalies;
|
|
87
|
+
}
|
|
88
|
+
for (let i = 1; i < timeline.length; i++) {
|
|
89
|
+
const current = timeline[i];
|
|
90
|
+
const rollingAvg = timeline[i - 1].rollingAverage;
|
|
91
|
+
if (rollingAvg === 0) {
|
|
92
|
+
if (current.commits === 0) {
|
|
93
|
+
anomalies.push({
|
|
94
|
+
weekStart: current.weekStart,
|
|
95
|
+
type: 'zero-activity',
|
|
96
|
+
severity: 'significant',
|
|
97
|
+
percentageChange: -100,
|
|
98
|
+
possibleCauses: ['Holiday period', 'Team blocked', 'Sprint planning', 'Vacation'],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const percentageChange = ((current.commits - rollingAvg) / rollingAvg) * 100;
|
|
104
|
+
// Detect drops
|
|
105
|
+
if (percentageChange <= -60) {
|
|
106
|
+
anomalies.push({
|
|
107
|
+
weekStart: current.weekStart,
|
|
108
|
+
type: 'drop',
|
|
109
|
+
severity: 'critical',
|
|
110
|
+
percentageChange: Math.round(percentageChange),
|
|
111
|
+
possibleCauses: ['Major blocker', 'Team disruption', 'Holiday/vacation', 'Context switching'],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
else if (percentageChange <= -40) {
|
|
115
|
+
anomalies.push({
|
|
116
|
+
weekStart: current.weekStart,
|
|
117
|
+
type: 'drop',
|
|
118
|
+
severity: 'significant',
|
|
119
|
+
percentageChange: Math.round(percentageChange),
|
|
120
|
+
possibleCauses: ['Potential blocker', 'Reduced capacity', 'Complex work'],
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else if (percentageChange <= -20) {
|
|
124
|
+
anomalies.push({
|
|
125
|
+
weekStart: current.weekStart,
|
|
126
|
+
type: 'drop',
|
|
127
|
+
severity: 'minor',
|
|
128
|
+
percentageChange: Math.round(percentageChange),
|
|
129
|
+
possibleCauses: ['Normal variance', 'Focus on larger tasks'],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
// Detect spikes
|
|
133
|
+
if (percentageChange >= 100) {
|
|
134
|
+
anomalies.push({
|
|
135
|
+
weekStart: current.weekStart,
|
|
136
|
+
type: 'spike',
|
|
137
|
+
severity: 'significant',
|
|
138
|
+
percentageChange: Math.round(percentageChange),
|
|
139
|
+
possibleCauses: ['Release push', 'Catch-up after blocker', 'New team members'],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else if (percentageChange >= 50) {
|
|
143
|
+
anomalies.push({
|
|
144
|
+
weekStart: current.weekStart,
|
|
145
|
+
type: 'spike',
|
|
146
|
+
severity: 'minor',
|
|
147
|
+
percentageChange: Math.round(percentageChange),
|
|
148
|
+
possibleCauses: ['Productive week', 'Smaller tasks'],
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// Zero activity detection
|
|
152
|
+
if (current.commits === 0) {
|
|
153
|
+
anomalies.push({
|
|
154
|
+
weekStart: current.weekStart,
|
|
155
|
+
type: 'zero-activity',
|
|
156
|
+
severity: 'critical',
|
|
157
|
+
percentageChange: -100,
|
|
158
|
+
possibleCauses: ['Holiday period', 'Team blocked', 'Vacation', 'Sprint transition'],
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return anomalies;
|
|
163
|
+
}
|
|
164
|
+
function calculateTrend(timeline) {
|
|
165
|
+
if (timeline.length < 4) {
|
|
166
|
+
return { trend: 'stable', trendPercentage: 0 };
|
|
167
|
+
}
|
|
168
|
+
// Compare first quarter to last quarter of the timeline
|
|
169
|
+
const quarterSize = Math.max(1, Math.floor(timeline.length / 4));
|
|
170
|
+
const firstQuarter = timeline.slice(0, quarterSize);
|
|
171
|
+
const lastQuarter = timeline.slice(-quarterSize);
|
|
172
|
+
const firstAvg = firstQuarter.reduce((sum, dp) => sum + dp.commits, 0) / firstQuarter.length;
|
|
173
|
+
const lastAvg = lastQuarter.reduce((sum, dp) => sum + dp.commits, 0) / lastQuarter.length;
|
|
174
|
+
if (firstAvg === 0) {
|
|
175
|
+
return { trend: lastAvg > 0 ? 'increasing' : 'stable', trendPercentage: 100 };
|
|
176
|
+
}
|
|
177
|
+
const percentageChange = ((lastAvg - firstAvg) / firstAvg) * 100;
|
|
178
|
+
// Check for volatility (high variance)
|
|
179
|
+
const allCommits = timeline.map(dp => dp.commits);
|
|
180
|
+
const mean = allCommits.reduce((a, b) => a + b, 0) / allCommits.length;
|
|
181
|
+
const variance = allCommits.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / allCommits.length;
|
|
182
|
+
const coefficientOfVariation = mean > 0 ? (Math.sqrt(variance) / mean) * 100 : 0;
|
|
183
|
+
if (coefficientOfVariation > 80) {
|
|
184
|
+
return { trend: 'volatile', trendPercentage: Math.round(percentageChange) };
|
|
185
|
+
}
|
|
186
|
+
if (percentageChange > 15) {
|
|
187
|
+
return { trend: 'increasing', trendPercentage: Math.round(percentageChange) };
|
|
188
|
+
}
|
|
189
|
+
else if (percentageChange < -15) {
|
|
190
|
+
return { trend: 'decreasing', trendPercentage: Math.round(percentageChange) };
|
|
191
|
+
}
|
|
192
|
+
return { trend: 'stable', trendPercentage: Math.round(percentageChange) };
|
|
193
|
+
}
|
|
194
|
+
function findPeakWeek(timeline) {
|
|
195
|
+
if (timeline.length === 0) {
|
|
196
|
+
return { weekStart: new Date(), commits: 0 };
|
|
197
|
+
}
|
|
198
|
+
const peak = timeline.reduce((max, dp) => dp.commits > max.commits ? dp : max, timeline[0]);
|
|
199
|
+
return { weekStart: peak.weekStart, commits: peak.commits };
|
|
200
|
+
}
|
|
201
|
+
function findLowestWeek(timeline) {
|
|
202
|
+
if (timeline.length === 0) {
|
|
203
|
+
return { weekStart: new Date(), commits: 0 };
|
|
204
|
+
}
|
|
205
|
+
// Find lowest non-zero week, or zero if all are zero
|
|
206
|
+
const nonZeroWeeks = timeline.filter(dp => dp.commits > 0);
|
|
207
|
+
if (nonZeroWeeks.length === 0) {
|
|
208
|
+
return { weekStart: timeline[0].weekStart, commits: 0 };
|
|
209
|
+
}
|
|
210
|
+
const lowest = nonZeroWeeks.reduce((min, dp) => dp.commits < min.commits ? dp : min, nonZeroWeeks[0]);
|
|
211
|
+
return { weekStart: lowest.weekStart, commits: lowest.commits };
|
|
212
|
+
}
|
|
213
|
+
function getEmptyVelocityAnalysis() {
|
|
214
|
+
return {
|
|
215
|
+
timeline: [],
|
|
216
|
+
overallTrend: 'stable',
|
|
217
|
+
trendPercentage: 0,
|
|
218
|
+
averageCommitsPerWeek: 0,
|
|
219
|
+
peakWeek: { weekStart: new Date(), commits: 0 },
|
|
220
|
+
lowestWeek: { weekStart: new Date(), commits: 0 },
|
|
221
|
+
anomalies: [],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Format velocity analysis for display
|
|
226
|
+
*/
|
|
227
|
+
function formatVelocityInsights(analysis) {
|
|
228
|
+
const insights = [];
|
|
229
|
+
if (analysis.timeline.length === 0) {
|
|
230
|
+
return ['No velocity data available'];
|
|
231
|
+
}
|
|
232
|
+
// Trend insight
|
|
233
|
+
const trendDirection = analysis.trendPercentage >= 0 ? 'up' : 'down';
|
|
234
|
+
const trendEmoji = analysis.overallTrend === 'increasing' ? '📈' :
|
|
235
|
+
analysis.overallTrend === 'decreasing' ? '📉' :
|
|
236
|
+
analysis.overallTrend === 'volatile' ? '📊' : '➡️';
|
|
237
|
+
insights.push(`${trendEmoji} Velocity trend: ${analysis.overallTrend} (${trendDirection === 'up' ? '+' : ''}${analysis.trendPercentage}%)`);
|
|
238
|
+
insights.push(`📊 Average: ${analysis.averageCommitsPerWeek} commits/week`);
|
|
239
|
+
// Peak week
|
|
240
|
+
const peakDate = (0, date_fns_1.format)(analysis.peakWeek.weekStart, 'MMM d, yyyy');
|
|
241
|
+
insights.push(`🚀 Peak week: ${peakDate} (${analysis.peakWeek.commits} commits)`);
|
|
242
|
+
// Anomalies summary
|
|
243
|
+
const criticalDrops = analysis.anomalies.filter(a => a.type === 'drop' && a.severity === 'critical');
|
|
244
|
+
const zeroWeeks = analysis.anomalies.filter(a => a.type === 'zero-activity');
|
|
245
|
+
if (criticalDrops.length > 0) {
|
|
246
|
+
insights.push(`⚠️ ${criticalDrops.length} critical productivity drop(s) detected`);
|
|
247
|
+
}
|
|
248
|
+
if (zeroWeeks.length > 0) {
|
|
249
|
+
insights.push(`🚫 ${zeroWeeks.length} week(s) with zero activity`);
|
|
250
|
+
}
|
|
251
|
+
return insights;
|
|
252
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "repo-wrapped",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "A tool to generate Git repository analytics and visualizations in CLI or HTML.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -13,7 +13,16 @@
|
|
|
13
13
|
"release:major": "npm version major && npm publish",
|
|
14
14
|
"release:dry": "npm publish --dry-run",
|
|
15
15
|
"preversion": "npm run build",
|
|
16
|
-
"postversion": "git push && git push --tags"
|
|
16
|
+
"postversion": "git push && git push --tags",
|
|
17
|
+
"debug:html": "npm run build && node dist/index.js generate . --html",
|
|
18
|
+
"debug:html:all": "npm run build && node dist/index.js generate . --html --all",
|
|
19
|
+
"debug:html:deep": "npm run build && node dist/index.js generate . --html --deep-analysis",
|
|
20
|
+
"debug:html:velocity": "npm run build && node dist/index.js generate . --html --velocity",
|
|
21
|
+
"debug:html:gaps": "npm run build && node dist/index.js generate . --html --gap-analysis",
|
|
22
|
+
"debug:html:executive": "npm run build && node dist/index.js generate . --html --executive-summary",
|
|
23
|
+
"debug:html:compare": "npm run build && node dist/index.js generate . --html --compare 2025-01-01..2025-06-30 2025-07-01..2025-12-31 --compare-labels H1,H2",
|
|
24
|
+
"debug:html:strategic": "npm run build && node dist/index.js generate . --html --velocity --gap-analysis --executive-summary",
|
|
25
|
+
"debug:html:full": "npm run build && node dist/index.js generate . --html --all --deep-analysis --velocity --gap-analysis --executive-summary"
|
|
17
26
|
},
|
|
18
27
|
"bin": {
|
|
19
28
|
"repo-wrapped": "dist/index.js"
|
package/test-team.txt
ADDED