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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateSummaryOnlyHTML = generateSummaryOnlyHTML;
|
|
3
4
|
exports.generateHTML = generateHTML;
|
|
4
5
|
const date_fns_1 = require("date-fns");
|
|
5
6
|
const achievementEngine_1 = require("./achievementEngine");
|
|
@@ -18,6 +19,12 @@ const commitQualitySection_1 = require("../generators/html/templates/commitQuali
|
|
|
18
19
|
const achievementsSection_1 = require("../generators/html/templates/achievementsSection");
|
|
19
20
|
const impactSection_1 = require("../generators/html/templates/impactSection");
|
|
20
21
|
const knowledgeSection_1 = require("../generators/html/templates/knowledgeSection");
|
|
22
|
+
const velocitySection_1 = require("../generators/html/templates/velocitySection");
|
|
23
|
+
const gapSection_1 = require("../generators/html/templates/gapSection");
|
|
24
|
+
const comparisonSection_1 = require("../generators/html/templates/comparisonSection");
|
|
25
|
+
const executiveSummarySection_1 = require("../generators/html/templates/executiveSummarySection");
|
|
26
|
+
const teamSection_1 = require("../generators/html/templates/teamSection");
|
|
27
|
+
const eventsSection_1 = require("../generators/html/templates/eventsSection");
|
|
21
28
|
const commitMapBuilder_1 = require("../generators/html/utils/commitMapBuilder");
|
|
22
29
|
const developerStatsCalculator_1 = require("../generators/html/utils/developerStatsCalculator");
|
|
23
30
|
const dateRangeCalculator_1 = require("../generators/html/utils/dateRangeCalculator");
|
|
@@ -69,7 +76,82 @@ function wrapInSection(id, title, content, icon = '') {
|
|
|
69
76
|
</section>
|
|
70
77
|
`;
|
|
71
78
|
}
|
|
72
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Generate a minimal HTML page with only the executive summary
|
|
81
|
+
*/
|
|
82
|
+
function generateSummaryOnlyHTML(summary, repoName) {
|
|
83
|
+
return `<!DOCTYPE html>
|
|
84
|
+
<html lang="en">
|
|
85
|
+
<head>
|
|
86
|
+
<meta charset="UTF-8">
|
|
87
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
88
|
+
<title>Executive Summary - ${repoName}</title>
|
|
89
|
+
<style>
|
|
90
|
+
${(0, styleLoader_1.loadStyles)()}
|
|
91
|
+
|
|
92
|
+
/* Summary-only specific styles */
|
|
93
|
+
body.summary-only {
|
|
94
|
+
background: #0d1117;
|
|
95
|
+
color: #e6edf3;
|
|
96
|
+
min-height: 100vh;
|
|
97
|
+
padding: 2rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.summary-page {
|
|
101
|
+
max-width: 900px;
|
|
102
|
+
margin: 0 auto;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.summary-header {
|
|
106
|
+
text-align: center;
|
|
107
|
+
margin-bottom: 2rem;
|
|
108
|
+
padding-bottom: 1.5rem;
|
|
109
|
+
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.summary-header h1 {
|
|
113
|
+
font-size: 2rem;
|
|
114
|
+
margin-bottom: 0.5rem;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.summary-header .period {
|
|
118
|
+
color: #8b949e;
|
|
119
|
+
font-size: 1rem;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.summary-header .generated {
|
|
123
|
+
color: #6e7681;
|
|
124
|
+
font-size: 0.875rem;
|
|
125
|
+
margin-top: 0.5rem;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@media print {
|
|
129
|
+
body.summary-only {
|
|
130
|
+
background: white;
|
|
131
|
+
color: black;
|
|
132
|
+
padding: 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.summary-page {
|
|
136
|
+
max-width: 100%;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
140
|
+
</head>
|
|
141
|
+
<body class="summary-only">
|
|
142
|
+
<div class="summary-page">
|
|
143
|
+
<header class="summary-header">
|
|
144
|
+
<h1>📊 ${repoName}</h1>
|
|
145
|
+
<p class="period">${(0, date_fns_1.format)(summary.period.start, 'MMM d, yyyy')} – ${(0, date_fns_1.format)(summary.period.end, 'MMM d, yyyy')}</p>
|
|
146
|
+
<p class="generated">Generated ${(0, date_fns_1.format)(summary.generatedAt, 'MMM d, yyyy \'at\' h:mm a')}</p>
|
|
147
|
+
</header>
|
|
148
|
+
|
|
149
|
+
${(0, executiveSummarySection_1.buildExecutiveSummarySection)(summary)}
|
|
150
|
+
</div>
|
|
151
|
+
</body>
|
|
152
|
+
</html>`;
|
|
153
|
+
}
|
|
154
|
+
function generateHTML(commits, year, monthsToShow, repoPath, repoName, repoUrl, skipBodyCheck = false, allTime = false, deepAnalysis = false, leadDevOptions) {
|
|
73
155
|
// Get current git user
|
|
74
156
|
const { getCurrentGitUser } = require('./gitParser');
|
|
75
157
|
const currentUser = getCurrentGitUser(repoPath);
|
|
@@ -77,31 +159,36 @@ function generateHTML(commits, year, monthsToShow, repoPath, repoName, repoUrl,
|
|
|
77
159
|
const { startDate, endDate, weekStartDate, weekEndDate } = allTime
|
|
78
160
|
? (0, dateRangeCalculator_1.calculateDateRangesFromCommits)(commits)
|
|
79
161
|
: (0, dateRangeCalculator_1.calculateDateRanges)(year, monthsToShow);
|
|
162
|
+
// Filter commits to the date range
|
|
163
|
+
const filteredCommits = commits.filter(commit => {
|
|
164
|
+
const commitDate = new Date(commit.date);
|
|
165
|
+
return commitDate >= startDate && commitDate <= endDate;
|
|
166
|
+
});
|
|
80
167
|
// Create commit maps for overall and personal commits
|
|
81
|
-
const { commitMap, commitDetailsMap } = (0, commitMapBuilder_1.createCommitMaps)(
|
|
82
|
-
const personalCommits =
|
|
168
|
+
const { commitMap, commitDetailsMap } = (0, commitMapBuilder_1.createCommitMaps)(filteredCommits, startDate, endDate);
|
|
169
|
+
const personalCommits = filteredCommits.filter(c => c.author === currentUser);
|
|
83
170
|
const { commitMap: personalCommitMap, commitDetailsMap: personalCommitDetailsMap } = (0, commitMapBuilder_1.createCommitMaps)(personalCommits, startDate, endDate);
|
|
84
171
|
// Calculate per-developer statistics
|
|
85
|
-
const developerStats = (0, developerStatsCalculator_1.calculateDeveloperStats)(
|
|
172
|
+
const developerStats = (0, developerStatsCalculator_1.calculateDeveloperStats)(filteredCommits, startDate, endDate);
|
|
86
173
|
// Calculate streak data (overall and personal)
|
|
87
|
-
const streakData = (0, streakCalculator_1.calculateStreaks)(
|
|
174
|
+
const streakData = (0, streakCalculator_1.calculateStreaks)(filteredCommits, startDate, endDate);
|
|
88
175
|
const personalStreakData = (0, streakCalculator_1.calculateStreaks)(personalCommits, startDate, endDate);
|
|
89
176
|
// Analyze time patterns (overall and personal)
|
|
90
|
-
const timePattern = (0, timePatternAnalyzer_1.analyzeTimePatterns)(
|
|
177
|
+
const timePattern = (0, timePatternAnalyzer_1.analyzeTimePatterns)(filteredCommits, startDate, endDate);
|
|
91
178
|
const personalTimePattern = (0, timePatternAnalyzer_1.analyzeTimePatterns)(personalCommits, startDate, endDate);
|
|
92
179
|
// Analyze commit quality (overall and personal)
|
|
93
|
-
const commitQuality = (0, commitQualityAnalyzer_1.analyzeCommitQuality)(
|
|
180
|
+
const commitQuality = (0, commitQualityAnalyzer_1.analyzeCommitQuality)(filteredCommits, { skipBodyCheck });
|
|
94
181
|
const personalCommitQuality = (0, commitQualityAnalyzer_1.analyzeCommitQuality)(personalCommits, { skipBodyCheck });
|
|
95
182
|
// Analyze file hotspots
|
|
96
183
|
const fileHotspots = (0, fileHotspotAnalyzer_1.analyzeFileHotspots)(repoPath, startDate, endDate);
|
|
97
184
|
// Analyze impact and knowledge distribution (enterprise insights)
|
|
98
|
-
const impactAnalysis = (0, impactAnalyzer_1.analyzeImpact)(
|
|
99
|
-
const knowledgeDistribution = (0, knowledgeDistributionAnalyzer_1.analyzeKnowledgeDistribution)(
|
|
185
|
+
const impactAnalysis = (0, impactAnalyzer_1.analyzeImpact)(filteredCommits);
|
|
186
|
+
const knowledgeDistribution = (0, knowledgeDistributionAnalyzer_1.analyzeKnowledgeDistribution)(filteredCommits, repoPath, deepAnalysis);
|
|
100
187
|
const totalCommits = Array.from(commitMap.values()).reduce((sum, count) => sum + count, 0);
|
|
101
188
|
const totalPersonalCommits = Array.from(personalCommitMap.values()).reduce((sum, count) => sum + count, 0);
|
|
102
189
|
// Prepare analysis data for achievements (overall and personal)
|
|
103
190
|
const analysisData = {
|
|
104
|
-
commits,
|
|
191
|
+
commits: filteredCommits,
|
|
105
192
|
totalCommits,
|
|
106
193
|
streakData,
|
|
107
194
|
timePattern,
|
|
@@ -121,7 +208,7 @@ function generateHTML(commits, year, monthsToShow, repoPath, repoName, repoUrl,
|
|
|
121
208
|
// Check achievements
|
|
122
209
|
const achievementProgress = (0, achievementEngine_1.checkAchievements)(analysisData);
|
|
123
210
|
const personalAchievementProgress = (0, achievementEngine_1.checkAchievements)(personalAnalysisData);
|
|
124
|
-
return generateHTMLContent(commitMap, commitDetailsMap, developerStats, streakData, timePattern, commitQuality, fileHotspots, achievementProgress, personalCommitMap, personalCommitDetailsMap, personalStreakData, personalTimePattern, personalCommitQuality, personalAchievementProgress, weekStartDate, weekEndDate, startDate, endDate, repoName, repoUrl, currentUser, skipBodyCheck, impactAnalysis, knowledgeDistribution);
|
|
211
|
+
return generateHTMLContent(commitMap, commitDetailsMap, developerStats, streakData, timePattern, commitQuality, fileHotspots, achievementProgress, personalCommitMap, personalCommitDetailsMap, personalStreakData, personalTimePattern, personalCommitQuality, personalAchievementProgress, weekStartDate, weekEndDate, startDate, endDate, repoName, repoUrl, currentUser, skipBodyCheck, impactAnalysis, knowledgeDistribution, leadDevOptions);
|
|
125
212
|
}
|
|
126
213
|
function generatePersonalContent(personalCommitMap, personalCommitDetailsMap, streakData, timePattern, commitQuality, achievementProgress, weekStartDate, weekEndDate, dataStartDate, dataEndDate, currentUser, totalCommits, skipBodyCheck = false) {
|
|
127
214
|
const weeks = (0, weekGrouper_1.groupDaysIntoWeeks)(weekStartDate, weekEndDate);
|
|
@@ -155,7 +242,7 @@ function generatePersonalContent(personalCommitMap, personalCommitDetailsMap, st
|
|
|
155
242
|
${wrapInSection('personal-achievements', 'Achievements', achievementsHtml)}
|
|
156
243
|
`;
|
|
157
244
|
}
|
|
158
|
-
function generateHTMLContent(commitMap, commitDetailsMap, developerStats, streakData, timePattern, commitQuality, fileHotspots, achievementProgress, personalCommitMap, personalCommitDetailsMap, personalStreakData, personalTimePattern, personalCommitQuality, personalAchievementProgress, weekStartDate, weekEndDate, dataStartDate, dataEndDate, repoName, repoUrl, currentUser, skipBodyCheck = false, impactAnalysis, knowledgeDistribution) {
|
|
245
|
+
function generateHTMLContent(commitMap, commitDetailsMap, developerStats, streakData, timePattern, commitQuality, fileHotspots, achievementProgress, personalCommitMap, personalCommitDetailsMap, personalStreakData, personalTimePattern, personalCommitQuality, personalAchievementProgress, weekStartDate, weekEndDate, dataStartDate, dataEndDate, repoName, repoUrl, currentUser, skipBodyCheck = false, impactAnalysis, knowledgeDistribution, leadDevOptions) {
|
|
159
246
|
const totalPersonalCommits = Array.from(personalCommitMap.values()).reduce((sum, count) => sum + count, 0);
|
|
160
247
|
const weeks = (0, weekGrouper_1.groupDaysIntoWeeks)(weekStartDate, weekEndDate);
|
|
161
248
|
// Create dynamic color function based on commit distribution
|
|
@@ -307,32 +394,16 @@ function generateHTMLContent(commitMap, commitDetailsMap, developerStats, streak
|
|
|
307
394
|
<div class="nav-section">
|
|
308
395
|
<div class="nav-section-title">Views</div>
|
|
309
396
|
<a href="#" class="nav-item active" data-tab="overall">
|
|
310
|
-
<span class="nav-icon">📊</span>
|
|
311
397
|
<span class="nav-label">Team Overview</span>
|
|
312
398
|
</a>
|
|
313
399
|
<a href="#" class="nav-item" data-tab="personal">
|
|
314
|
-
<span class="nav-icon">👤</span>
|
|
315
400
|
<span class="nav-label">Personal Stats</span>
|
|
316
401
|
</a>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
<div class="nav-section-title">Quick Jump</div>
|
|
321
|
-
<a href="#contributions" class="nav-item nav-item-small">
|
|
322
|
-
<span class="nav-label">Contributions</span>
|
|
323
|
-
</a>
|
|
324
|
-
<a href="#personal-streaks" class="nav-item nav-item-small" data-tab="personal">
|
|
325
|
-
<span class="nav-label">Streaks</span>
|
|
326
|
-
</a>
|
|
327
|
-
<a href="#personal-patterns" class="nav-item nav-item-small" data-tab="personal">
|
|
328
|
-
<span class="nav-label">Patterns</span>
|
|
329
|
-
</a>
|
|
330
|
-
<a href="#personal-quality" class="nav-item nav-item-small" data-tab="personal">
|
|
331
|
-
<span class="nav-label">Quality</span>
|
|
332
|
-
</a>
|
|
333
|
-
<a href="#personal-achievements" class="nav-item nav-item-small" data-tab="personal">
|
|
334
|
-
<span class="nav-label">Achievements</span>
|
|
402
|
+
${leadDevOptions?.velocityAnalysis || leadDevOptions?.gapAnalysis || leadDevOptions?.rangeComparison || leadDevOptions?.executiveSummary || leadDevOptions?.teamAnalysis || leadDevOptions?.events?.length ? `
|
|
403
|
+
<a href="#" class="nav-item" data-tab="leaddev">
|
|
404
|
+
<span class="nav-label">Strategic Insights</span>
|
|
335
405
|
</a>
|
|
406
|
+
` : ''}
|
|
336
407
|
</div>
|
|
337
408
|
|
|
338
409
|
<div class="sidebar-footer">
|
|
@@ -373,6 +444,7 @@ function generateHTMLContent(commitMap, commitDetailsMap, developerStats, streak
|
|
|
373
444
|
<div class="tab-nav">
|
|
374
445
|
<button class="tab-button active" data-tab="overall">📊 Team Overview</button>
|
|
375
446
|
<button class="tab-button" data-tab="personal">👤 Personal</button>
|
|
447
|
+
${leadDevOptions?.velocityAnalysis || leadDevOptions?.gapAnalysis || leadDevOptions?.rangeComparison || leadDevOptions?.executiveSummary || leadDevOptions?.teamAnalysis ? '<button class="tab-button" data-tab="leaddev">📊 Strategic Insights</button>' : ''}
|
|
376
448
|
</div>
|
|
377
449
|
|
|
378
450
|
<!-- Overall Tab Content -->
|
|
@@ -386,6 +458,19 @@ function generateHTMLContent(commitMap, commitDetailsMap, developerStats, streak
|
|
|
386
458
|
</div>
|
|
387
459
|
<!-- End Overall Tab Content -->
|
|
388
460
|
|
|
461
|
+
<!-- Lead Developer Tab Content -->
|
|
462
|
+
${(leadDevOptions?.velocityAnalysis || leadDevOptions?.gapAnalysis || leadDevOptions?.rangeComparison || leadDevOptions?.executiveSummary || leadDevOptions?.teamAnalysis || leadDevOptions?.events?.length) ? `
|
|
463
|
+
<div class="tab-content" id="leaddev-content">
|
|
464
|
+
${leadDevOptions?.executiveSummary ? wrapInSection('executive-summary', 'Executive Summary', (0, executiveSummarySection_1.buildExecutiveSummarySection)(leadDevOptions.executiveSummary), '📋') : ''}
|
|
465
|
+
${leadDevOptions?.velocityAnalysis ? wrapInSection('velocity', 'Velocity Timeline', (0, velocitySection_1.buildVelocitySection)(leadDevOptions.velocityAnalysis), '📈') : ''}
|
|
466
|
+
${leadDevOptions?.gapAnalysis ? wrapInSection('gaps', 'Gap Analysis', (0, gapSection_1.buildGapSection)(leadDevOptions.gapAnalysis), '🚧') : ''}
|
|
467
|
+
${leadDevOptions?.rangeComparison ? wrapInSection('comparison', 'Period Comparison', (0, comparisonSection_1.buildComparisonSection)(leadDevOptions.rangeComparison), '⚖️') : ''}
|
|
468
|
+
${leadDevOptions?.teamAnalysis ? wrapInSection('team-analysis', 'Team Analysis', (0, teamSection_1.buildTeamSection)(leadDevOptions.teamAnalysis), '👥') : ''}
|
|
469
|
+
${leadDevOptions?.events?.length ? wrapInSection('events', 'Event Timeline', (0, eventsSection_1.buildEventsSection)(leadDevOptions.events), '📅') : ''}
|
|
470
|
+
</div>
|
|
471
|
+
` : ''}
|
|
472
|
+
<!-- End Lead Developer Tab Content -->
|
|
473
|
+
|
|
389
474
|
<!-- Personal Tab Content -->
|
|
390
475
|
<div class="tab-content" id="personal-content">
|
|
391
476
|
${generatePersonalContent(personalCommitMap, personalCommitDetailsMap, personalStreakData, personalTimePattern, personalCommitQuality, personalAchievementProgress, weekStartDate, weekEndDate, dataStartDate, dataEndDate, currentUser, totalPersonalCommits, skipBodyCheck)}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.compareRanges = compareRanges;
|
|
4
|
+
exports.parseDateRange = parseDateRange;
|
|
5
|
+
exports.formatComparisonInsights = formatComparisonInsights;
|
|
6
|
+
const date_fns_1 = require("date-fns");
|
|
7
|
+
const commitQualityAnalyzer_1 = require("./commitQualityAnalyzer");
|
|
8
|
+
const streakCalculator_1 = require("./streakCalculator");
|
|
9
|
+
/**
|
|
10
|
+
* Compares metrics between two date ranges to show before/after impact
|
|
11
|
+
*/
|
|
12
|
+
function compareRanges(commits, range1Start, range1End, range2Start, range2End, label1 = 'Period 1', label2 = 'Period 2', skipBodyCheck = true) {
|
|
13
|
+
// Filter commits for each range
|
|
14
|
+
const range1Commits = filterCommitsByDateRange(commits, range1Start, range1End);
|
|
15
|
+
const range2Commits = filterCommitsByDateRange(commits, range2Start, range2End);
|
|
16
|
+
// Calculate metrics for each range
|
|
17
|
+
const metrics1 = calculateRangeMetrics(range1Commits, range1Start, range1End, skipBodyCheck);
|
|
18
|
+
const metrics2 = calculateRangeMetrics(range2Commits, range2Start, range2End, skipBodyCheck);
|
|
19
|
+
// Calculate changes
|
|
20
|
+
const changes = calculateChanges(metrics1, metrics2);
|
|
21
|
+
// Generate summary narrative
|
|
22
|
+
const summary = generateSummary(label1, label2, metrics1, metrics2, changes);
|
|
23
|
+
return {
|
|
24
|
+
range1: {
|
|
25
|
+
label: label1,
|
|
26
|
+
start: range1Start,
|
|
27
|
+
end: range1End,
|
|
28
|
+
metrics: metrics1,
|
|
29
|
+
},
|
|
30
|
+
range2: {
|
|
31
|
+
label: label2,
|
|
32
|
+
start: range2Start,
|
|
33
|
+
end: range2End,
|
|
34
|
+
metrics: metrics2,
|
|
35
|
+
},
|
|
36
|
+
changes,
|
|
37
|
+
summary,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function filterCommitsByDateRange(commits, start, end) {
|
|
41
|
+
return commits.filter(commit => {
|
|
42
|
+
const commitDate = new Date(commit.date);
|
|
43
|
+
return commitDate >= start && commitDate <= end;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function calculateRangeMetrics(commits, startDate, endDate, skipBodyCheck) {
|
|
47
|
+
const totalDays = (0, date_fns_1.differenceInDays)(endDate, startDate) + 1;
|
|
48
|
+
const totalWeeks = Math.max(1, (0, date_fns_1.differenceInWeeks)(endDate, startDate) + 1);
|
|
49
|
+
// Basic counts
|
|
50
|
+
const totalCommits = commits.length;
|
|
51
|
+
const commitsPerWeek = Math.round((totalCommits / totalWeeks) * 10) / 10;
|
|
52
|
+
// Unique authors
|
|
53
|
+
const authors = [...new Set(commits.map(c => c.author))];
|
|
54
|
+
const totalAuthors = authors.length;
|
|
55
|
+
// Active days
|
|
56
|
+
const activeDatesSet = new Set(commits.map(c => {
|
|
57
|
+
const d = new Date(c.date);
|
|
58
|
+
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
59
|
+
}));
|
|
60
|
+
const activeDays = activeDatesSet.size;
|
|
61
|
+
const activeDaysPercentage = Math.round((activeDays / totalDays) * 1000) / 10;
|
|
62
|
+
const averageCommitsPerDay = activeDays > 0 ? Math.round((totalCommits / activeDays) * 10) / 10 : 0;
|
|
63
|
+
// Quality score
|
|
64
|
+
const qualityResult = (0, commitQualityAnalyzer_1.analyzeCommitQuality)(commits, { skipBodyCheck });
|
|
65
|
+
const qualityScore = Math.round(qualityResult.overallScore * 10) / 10;
|
|
66
|
+
// Streak data
|
|
67
|
+
const streakData = (0, streakCalculator_1.calculateStreaks)(commits, startDate, endDate);
|
|
68
|
+
const longestStreak = streakData.longestStreak.days;
|
|
69
|
+
const averageStreak = streakData.streaks.length > 0
|
|
70
|
+
? Math.round(streakData.streaks.reduce((sum, s) => sum + s.days, 0) / streakData.streaks.length * 10) / 10
|
|
71
|
+
: 0;
|
|
72
|
+
// Commits by type (from conventional commits)
|
|
73
|
+
const commitsByType = {
|
|
74
|
+
feat: 0,
|
|
75
|
+
fix: 0,
|
|
76
|
+
docs: 0,
|
|
77
|
+
refactor: 0,
|
|
78
|
+
test: 0,
|
|
79
|
+
chore: 0,
|
|
80
|
+
other: 0,
|
|
81
|
+
};
|
|
82
|
+
for (const commit of commits) {
|
|
83
|
+
const match = commit.message.match(/^(feat|fix|docs|refactor|test|chore|style|perf|ci|build|revert)(\(.+\))?:/i);
|
|
84
|
+
if (match) {
|
|
85
|
+
const type = match[1].toLowerCase();
|
|
86
|
+
if (type in commitsByType) {
|
|
87
|
+
commitsByType[type]++;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
commitsByType.other++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
commitsByType.other++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Commits by author
|
|
98
|
+
const commitsByAuthor = {};
|
|
99
|
+
for (const commit of commits) {
|
|
100
|
+
commitsByAuthor[commit.author] = (commitsByAuthor[commit.author] || 0) + 1;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
totalCommits,
|
|
104
|
+
commitsPerWeek,
|
|
105
|
+
totalAuthors,
|
|
106
|
+
activeDays,
|
|
107
|
+
activeDaysPercentage,
|
|
108
|
+
averageCommitsPerDay,
|
|
109
|
+
qualityScore,
|
|
110
|
+
longestStreak,
|
|
111
|
+
averageStreak,
|
|
112
|
+
commitsByType,
|
|
113
|
+
commitsByAuthor,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function calculateChanges(metrics1, metrics2) {
|
|
117
|
+
return {
|
|
118
|
+
commits: calculateMetricChange(metrics1.totalCommits, metrics2.totalCommits),
|
|
119
|
+
velocity: calculateMetricChange(metrics1.commitsPerWeek, metrics2.commitsPerWeek),
|
|
120
|
+
quality: calculateMetricChange(metrics1.qualityScore, metrics2.qualityScore),
|
|
121
|
+
activeDays: calculateMetricChange(metrics1.activeDays, metrics2.activeDays),
|
|
122
|
+
authors: calculateMetricChange(metrics1.totalAuthors, metrics2.totalAuthors),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function calculateMetricChange(value1, value2) {
|
|
126
|
+
const absolute = Math.round((value2 - value1) * 10) / 10;
|
|
127
|
+
const percentage = value1 !== 0
|
|
128
|
+
? Math.round(((value2 - value1) / value1) * 1000) / 10
|
|
129
|
+
: (value2 > 0 ? 100 : 0);
|
|
130
|
+
let trend;
|
|
131
|
+
if (percentage > 5) {
|
|
132
|
+
trend = 'up';
|
|
133
|
+
}
|
|
134
|
+
else if (percentage < -5) {
|
|
135
|
+
trend = 'down';
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
trend = 'stable';
|
|
139
|
+
}
|
|
140
|
+
return { absolute, percentage, trend };
|
|
141
|
+
}
|
|
142
|
+
function generateSummary(label1, label2, metrics1, metrics2, changes) {
|
|
143
|
+
const parts = [];
|
|
144
|
+
// Velocity change
|
|
145
|
+
if (changes.velocity.trend === 'down' && changes.velocity.percentage <= -20) {
|
|
146
|
+
parts.push(`Velocity decreased ${Math.abs(changes.velocity.percentage)}% in ${label2}`);
|
|
147
|
+
}
|
|
148
|
+
else if (changes.velocity.trend === 'up' && changes.velocity.percentage >= 20) {
|
|
149
|
+
parts.push(`Velocity increased ${changes.velocity.percentage}% in ${label2}`);
|
|
150
|
+
}
|
|
151
|
+
// Active days change
|
|
152
|
+
if (changes.activeDays.trend === 'down' && changes.activeDays.percentage <= -20) {
|
|
153
|
+
parts.push(`${Math.abs(changes.activeDays.percentage)}% fewer active days`);
|
|
154
|
+
}
|
|
155
|
+
else if (changes.activeDays.trend === 'up' && changes.activeDays.percentage >= 20) {
|
|
156
|
+
parts.push(`${changes.activeDays.percentage}% more active days`);
|
|
157
|
+
}
|
|
158
|
+
// Quality change
|
|
159
|
+
if (changes.quality.trend === 'down' && changes.quality.percentage <= -15) {
|
|
160
|
+
parts.push(`commit quality dropped ${Math.abs(changes.quality.percentage)}%`);
|
|
161
|
+
}
|
|
162
|
+
else if (changes.quality.trend === 'up' && changes.quality.percentage >= 15) {
|
|
163
|
+
parts.push(`commit quality improved ${changes.quality.percentage}%`);
|
|
164
|
+
}
|
|
165
|
+
// Author change
|
|
166
|
+
if (changes.authors.trend === 'down' && changes.authors.absolute < 0) {
|
|
167
|
+
parts.push(`${Math.abs(changes.authors.absolute)} fewer contributor(s)`);
|
|
168
|
+
}
|
|
169
|
+
else if (changes.authors.trend === 'up' && changes.authors.absolute > 0) {
|
|
170
|
+
parts.push(`${changes.authors.absolute} additional contributor(s)`);
|
|
171
|
+
}
|
|
172
|
+
if (parts.length === 0) {
|
|
173
|
+
return `Metrics remained relatively stable between ${label1} and ${label2}.`;
|
|
174
|
+
}
|
|
175
|
+
return parts.join(', ') + '.';
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Parse date range string in format "YYYY-MM-DD..YYYY-MM-DD"
|
|
179
|
+
*/
|
|
180
|
+
function parseDateRange(rangeStr) {
|
|
181
|
+
const match = rangeStr.match(/^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/);
|
|
182
|
+
if (!match) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const start = new Date(match[1]);
|
|
186
|
+
const end = new Date(match[2]);
|
|
187
|
+
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
return { start, end };
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Format comparison for display
|
|
194
|
+
*/
|
|
195
|
+
function formatComparisonInsights(comparison) {
|
|
196
|
+
const insights = [];
|
|
197
|
+
const { range1, range2, changes } = comparison;
|
|
198
|
+
insights.push(`📊 Comparing: ${range1.label} vs ${range2.label}`);
|
|
199
|
+
insights.push('');
|
|
200
|
+
// Commits
|
|
201
|
+
const commitEmoji = changes.commits.trend === 'up' ? '📈' : changes.commits.trend === 'down' ? '📉' : '➡️';
|
|
202
|
+
insights.push(`${commitEmoji} Commits: ${range1.metrics.totalCommits} → ${range2.metrics.totalCommits} (${formatChange(changes.commits)})`);
|
|
203
|
+
// Velocity
|
|
204
|
+
const velocityEmoji = changes.velocity.trend === 'up' ? '🚀' : changes.velocity.trend === 'down' ? '🐢' : '➡️';
|
|
205
|
+
insights.push(`${velocityEmoji} Velocity: ${range1.metrics.commitsPerWeek} → ${range2.metrics.commitsPerWeek}/week (${formatChange(changes.velocity)})`);
|
|
206
|
+
// Quality
|
|
207
|
+
const qualityEmoji = changes.quality.trend === 'up' ? '✨' : changes.quality.trend === 'down' ? '⚠️' : '➡️';
|
|
208
|
+
insights.push(`${qualityEmoji} Quality: ${range1.metrics.qualityScore} → ${range2.metrics.qualityScore}/10 (${formatChange(changes.quality)})`);
|
|
209
|
+
// Active days
|
|
210
|
+
const activeEmoji = changes.activeDays.trend === 'up' ? '📅' : changes.activeDays.trend === 'down' ? '🗓️' : '➡️';
|
|
211
|
+
insights.push(`${activeEmoji} Active days: ${range1.metrics.activeDays} → ${range2.metrics.activeDays} (${formatChange(changes.activeDays)})`);
|
|
212
|
+
// Contributors
|
|
213
|
+
const authorEmoji = changes.authors.trend === 'up' ? '👥' : changes.authors.trend === 'down' ? '👤' : '➡️';
|
|
214
|
+
insights.push(`${authorEmoji} Contributors: ${range1.metrics.totalAuthors} → ${range2.metrics.totalAuthors} (${formatChange(changes.authors)})`);
|
|
215
|
+
insights.push('');
|
|
216
|
+
insights.push(`💡 ${comparison.summary}`);
|
|
217
|
+
return insights;
|
|
218
|
+
}
|
|
219
|
+
function formatChange(change) {
|
|
220
|
+
const sign = change.percentage >= 0 ? '+' : '';
|
|
221
|
+
return `${sign}${change.percentage}%`;
|
|
222
|
+
}
|