repo-wrapped 0.0.6 → 0.0.9
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/.github/agents/complete.agent.md +257 -0
- package/.github/agents/feature-scaffold.agent.md +248 -0
- package/.github/agents/jsdoc.agent.md +243 -0
- package/.github/agents/plan.agent.md +202 -0
- package/.github/agents/spec-writer.agent.md +169 -0
- package/.github/agents/test-writer.agent.md +169 -0
- package/.stylelintrc.json +27 -0
- package/README.md +94 -94
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +446 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +446 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +7039 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/commands/generate.js +262 -5
- package/dist/config/defaults.js +158 -0
- package/dist/config/index.js +10 -0
- package/dist/features/achievements/data/achievements.json +284 -0
- package/dist/features/achievements/engine.js +140 -0
- package/dist/features/achievements/evaluators.js +246 -0
- package/dist/features/achievements/helpers.js +58 -0
- package/dist/features/achievements/index.js +57 -0
- package/dist/features/achievements/loader.js +88 -0
- package/dist/features/achievements/template.js +155 -0
- package/dist/features/achievements/types.js +7 -0
- package/dist/features/commit-quality/analyzer.js +378 -0
- package/dist/features/commit-quality/analyzer.test.js +484 -0
- package/dist/features/commit-quality/index.js +28 -0
- package/dist/features/commit-quality/template.js +114 -0
- package/dist/features/commit-quality/types.js +2 -0
- package/dist/features/comparison/analyzer.js +222 -0
- package/dist/features/comparison/index.js +28 -0
- package/dist/features/comparison/template.js +119 -0
- package/dist/features/comparison/types.js +2 -0
- package/dist/features/contribution-graph/index.js +9 -0
- package/dist/features/contribution-graph/template.js +89 -0
- package/dist/features/events/index.js +31 -0
- package/dist/features/events/parser.js +253 -0
- package/dist/features/events/template.js +113 -0
- package/dist/features/events/types.js +2 -0
- package/dist/features/executive-summary/generator.js +275 -0
- package/dist/features/executive-summary/index.js +27 -0
- package/dist/features/executive-summary/template.js +80 -0
- package/dist/features/executive-summary/types.js +2 -0
- package/dist/features/gaps/analyzer.js +298 -0
- package/dist/features/gaps/analyzer.test.js +517 -0
- package/dist/features/gaps/index.js +27 -0
- package/dist/features/gaps/template.js +190 -0
- package/dist/features/gaps/types.js +2 -0
- package/dist/features/impact/analyzer.js +248 -0
- package/dist/features/impact/index.js +26 -0
- package/dist/features/impact/template.js +118 -0
- package/dist/features/impact/types.js +2 -0
- package/dist/features/index.js +40 -0
- package/dist/features/knowledge/analyzer.js +385 -0
- package/dist/features/knowledge/index.js +26 -0
- package/dist/features/knowledge/template.js +239 -0
- package/dist/features/knowledge/types.js +2 -0
- package/dist/features/streaks/calculator.js +184 -0
- package/dist/features/streaks/calculator.test.js +366 -0
- package/dist/features/streaks/index.js +36 -0
- package/dist/features/streaks/template.js +41 -0
- package/dist/features/streaks/types.js +9 -0
- package/dist/features/team/analyzer.js +316 -0
- package/dist/features/team/index.js +30 -0
- package/dist/features/team/template.js +146 -0
- package/dist/features/team/types.js +2 -0
- package/dist/features/time-patterns/analyzer.js +319 -0
- package/dist/features/time-patterns/analyzer.test.js +278 -0
- package/dist/features/time-patterns/index.js +37 -0
- package/dist/features/time-patterns/template.js +109 -0
- package/dist/features/time-patterns/types.js +9 -0
- package/dist/features/velocity/analyzer.js +257 -0
- package/dist/features/velocity/analyzer.test.js +383 -0
- package/dist/features/velocity/index.js +27 -0
- package/dist/features/velocity/template.js +189 -0
- package/dist/features/velocity/types.js +2 -0
- package/dist/generators/html/scripts/knowledge.js +17 -0
- package/dist/generators/html/styles/base.css +10 -6
- package/dist/generators/html/styles/components.css +121 -1
- package/dist/generators/html/styles/knowledge.css +21 -0
- package/dist/generators/html/styles/leaddev.css +1335 -0
- package/dist/generators/html/styles/strategic-insights.css +1337 -0
- package/dist/generators/html/templates/commitQualitySection.js +28 -2
- 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 +80 -0
- package/dist/generators/html/templates/gapSection.js +190 -0
- package/dist/generators/html/templates/impactSection.js +8 -6
- package/dist/generators/html/templates/knowledgeSection.js +16 -2
- package/dist/generators/html/templates/teamSection.js +146 -0
- package/dist/generators/html/templates/velocitySection.js +189 -0
- package/dist/generators/html/types.js +7 -0
- package/dist/generators/html/utils/analysisRunner.js +93 -0
- package/dist/generators/html/utils/cardBuilder.js +47 -0
- package/dist/generators/html/utils/contextBuilder.js +54 -0
- package/dist/generators/html/utils/htmlDocumentBuilder.js +396 -0
- package/dist/generators/html/utils/kpiBuilder.js +76 -0
- package/dist/generators/html/utils/sectionWrapper.js +71 -0
- package/dist/generators/html/utils/styleLoader.js +2 -1
- package/dist/html/analysisRunner.js +93 -0
- package/dist/html/htmlDocumentBuilder.js +396 -0
- package/dist/html/index.js +29 -0
- package/dist/html/shared/colorUtils.js +61 -0
- package/dist/html/shared/commitMapBuilder.js +23 -0
- package/dist/html/shared/components/cardBuilder.js +47 -0
- package/dist/html/shared/components/index.js +18 -0
- package/dist/html/shared/components/kpiBuilder.js +76 -0
- package/dist/html/shared/components/sectionWrapper.js +71 -0
- package/dist/html/shared/contextBuilder.js +54 -0
- package/dist/html/shared/dateRangeCalculator.js +56 -0
- package/dist/html/shared/developerStatsCalculator.js +28 -0
- package/dist/html/shared/index.js +39 -0
- package/dist/html/shared/scriptLoader.js +15 -0
- package/dist/html/shared/scripts/export.js +125 -0
- package/dist/html/shared/scripts/knowledge.js +137 -0
- package/dist/html/shared/scripts/modal.js +68 -0
- package/dist/html/shared/scripts/navigation.js +156 -0
- package/dist/html/shared/scripts/tabs.js +18 -0
- package/dist/html/shared/scripts/tooltip.js +21 -0
- package/dist/html/shared/styleLoader.js +18 -0
- package/dist/html/shared/styles/achievements.css +387 -0
- package/dist/html/shared/styles/base.css +822 -0
- package/dist/html/shared/styles/components.css +1511 -0
- package/dist/html/shared/styles/knowledge.css +242 -0
- package/dist/html/shared/styles/strategic-insights.css +1337 -0
- package/dist/html/shared/weekGrouper.js +27 -0
- package/dist/html/types.js +7 -0
- package/dist/index.js +54 -21
- package/dist/test/helpers/commitFactory.js +166 -0
- package/dist/test/helpers/dateUtils.js +101 -0
- package/dist/test/helpers/index.js +29 -0
- package/dist/test/setup.js +17 -0
- package/dist/test/smoke.test.js +94 -0
- package/dist/types/achievements.js +7 -0
- package/dist/types/analysis.js +7 -0
- package/dist/types/core.js +7 -0
- package/dist/types/index.js +38 -0
- package/dist/types/options.js +7 -0
- package/dist/types/shared.js +7 -0
- package/dist/types/strategic.js +7 -0
- package/dist/types/summary.js +7 -0
- package/dist/utils/achievementDefinitions.js +22 -22
- package/dist/utils/analyzerContextBuilder.js +124 -0
- package/dist/utils/commitQualityAnalyzer.js +13 -2
- package/dist/utils/emptyResults.js +95 -0
- package/dist/utils/eventAnnotationParser.js +253 -0
- package/dist/utils/executiveSummaryGenerator.js +275 -0
- package/dist/utils/fileHotspotAnalyzer.js +4 -12
- package/dist/utils/gapAnalyzer.js +298 -0
- package/dist/utils/gitParser.test.js +363 -0
- package/dist/utils/htmlGenerator.js +126 -450
- package/dist/utils/impactAnalyzer.js +20 -19
- package/dist/utils/knowledgeDistributionAnalyzer.js +32 -27
- package/dist/utils/matrixGenerator.js +13 -13
- package/dist/utils/rangeComparisonAnalyzer.js +222 -0
- package/dist/utils/streakCalculator.js +77 -27
- package/dist/utils/teamAnalyzer.js +316 -0
- package/dist/utils/timePatternAnalyzer.js +18 -3
- package/dist/utils/velocityAnalyzer.js +257 -0
- package/dist/utils/wrappedGenerator.js +8 -8
- package/package.json +74 -55
- package/vitest.config.ts +46 -0
- package/dist/cli.js +0 -24
- package/dist/commands/index.js +0 -24
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.analyzeKnowledgeDistribution = analyzeKnowledgeDistribution;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
|
+
const emptyResults_1 = require("./emptyResults");
|
|
5
6
|
/**
|
|
6
7
|
* Extract directory from file path
|
|
7
8
|
*/
|
|
@@ -13,18 +14,15 @@ function getDirectory(filePath) {
|
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* Calculate bus factor risk score
|
|
16
|
-
* 1 owner = 10 (critical), 2
|
|
17
|
+
* 1 owner = 10 (critical), 2+ = 1 (low/safe)
|
|
18
|
+
* Adjusted for realistic team dynamics where 2+ knowledgeable owners is considered safe
|
|
17
19
|
*/
|
|
18
20
|
function calculateRiskScore(significantOwners) {
|
|
19
21
|
if (significantOwners === 0)
|
|
20
22
|
return 10;
|
|
21
23
|
if (significantOwners === 1)
|
|
22
24
|
return 10;
|
|
23
|
-
|
|
24
|
-
return 7;
|
|
25
|
-
if (significantOwners === 3)
|
|
26
|
-
return 4;
|
|
27
|
-
return 1;
|
|
25
|
+
return 1; // 2+ owners = low risk
|
|
28
26
|
}
|
|
29
27
|
/**
|
|
30
28
|
* Determine ownership type based on contribution percentages
|
|
@@ -151,7 +149,7 @@ function buildFileOwnership(fileStats, directoryPath) {
|
|
|
151
149
|
});
|
|
152
150
|
const ownershipPercentage = Math.round((maxCommits / totalCommits) * 100);
|
|
153
151
|
const knowledgeAge = calculateKnowledgeAge(lastModified);
|
|
154
|
-
const significantOwners = Array.from(data.authors.values()).filter(a => (a.commits / totalCommits) >= 0.
|
|
152
|
+
const significantOwners = Array.from(data.authors.values()).filter(a => (a.commits / totalCommits) >= 0.65).length;
|
|
155
153
|
const busFactorRiskScore = calculateRiskScore(significantOwners);
|
|
156
154
|
const riskLevel = getRiskLevel(busFactorRiskScore);
|
|
157
155
|
files.push({
|
|
@@ -175,11 +173,29 @@ function buildFileOwnership(fileStats, directoryPath) {
|
|
|
175
173
|
.slice(0, 20); // Limit to 20 files per directory
|
|
176
174
|
}
|
|
177
175
|
/**
|
|
178
|
-
*
|
|
176
|
+
* Analyzes knowledge distribution across the codebase to identify bus factor risks.
|
|
177
|
+
*
|
|
178
|
+
* Identifies:
|
|
179
|
+
* - Directory ownership patterns (solo, primary, shared, collaborative)
|
|
180
|
+
* - Bus factor risks (areas with single knowledge holders)
|
|
181
|
+
* - Knowledge silos (directories owned by one person)
|
|
182
|
+
* - Shared knowledge areas (directories with 3+ active contributors)
|
|
183
|
+
*
|
|
184
|
+
* @param commits - Array of commit data to analyze
|
|
185
|
+
* @param repoPath - Optional repository path for deep file-level analysis via git log
|
|
186
|
+
* @param deepAnalysis - Whether to perform file-level analysis (slower but more accurate)
|
|
187
|
+
* @returns Knowledge distribution with ownership data, risks, and recommendations
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* const knowledge = analyzeKnowledgeDistribution(commits, '/path/to/repo', true);
|
|
192
|
+
* console.log(`Bus factor risk: ${knowledge.busFactorRisk.level}`);
|
|
193
|
+
* console.log(`Knowledge silos: ${knowledge.knowledgeSilos.length}`);
|
|
194
|
+
* ```
|
|
179
195
|
*/
|
|
180
196
|
function analyzeKnowledgeDistribution(commits, repoPath, deepAnalysis = false) {
|
|
181
197
|
if (commits.length === 0) {
|
|
182
|
-
return getEmptyKnowledgeDistribution();
|
|
198
|
+
return (0, emptyResults_1.getEmptyKnowledgeDistribution)();
|
|
183
199
|
}
|
|
184
200
|
// For deep analysis, parse git log with file names
|
|
185
201
|
let fileStats = null;
|
|
@@ -262,6 +278,11 @@ function analyzeKnowledgeDistribution(commits, repoPath, deepAnalysis = false) {
|
|
|
262
278
|
let dirCount = 0;
|
|
263
279
|
directoryStats.forEach((authorStats, dirPath) => {
|
|
264
280
|
const totalCommits = Array.from(authorStats.values()).reduce((sum, a) => sum + a.commits, 0);
|
|
281
|
+
// Skip areas with too few commits - not enough data to assess bus factor
|
|
282
|
+
const MIN_COMMITS_FOR_ASSESSMENT = 20;
|
|
283
|
+
if (totalCommits < MIN_COMMITS_FOR_ASSESSMENT) {
|
|
284
|
+
return; // Skip this directory
|
|
285
|
+
}
|
|
265
286
|
// Get the latest and earliest activity date for the directory
|
|
266
287
|
let latestActivity = new Date(0);
|
|
267
288
|
let earliestActivity = new Date();
|
|
@@ -285,8 +306,8 @@ function analyzeKnowledgeDistribution(commits, repoPath, deepAnalysis = false) {
|
|
|
285
306
|
knowledgeAge: calculateKnowledgeAge(stats.lastActive)
|
|
286
307
|
}))
|
|
287
308
|
.sort((a, b) => b.commits - a.commits);
|
|
288
|
-
// Calculate significant owners (>=
|
|
289
|
-
const significantOwners = owners.filter(o => o.percentage >=
|
|
309
|
+
// Calculate significant owners (>= 65% contribution)
|
|
310
|
+
const significantOwners = owners.filter(o => o.percentage >= 65).length;
|
|
290
311
|
const busFactorRisk = calculateRiskScore(significantOwners);
|
|
291
312
|
const ownershipType = determineOwnershipType(owners);
|
|
292
313
|
// Build high-risk files list if deep analysis is enabled
|
|
@@ -362,19 +383,3 @@ function generateRecommendations(data) {
|
|
|
362
383
|
}
|
|
363
384
|
return recommendations.slice(0, 5);
|
|
364
385
|
}
|
|
365
|
-
/**
|
|
366
|
-
* Return empty distribution for no commits
|
|
367
|
-
*/
|
|
368
|
-
function getEmptyKnowledgeDistribution() {
|
|
369
|
-
return {
|
|
370
|
-
directories: [],
|
|
371
|
-
busFactorRisk: {
|
|
372
|
-
overall: 0,
|
|
373
|
-
level: 'low',
|
|
374
|
-
criticalPaths: []
|
|
375
|
-
},
|
|
376
|
-
knowledgeSilos: [],
|
|
377
|
-
sharedKnowledge: [],
|
|
378
|
-
recommendations: ['No commit data available for analysis']
|
|
379
|
-
};
|
|
380
|
-
}
|
|
@@ -6,11 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.generateCommitMatrix = generateCommitMatrix;
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const date_fns_1 = require("date-fns");
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const achievements_1 = require("../features/achievements");
|
|
10
|
+
const commit_quality_1 = require("../features/commit-quality");
|
|
11
11
|
const fileHotspotAnalyzer_1 = require("./fileHotspotAnalyzer");
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const streaks_1 = require("../features/streaks");
|
|
13
|
+
const time_patterns_1 = require("../features/time-patterns");
|
|
14
14
|
/**
|
|
15
15
|
* Calculate dynamic thresholds based on commit distribution for terminal output
|
|
16
16
|
*/
|
|
@@ -53,11 +53,11 @@ function generateCommitMatrix(commits, year, monthsToShow, repoPath, repoName, r
|
|
|
53
53
|
});
|
|
54
54
|
const totalCommits = Array.from(commitMap.values()).reduce((sum, count) => sum + count, 0);
|
|
55
55
|
// Calculate streaks
|
|
56
|
-
const streakData = (0,
|
|
56
|
+
const streakData = (0, streaks_1.calculateStreaks)(commits, startDate, endDate);
|
|
57
57
|
// Analyze time patterns
|
|
58
|
-
const timePattern = (0,
|
|
58
|
+
const timePattern = (0, time_patterns_1.analyzeTimePatterns)(commits, startDate, endDate);
|
|
59
59
|
// Analyze commit quality
|
|
60
|
-
const commitQuality = (0,
|
|
60
|
+
const commitQuality = (0, commit_quality_1.analyzeCommitQuality)(commits);
|
|
61
61
|
// Analyze file hotspots
|
|
62
62
|
const fileHotspots = (0, fileHotspotAnalyzer_1.analyzeFileHotspots)(repoPath, startDate, endDate);
|
|
63
63
|
// Prepare analysis data for achievements
|
|
@@ -71,7 +71,7 @@ function generateCommitMatrix(commits, year, monthsToShow, repoPath, repoName, r
|
|
|
71
71
|
dateRange: { start: startDate, end: endDate }
|
|
72
72
|
};
|
|
73
73
|
// Check achievements
|
|
74
|
-
const achievementProgress = (0,
|
|
74
|
+
const achievementProgress = (0, achievements_1.checkAchievements)(analysisData);
|
|
75
75
|
return formatGitHubStyle(commitMap, weekStartDate, weekEndDate, startDate, endDate, totalCommits, streakData, timePattern, commitQuality, fileHotspots, achievementProgress, repoName, repoUrl);
|
|
76
76
|
}
|
|
77
77
|
function formatGitHubStyle(commitMap, weekStartDate, weekEndDate, dataStartDate, dataEndDate, totalCommits, streakData, timePattern, commitQuality, fileHotspots, achievementProgress, repoName, repoUrl) {
|
|
@@ -174,7 +174,7 @@ function formatGitHubStyle(commitMap, weekStartDate, weekEndDate, dataStartDate,
|
|
|
174
174
|
: 'No active streak';
|
|
175
175
|
output += `${currentStreakEmoji} Current Streak: ${chalk_1.default.bold.green(currentStreakText)}`;
|
|
176
176
|
if (streakData.currentStreak.isActive && streakData.currentStreak.days > 0) {
|
|
177
|
-
output += chalk_1.default.dim(` - ${(0,
|
|
177
|
+
output += chalk_1.default.dim(` - ${(0, streaks_1.getStreakMotivation)(streakData.currentStreak.days, true)}`);
|
|
178
178
|
}
|
|
179
179
|
output += '\n';
|
|
180
180
|
const longestStreakText = streakData.longestStreak.days > 0
|
|
@@ -194,8 +194,8 @@ function formatGitHubStyle(commitMap, weekStartDate, weekEndDate, dataStartDate,
|
|
|
194
194
|
`${timePattern.peakHour.hour - 12}:00 PM`;
|
|
195
195
|
output += `⏱️ Peak Hour: ${chalk_1.default.bold.green(peakHourLabel)} (${timePattern.peakHour.commitCount} commits)\n`;
|
|
196
196
|
// Chronotype
|
|
197
|
-
const chronotypeLabel = (0,
|
|
198
|
-
const chronotypeDesc = (0,
|
|
197
|
+
const chronotypeLabel = (0, time_patterns_1.getChronotypeLabel)(timePattern.chronotype);
|
|
198
|
+
const chronotypeDesc = (0, time_patterns_1.getChronotypeDescription)(timePattern.chronotype);
|
|
199
199
|
output += `${chronotypeLabel} ${chalk_1.default.dim(`(${timePattern.chronotypeConfidence.toFixed(0)}% confidence)`)}\n`;
|
|
200
200
|
if (chronotypeDesc) {
|
|
201
201
|
output += chalk_1.default.dim(` ${chronotypeDesc}\n`);
|
|
@@ -217,8 +217,8 @@ function formatGitHubStyle(commitMap, weekStartDate, weekEndDate, dataStartDate,
|
|
|
217
217
|
output += '\n';
|
|
218
218
|
output += chalk_1.default.bold.cyan('📝 Commit Quality\n');
|
|
219
219
|
output += chalk_1.default.dim('─'.repeat(50)) + '\n';
|
|
220
|
-
const rating = (0,
|
|
221
|
-
const level = (0,
|
|
220
|
+
const rating = (0, commit_quality_1.getQualityRating)(commitQuality.overallScore);
|
|
221
|
+
const level = (0, commit_quality_1.getQualityLevel)(commitQuality.overallScore);
|
|
222
222
|
output += `📊 Overall Score: ${chalk_1.default.bold.green(commitQuality.overallScore.toFixed(1))}/10 ${rating} ${chalk_1.default.dim(`(${level})`)}\n`;
|
|
223
223
|
output += `✅ Convention: ${chalk_1.default.bold.cyan(commitQuality.conventionalCommits.adherence.toFixed(1))}%`;
|
|
224
224
|
if (commitQuality.conventionalCommits.adherence < 70) {
|
|
@@ -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 streaks_1 = require("../features/streaks");
|
|
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, streaks_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
|
+
}
|
|
@@ -3,25 +3,61 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.calculateStreaks = calculateStreaks;
|
|
4
4
|
exports.getStreakMotivation = getStreakMotivation;
|
|
5
5
|
const date_fns_1 = require("date-fns");
|
|
6
|
-
|
|
6
|
+
const analyzerContextBuilder_1 = require("./analyzerContextBuilder");
|
|
7
|
+
/**
|
|
8
|
+
* Check if a date is a business day (Monday-Friday)
|
|
9
|
+
*/
|
|
10
|
+
function isBusinessDay(date) {
|
|
11
|
+
const day = (0, date_fns_1.getDay)(date);
|
|
12
|
+
return day !== 0 && day !== 6; // 0 = Sunday, 6 = Saturday
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get the previous business day from a given date
|
|
16
|
+
*/
|
|
17
|
+
function getPreviousBusinessDay(date) {
|
|
18
|
+
let prevDate = (0, date_fns_1.subDays)(date, 1);
|
|
19
|
+
while (!isBusinessDay(prevDate)) {
|
|
20
|
+
prevDate = (0, date_fns_1.subDays)(prevDate, 1);
|
|
21
|
+
}
|
|
22
|
+
return prevDate;
|
|
23
|
+
}
|
|
24
|
+
function calculateStreaks(ctxOrCommits, startDate, endDate) {
|
|
25
|
+
// Normalize to internal variables
|
|
26
|
+
let commits;
|
|
27
|
+
let start;
|
|
28
|
+
let end;
|
|
29
|
+
if ((0, analyzerContextBuilder_1.isAnalyzerContext)(ctxOrCommits)) {
|
|
30
|
+
commits = ctxOrCommits.commits;
|
|
31
|
+
start = ctxOrCommits.dateRange.start;
|
|
32
|
+
end = ctxOrCommits.dateRange.end;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
commits = ctxOrCommits;
|
|
36
|
+
start = startDate;
|
|
37
|
+
end = endDate;
|
|
38
|
+
}
|
|
7
39
|
// Create set of unique commit dates
|
|
8
40
|
const commitDates = new Set();
|
|
9
41
|
commits.forEach(commit => {
|
|
10
42
|
const commitDate = new Date(commit.date);
|
|
11
|
-
if (commitDate >=
|
|
43
|
+
if (commitDate >= start && commitDate <= end) {
|
|
12
44
|
commitDates.add((0, date_fns_1.format)(commitDate, 'yyyy-MM-dd'));
|
|
13
45
|
}
|
|
14
46
|
});
|
|
15
47
|
// Calculate total days and active days
|
|
16
|
-
const totalDays = (0, date_fns_1.differenceInDays)(
|
|
48
|
+
const totalDays = (0, date_fns_1.differenceInDays)(end, start) + 1;
|
|
17
49
|
const totalActiveDays = commitDates.size;
|
|
18
50
|
const activeDayPercentage = totalDays > 0 ? (totalActiveDays / totalDays) * 100 : 0;
|
|
19
|
-
//
|
|
51
|
+
// Calculate business day stats
|
|
52
|
+
const allDays = (0, date_fns_1.eachDayOfInterval)({ start: start, end: end });
|
|
53
|
+
const businessDays = allDays.filter(isBusinessDay);
|
|
54
|
+
const totalBusinessDays = businessDays.length;
|
|
55
|
+
const activeBusinessDays = businessDays.filter(d => commitDates.has((0, date_fns_1.format)(d, 'yyyy-MM-dd'))).length;
|
|
56
|
+
// Find all streaks (business days only)
|
|
20
57
|
const streaks = [];
|
|
21
58
|
let currentStreakDays = 0;
|
|
22
59
|
let currentStreakStart = null;
|
|
23
|
-
const
|
|
24
|
-
for (const date of allDays) {
|
|
60
|
+
for (const date of businessDays) {
|
|
25
61
|
const dateKey = (0, date_fns_1.format)(date, 'yyyy-MM-dd');
|
|
26
62
|
if (commitDates.has(dateKey)) {
|
|
27
63
|
if (currentStreakDays === 0) {
|
|
@@ -31,10 +67,12 @@ function calculateStreaks(commits, startDate, endDate) {
|
|
|
31
67
|
}
|
|
32
68
|
else {
|
|
33
69
|
if (currentStreakDays > 0 && currentStreakStart) {
|
|
70
|
+
// Find the last business day in the streak
|
|
71
|
+
const streakEnd = getPreviousBusinessDay(date);
|
|
34
72
|
streaks.push({
|
|
35
73
|
days: currentStreakDays,
|
|
36
74
|
startDate: currentStreakStart,
|
|
37
|
-
endDate:
|
|
75
|
+
endDate: streakEnd > currentStreakStart ? streakEnd : currentStreakStart
|
|
38
76
|
});
|
|
39
77
|
currentStreakDays = 0;
|
|
40
78
|
currentStreakStart = null;
|
|
@@ -43,54 +81,63 @@ function calculateStreaks(commits, startDate, endDate) {
|
|
|
43
81
|
}
|
|
44
82
|
// Handle streak that extends to end date
|
|
45
83
|
if (currentStreakDays > 0 && currentStreakStart) {
|
|
84
|
+
// Find the last business day in range
|
|
85
|
+
const lastBusinessDay = businessDays[businessDays.length - 1] || end;
|
|
46
86
|
streaks.push({
|
|
47
87
|
days: currentStreakDays,
|
|
48
88
|
startDate: currentStreakStart,
|
|
49
|
-
endDate:
|
|
89
|
+
endDate: lastBusinessDay
|
|
50
90
|
});
|
|
51
91
|
}
|
|
52
92
|
// Find longest streak
|
|
53
93
|
const longestStreak = streaks.length > 0
|
|
54
94
|
? streaks.reduce((longest, current) => current.days > longest.days ? current : longest)
|
|
55
|
-
: { days: 0, startDate:
|
|
56
|
-
// Determine current streak (from today or most recent
|
|
95
|
+
: { days: 0, startDate: start, endDate: start };
|
|
96
|
+
// Determine current streak (from today or most recent business day going backward)
|
|
57
97
|
const today = new Date();
|
|
58
98
|
const todayKey = (0, date_fns_1.format)(today, 'yyyy-MM-dd');
|
|
59
99
|
let currentStreak;
|
|
60
|
-
|
|
61
|
-
|
|
100
|
+
// For current streak, we check today or the last business day
|
|
101
|
+
const checkToday = isBusinessDay(today) ? today : getPreviousBusinessDay(today);
|
|
102
|
+
const checkTodayKey = (0, date_fns_1.format)(checkToday, 'yyyy-MM-dd');
|
|
103
|
+
if (commitDates.has(checkTodayKey)) {
|
|
104
|
+
// Recent business day has a commit, find streak backward through business days only
|
|
62
105
|
let streakDays = 0;
|
|
63
|
-
let checkDate =
|
|
106
|
+
let checkDate = checkToday;
|
|
107
|
+
let streakStartDate = checkToday;
|
|
64
108
|
while (commitDates.has((0, date_fns_1.format)(checkDate, 'yyyy-MM-dd'))) {
|
|
65
109
|
streakDays++;
|
|
66
|
-
|
|
67
|
-
|
|
110
|
+
streakStartDate = checkDate;
|
|
111
|
+
checkDate = getPreviousBusinessDay(checkDate);
|
|
112
|
+
if (checkDate < start)
|
|
68
113
|
break;
|
|
69
114
|
}
|
|
70
115
|
currentStreak = {
|
|
71
116
|
days: streakDays,
|
|
72
|
-
startDate:
|
|
73
|
-
endDate:
|
|
117
|
+
startDate: streakStartDate,
|
|
118
|
+
endDate: checkToday,
|
|
74
119
|
isActive: true
|
|
75
120
|
};
|
|
76
121
|
}
|
|
77
122
|
else {
|
|
78
|
-
// No commit
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
if (commitDates.has(
|
|
123
|
+
// No commit on recent business day, check previous business day
|
|
124
|
+
const prevBusinessDay = getPreviousBusinessDay(checkToday);
|
|
125
|
+
const prevKey = (0, date_fns_1.format)(prevBusinessDay, 'yyyy-MM-dd');
|
|
126
|
+
if (commitDates.has(prevKey)) {
|
|
82
127
|
let streakDays = 0;
|
|
83
|
-
let checkDate =
|
|
128
|
+
let checkDate = prevBusinessDay;
|
|
129
|
+
let streakStartDate = prevBusinessDay;
|
|
84
130
|
while (commitDates.has((0, date_fns_1.format)(checkDate, 'yyyy-MM-dd'))) {
|
|
85
131
|
streakDays++;
|
|
86
|
-
|
|
87
|
-
|
|
132
|
+
streakStartDate = checkDate;
|
|
133
|
+
checkDate = getPreviousBusinessDay(checkDate);
|
|
134
|
+
if (checkDate < start)
|
|
88
135
|
break;
|
|
89
136
|
}
|
|
90
137
|
currentStreak = {
|
|
91
138
|
days: streakDays,
|
|
92
|
-
startDate:
|
|
93
|
-
endDate:
|
|
139
|
+
startDate: streakStartDate,
|
|
140
|
+
endDate: prevBusinessDay,
|
|
94
141
|
isActive: false
|
|
95
142
|
};
|
|
96
143
|
}
|
|
@@ -110,7 +157,10 @@ function calculateStreaks(commits, startDate, endDate) {
|
|
|
110
157
|
totalActiveDays,
|
|
111
158
|
totalDays,
|
|
112
159
|
activeDayPercentage,
|
|
113
|
-
streaks
|
|
160
|
+
streaks,
|
|
161
|
+
// Add business day specific data
|
|
162
|
+
totalBusinessDays,
|
|
163
|
+
activeBusinessDays
|
|
114
164
|
};
|
|
115
165
|
}
|
|
116
166
|
function getStreakMotivation(days, isActive) {
|