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.
Files changed (176) hide show
  1. package/.github/agents/complete.agent.md +257 -0
  2. package/.github/agents/feature-scaffold.agent.md +248 -0
  3. package/.github/agents/jsdoc.agent.md +243 -0
  4. package/.github/agents/plan.agent.md +202 -0
  5. package/.github/agents/spec-writer.agent.md +169 -0
  6. package/.github/agents/test-writer.agent.md +169 -0
  7. package/.stylelintrc.json +27 -0
  8. package/README.md +94 -94
  9. package/coverage/base.css +224 -0
  10. package/coverage/block-navigation.js +87 -0
  11. package/coverage/favicon.png +0 -0
  12. package/coverage/index.html +446 -0
  13. package/coverage/lcov-report/base.css +224 -0
  14. package/coverage/lcov-report/block-navigation.js +87 -0
  15. package/coverage/lcov-report/favicon.png +0 -0
  16. package/coverage/lcov-report/index.html +446 -0
  17. package/coverage/lcov-report/prettify.css +1 -0
  18. package/coverage/lcov-report/prettify.js +2 -0
  19. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  20. package/coverage/lcov-report/sorter.js +210 -0
  21. package/coverage/lcov.info +7039 -0
  22. package/coverage/prettify.css +1 -0
  23. package/coverage/prettify.js +2 -0
  24. package/coverage/sort-arrow-sprite.png +0 -0
  25. package/coverage/sorter.js +210 -0
  26. package/dist/commands/generate.js +262 -5
  27. package/dist/config/defaults.js +158 -0
  28. package/dist/config/index.js +10 -0
  29. package/dist/features/achievements/data/achievements.json +284 -0
  30. package/dist/features/achievements/engine.js +140 -0
  31. package/dist/features/achievements/evaluators.js +246 -0
  32. package/dist/features/achievements/helpers.js +58 -0
  33. package/dist/features/achievements/index.js +57 -0
  34. package/dist/features/achievements/loader.js +88 -0
  35. package/dist/features/achievements/template.js +155 -0
  36. package/dist/features/achievements/types.js +7 -0
  37. package/dist/features/commit-quality/analyzer.js +378 -0
  38. package/dist/features/commit-quality/analyzer.test.js +484 -0
  39. package/dist/features/commit-quality/index.js +28 -0
  40. package/dist/features/commit-quality/template.js +114 -0
  41. package/dist/features/commit-quality/types.js +2 -0
  42. package/dist/features/comparison/analyzer.js +222 -0
  43. package/dist/features/comparison/index.js +28 -0
  44. package/dist/features/comparison/template.js +119 -0
  45. package/dist/features/comparison/types.js +2 -0
  46. package/dist/features/contribution-graph/index.js +9 -0
  47. package/dist/features/contribution-graph/template.js +89 -0
  48. package/dist/features/events/index.js +31 -0
  49. package/dist/features/events/parser.js +253 -0
  50. package/dist/features/events/template.js +113 -0
  51. package/dist/features/events/types.js +2 -0
  52. package/dist/features/executive-summary/generator.js +275 -0
  53. package/dist/features/executive-summary/index.js +27 -0
  54. package/dist/features/executive-summary/template.js +80 -0
  55. package/dist/features/executive-summary/types.js +2 -0
  56. package/dist/features/gaps/analyzer.js +298 -0
  57. package/dist/features/gaps/analyzer.test.js +517 -0
  58. package/dist/features/gaps/index.js +27 -0
  59. package/dist/features/gaps/template.js +190 -0
  60. package/dist/features/gaps/types.js +2 -0
  61. package/dist/features/impact/analyzer.js +248 -0
  62. package/dist/features/impact/index.js +26 -0
  63. package/dist/features/impact/template.js +118 -0
  64. package/dist/features/impact/types.js +2 -0
  65. package/dist/features/index.js +40 -0
  66. package/dist/features/knowledge/analyzer.js +385 -0
  67. package/dist/features/knowledge/index.js +26 -0
  68. package/dist/features/knowledge/template.js +239 -0
  69. package/dist/features/knowledge/types.js +2 -0
  70. package/dist/features/streaks/calculator.js +184 -0
  71. package/dist/features/streaks/calculator.test.js +366 -0
  72. package/dist/features/streaks/index.js +36 -0
  73. package/dist/features/streaks/template.js +41 -0
  74. package/dist/features/streaks/types.js +9 -0
  75. package/dist/features/team/analyzer.js +316 -0
  76. package/dist/features/team/index.js +30 -0
  77. package/dist/features/team/template.js +146 -0
  78. package/dist/features/team/types.js +2 -0
  79. package/dist/features/time-patterns/analyzer.js +319 -0
  80. package/dist/features/time-patterns/analyzer.test.js +278 -0
  81. package/dist/features/time-patterns/index.js +37 -0
  82. package/dist/features/time-patterns/template.js +109 -0
  83. package/dist/features/time-patterns/types.js +9 -0
  84. package/dist/features/velocity/analyzer.js +257 -0
  85. package/dist/features/velocity/analyzer.test.js +383 -0
  86. package/dist/features/velocity/index.js +27 -0
  87. package/dist/features/velocity/template.js +189 -0
  88. package/dist/features/velocity/types.js +2 -0
  89. package/dist/generators/html/scripts/knowledge.js +17 -0
  90. package/dist/generators/html/styles/base.css +10 -6
  91. package/dist/generators/html/styles/components.css +121 -1
  92. package/dist/generators/html/styles/knowledge.css +21 -0
  93. package/dist/generators/html/styles/leaddev.css +1335 -0
  94. package/dist/generators/html/styles/strategic-insights.css +1337 -0
  95. package/dist/generators/html/templates/commitQualitySection.js +28 -2
  96. package/dist/generators/html/templates/comparisonSection.js +119 -0
  97. package/dist/generators/html/templates/eventsSection.js +113 -0
  98. package/dist/generators/html/templates/executiveSummarySection.js +80 -0
  99. package/dist/generators/html/templates/gapSection.js +190 -0
  100. package/dist/generators/html/templates/impactSection.js +8 -6
  101. package/dist/generators/html/templates/knowledgeSection.js +16 -2
  102. package/dist/generators/html/templates/teamSection.js +146 -0
  103. package/dist/generators/html/templates/velocitySection.js +189 -0
  104. package/dist/generators/html/types.js +7 -0
  105. package/dist/generators/html/utils/analysisRunner.js +93 -0
  106. package/dist/generators/html/utils/cardBuilder.js +47 -0
  107. package/dist/generators/html/utils/contextBuilder.js +54 -0
  108. package/dist/generators/html/utils/htmlDocumentBuilder.js +396 -0
  109. package/dist/generators/html/utils/kpiBuilder.js +76 -0
  110. package/dist/generators/html/utils/sectionWrapper.js +71 -0
  111. package/dist/generators/html/utils/styleLoader.js +2 -1
  112. package/dist/html/analysisRunner.js +93 -0
  113. package/dist/html/htmlDocumentBuilder.js +396 -0
  114. package/dist/html/index.js +29 -0
  115. package/dist/html/shared/colorUtils.js +61 -0
  116. package/dist/html/shared/commitMapBuilder.js +23 -0
  117. package/dist/html/shared/components/cardBuilder.js +47 -0
  118. package/dist/html/shared/components/index.js +18 -0
  119. package/dist/html/shared/components/kpiBuilder.js +76 -0
  120. package/dist/html/shared/components/sectionWrapper.js +71 -0
  121. package/dist/html/shared/contextBuilder.js +54 -0
  122. package/dist/html/shared/dateRangeCalculator.js +56 -0
  123. package/dist/html/shared/developerStatsCalculator.js +28 -0
  124. package/dist/html/shared/index.js +39 -0
  125. package/dist/html/shared/scriptLoader.js +15 -0
  126. package/dist/html/shared/scripts/export.js +125 -0
  127. package/dist/html/shared/scripts/knowledge.js +137 -0
  128. package/dist/html/shared/scripts/modal.js +68 -0
  129. package/dist/html/shared/scripts/navigation.js +156 -0
  130. package/dist/html/shared/scripts/tabs.js +18 -0
  131. package/dist/html/shared/scripts/tooltip.js +21 -0
  132. package/dist/html/shared/styleLoader.js +18 -0
  133. package/dist/html/shared/styles/achievements.css +387 -0
  134. package/dist/html/shared/styles/base.css +822 -0
  135. package/dist/html/shared/styles/components.css +1511 -0
  136. package/dist/html/shared/styles/knowledge.css +242 -0
  137. package/dist/html/shared/styles/strategic-insights.css +1337 -0
  138. package/dist/html/shared/weekGrouper.js +27 -0
  139. package/dist/html/types.js +7 -0
  140. package/dist/index.js +54 -21
  141. package/dist/test/helpers/commitFactory.js +166 -0
  142. package/dist/test/helpers/dateUtils.js +101 -0
  143. package/dist/test/helpers/index.js +29 -0
  144. package/dist/test/setup.js +17 -0
  145. package/dist/test/smoke.test.js +94 -0
  146. package/dist/types/achievements.js +7 -0
  147. package/dist/types/analysis.js +7 -0
  148. package/dist/types/core.js +7 -0
  149. package/dist/types/index.js +38 -0
  150. package/dist/types/options.js +7 -0
  151. package/dist/types/shared.js +7 -0
  152. package/dist/types/strategic.js +7 -0
  153. package/dist/types/summary.js +7 -0
  154. package/dist/utils/achievementDefinitions.js +22 -22
  155. package/dist/utils/analyzerContextBuilder.js +124 -0
  156. package/dist/utils/commitQualityAnalyzer.js +13 -2
  157. package/dist/utils/emptyResults.js +95 -0
  158. package/dist/utils/eventAnnotationParser.js +253 -0
  159. package/dist/utils/executiveSummaryGenerator.js +275 -0
  160. package/dist/utils/fileHotspotAnalyzer.js +4 -12
  161. package/dist/utils/gapAnalyzer.js +298 -0
  162. package/dist/utils/gitParser.test.js +363 -0
  163. package/dist/utils/htmlGenerator.js +126 -450
  164. package/dist/utils/impactAnalyzer.js +20 -19
  165. package/dist/utils/knowledgeDistributionAnalyzer.js +32 -27
  166. package/dist/utils/matrixGenerator.js +13 -13
  167. package/dist/utils/rangeComparisonAnalyzer.js +222 -0
  168. package/dist/utils/streakCalculator.js +77 -27
  169. package/dist/utils/teamAnalyzer.js +316 -0
  170. package/dist/utils/timePatternAnalyzer.js +18 -3
  171. package/dist/utils/velocityAnalyzer.js +257 -0
  172. package/dist/utils/wrappedGenerator.js +8 -8
  173. package/package.json +74 -55
  174. package/vitest.config.ts +46 -0
  175. package/dist/cli.js +0 -24
  176. 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 = 7 (high), 3 = 4 (medium), 4+ = 1 (low)
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
- if (significantOwners === 2)
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.2).length;
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
- * Analyze knowledge distribution across the codebase
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 (>= 20% contribution)
289
- const significantOwners = owners.filter(o => o.percentage >= 20).length;
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 achievementEngine_1 = require("./achievementEngine");
10
- const commitQualityAnalyzer_1 = require("./commitQualityAnalyzer");
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 streakCalculator_1 = require("./streakCalculator");
13
- const timePatternAnalyzer_1 = require("./timePatternAnalyzer");
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, streakCalculator_1.calculateStreaks)(commits, startDate, endDate);
56
+ const streakData = (0, streaks_1.calculateStreaks)(commits, startDate, endDate);
57
57
  // Analyze time patterns
58
- const timePattern = (0, timePatternAnalyzer_1.analyzeTimePatterns)(commits, startDate, endDate);
58
+ const timePattern = (0, time_patterns_1.analyzeTimePatterns)(commits, startDate, endDate);
59
59
  // Analyze commit quality
60
- const commitQuality = (0, commitQualityAnalyzer_1.analyzeCommitQuality)(commits);
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, achievementEngine_1.checkAchievements)(analysisData);
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, streakCalculator_1.getStreakMotivation)(streakData.currentStreak.days, true)}`);
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, timePatternAnalyzer_1.getChronotypeLabel)(timePattern.chronotype);
198
- const chronotypeDesc = (0, timePatternAnalyzer_1.getChronotypeDescription)(timePattern.chronotype);
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, commitQualityAnalyzer_1.getQualityRating)(commitQuality.overallScore);
221
- const level = (0, commitQualityAnalyzer_1.getQualityLevel)(commitQuality.overallScore);
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
- function calculateStreaks(commits, startDate, endDate) {
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 >= startDate && commitDate <= endDate) {
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)(endDate, startDate) + 1;
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
- // Find all streaks
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 allDays = (0, date_fns_1.eachDayOfInterval)({ start: startDate, end: endDate });
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: (0, date_fns_1.subDays)(date, 1)
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: 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: startDate, endDate: startDate };
56
- // Determine current streak (from today or most recent commit going backward)
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
- if (commitDates.has(todayKey)) {
61
- // Today has a commit, find streak from today backward
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 = today;
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
- checkDate = (0, date_fns_1.subDays)(checkDate, 1);
67
- if (checkDate < startDate)
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: (0, date_fns_1.subDays)(today, streakDays - 1),
73
- endDate: today,
117
+ startDate: streakStartDate,
118
+ endDate: checkToday,
74
119
  isActive: true
75
120
  };
76
121
  }
77
122
  else {
78
- // No commit today, check if yesterday had commit (streak just broke)
79
- const yesterday = (0, date_fns_1.subDays)(today, 1);
80
- const yesterdayKey = (0, date_fns_1.format)(yesterday, 'yyyy-MM-dd');
81
- if (commitDates.has(yesterdayKey)) {
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 = yesterday;
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
- checkDate = (0, date_fns_1.subDays)(checkDate, 1);
87
- if (checkDate < startDate)
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: (0, date_fns_1.subDays)(yesterday, streakDays - 1),
93
- endDate: yesterday,
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) {