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
@@ -0,0 +1,275 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateExecutiveSummary = generateExecutiveSummary;
4
+ exports.formatExecutiveSummary = formatExecutiveSummary;
5
+ const date_fns_1 = require("date-fns");
6
+ /**
7
+ * Generates an executive summary with KPIs, insights, and risks
8
+ */
9
+ function generateExecutiveSummary(input) {
10
+ const { commits, startDate, endDate, repoName, streakData, timePattern, commitQuality, knowledgeDistribution, velocityAnalysis, gapAnalysis, } = input;
11
+ const kpis = generateKPIs(commits, startDate, endDate, streakData, commitQuality, velocityAnalysis);
12
+ const keyInsights = generateKeyInsights(commits, streakData, timePattern, commitQuality, velocityAnalysis, gapAnalysis);
13
+ const risks = assessRisks(knowledgeDistribution, gapAnalysis, commitQuality, timePattern);
14
+ const recommendations = generateRecommendations(risks, keyInsights, commitQuality);
15
+ return {
16
+ generatedAt: new Date(),
17
+ period: { start: startDate, end: endDate },
18
+ repoName,
19
+ kpis,
20
+ keyInsights,
21
+ risks,
22
+ recommendations,
23
+ };
24
+ }
25
+ function generateKPIs(commits, startDate, endDate, streakData, commitQuality, velocityAnalysis) {
26
+ const kpis = [];
27
+ const totalDays = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;
28
+ const totalWeeks = Math.max(1, Math.ceil(totalDays / 7));
29
+ // 1. Velocity KPI
30
+ const commitsPerWeek = Math.round((commits.length / totalWeeks) * 10) / 10;
31
+ const velocityTrend = velocityAnalysis?.overallTrend || 'stable';
32
+ const velocityTrendPct = velocityAnalysis?.trendPercentage || 0;
33
+ kpis.push({
34
+ name: 'Velocity',
35
+ value: `${commitsPerWeek}/week`,
36
+ trend: velocityTrend === 'increasing' ? 'up' : velocityTrend === 'decreasing' ? 'down' : 'stable',
37
+ trendValue: `${velocityTrendPct >= 0 ? '+' : ''}${velocityTrendPct}%`,
38
+ status: commitsPerWeek >= 10 ? 'green' : commitsPerWeek >= 5 ? 'yellow' : 'red',
39
+ benchmark: 'vs start of period',
40
+ });
41
+ // 2. Quality Score KPI
42
+ const qualityScore = Math.round(commitQuality.overallScore * 10) / 10;
43
+ kpis.push({
44
+ name: 'Quality Score',
45
+ value: `${qualityScore}/10`,
46
+ trend: qualityScore >= 7 ? 'up' : qualityScore >= 5 ? 'stable' : 'down',
47
+ trendValue: qualityScore >= 7 ? 'Good' : qualityScore >= 5 ? 'Fair' : 'Needs work',
48
+ status: qualityScore >= 7 ? 'green' : qualityScore >= 5 ? 'yellow' : 'red',
49
+ });
50
+ // 3. Active Contributors
51
+ const uniqueAuthors = new Set(commits.map(c => c.author)).size;
52
+ kpis.push({
53
+ name: 'Active Contributors',
54
+ value: uniqueAuthors,
55
+ trend: 'stable',
56
+ trendValue: `${uniqueAuthors} developer${uniqueAuthors !== 1 ? 's' : ''}`,
57
+ status: uniqueAuthors >= 3 ? 'green' : uniqueAuthors >= 2 ? 'yellow' : 'red',
58
+ });
59
+ // 4. Consistency
60
+ const consistencyPct = Math.round(streakData.activeDayPercentage);
61
+ kpis.push({
62
+ name: 'Consistency',
63
+ value: `${consistencyPct}%`,
64
+ trend: consistencyPct >= 50 ? 'up' : consistencyPct >= 30 ? 'stable' : 'down',
65
+ trendValue: `${streakData.totalActiveDays}/${totalDays} days active`,
66
+ status: consistencyPct >= 50 ? 'green' : consistencyPct >= 30 ? 'yellow' : 'red',
67
+ });
68
+ // 5. Longest Streak
69
+ kpis.push({
70
+ name: 'Best Streak',
71
+ value: `${streakData.longestStreak.days} days`,
72
+ trend: streakData.longestStreak.days >= 7 ? 'up' : 'stable',
73
+ trendValue: streakData.currentStreak.isActive ? 'Current active' : 'Historical',
74
+ status: streakData.longestStreak.days >= 14 ? 'green' : streakData.longestStreak.days >= 7 ? 'yellow' : 'red',
75
+ });
76
+ return kpis;
77
+ }
78
+ function generateKeyInsights(commits, streakData, timePattern, commitQuality, velocityAnalysis, gapAnalysis) {
79
+ const insights = [];
80
+ // Velocity insight
81
+ if (velocityAnalysis) {
82
+ if (velocityAnalysis.overallTrend === 'decreasing' && velocityAnalysis.trendPercentage <= -20) {
83
+ insights.push({
84
+ type: 'negative',
85
+ insight: `Velocity has decreased ${Math.abs(velocityAnalysis.trendPercentage)}% over this period`,
86
+ });
87
+ }
88
+ else if (velocityAnalysis.overallTrend === 'increasing' && velocityAnalysis.trendPercentage >= 20) {
89
+ insights.push({
90
+ type: 'positive',
91
+ insight: `Velocity has increased ${velocityAnalysis.trendPercentage}% over this period`,
92
+ });
93
+ }
94
+ // Anomalies
95
+ const criticalAnomalies = velocityAnalysis.anomalies.filter(a => a.severity === 'critical');
96
+ if (criticalAnomalies.length >= 2) {
97
+ insights.push({
98
+ type: 'negative',
99
+ insight: `${criticalAnomalies.length} critical productivity drops detected - potential blockers or disruptions`,
100
+ });
101
+ }
102
+ }
103
+ // Gap insight
104
+ if (gapAnalysis && gapAnalysis.gaps.length > 0) {
105
+ if (gapAnalysis.riskLevel === 'critical' || gapAnalysis.riskLevel === 'high') {
106
+ insights.push({
107
+ type: 'negative',
108
+ insight: `${gapAnalysis.totalGapDays} days lost to activity gaps (${gapAnalysis.percentageOfPeriodInGaps}% of period)`,
109
+ });
110
+ }
111
+ }
112
+ // Quality insight
113
+ if (commitQuality.overallScore >= 8) {
114
+ insights.push({
115
+ type: 'positive',
116
+ insight: 'Excellent commit quality with clear, well-structured messages',
117
+ });
118
+ }
119
+ else if (commitQuality.overallScore < 5) {
120
+ insights.push({
121
+ type: 'negative',
122
+ insight: `Commit quality score of ${Math.round(commitQuality.overallScore * 10) / 10}/10 indicates room for improvement`,
123
+ });
124
+ }
125
+ // Burnout insight
126
+ if (timePattern.burnoutRisk.level === 'high') {
127
+ insights.push({
128
+ type: 'negative',
129
+ insight: 'High burnout risk indicators detected (late nights, irregular hours)',
130
+ });
131
+ }
132
+ // Consistency insight
133
+ if (streakData.activeDayPercentage >= 60) {
134
+ insights.push({
135
+ type: 'positive',
136
+ insight: `Strong consistency with ${Math.round(streakData.activeDayPercentage)}% of days active`,
137
+ });
138
+ }
139
+ // Conventional commits
140
+ if (commitQuality.conventionalCommits.adherence >= 80) {
141
+ insights.push({
142
+ type: 'positive',
143
+ insight: `${Math.round(commitQuality.conventionalCommits.adherence)}% conventional commit adherence enables better automation`,
144
+ });
145
+ }
146
+ // Ensure we have at least 3 insights
147
+ if (insights.length < 3) {
148
+ insights.push({
149
+ type: 'neutral',
150
+ insight: `${commits.length} commits analyzed across ${streakData.totalActiveDays} active days`,
151
+ });
152
+ }
153
+ return insights.slice(0, 5);
154
+ }
155
+ function assessRisks(knowledgeDistribution, gapAnalysis, commitQuality, timePattern) {
156
+ const risks = [];
157
+ // Bus factor risk
158
+ if (knowledgeDistribution) {
159
+ const busFactorLevel = knowledgeDistribution.busFactorRisk.level;
160
+ if (busFactorLevel === 'critical' || busFactorLevel === 'high') {
161
+ risks.push({
162
+ category: 'Knowledge Distribution',
163
+ level: busFactorLevel === 'critical' ? 'high' : 'medium',
164
+ description: `Bus factor risk: ${knowledgeDistribution.knowledgeSilos.length} areas have single-owner knowledge`,
165
+ });
166
+ }
167
+ }
168
+ // Gap/blocker risk
169
+ if (gapAnalysis && gapAnalysis.riskLevel !== 'low') {
170
+ risks.push({
171
+ category: 'Productivity',
172
+ level: gapAnalysis.riskLevel === 'critical' ? 'high' : gapAnalysis.riskLevel,
173
+ description: `${gapAnalysis.gaps.length} activity gaps totaling ${gapAnalysis.totalGapDays} days of lost productivity`,
174
+ });
175
+ }
176
+ // Quality risk
177
+ if (commitQuality && commitQuality.overallScore < 5) {
178
+ risks.push({
179
+ category: 'Code Quality',
180
+ level: commitQuality.overallScore < 3 ? 'high' : 'medium',
181
+ description: 'Low commit quality may indicate rushed work or unclear requirements',
182
+ });
183
+ }
184
+ // Burnout risk
185
+ if (timePattern && timePattern.burnoutRisk.level === 'high') {
186
+ risks.push({
187
+ category: 'Team Health',
188
+ level: 'medium',
189
+ description: `Burnout indicators: ${timePattern.burnoutRisk.indicators.slice(0, 2).join(', ')}`,
190
+ });
191
+ }
192
+ // Sort by level
193
+ const levelOrder = { high: 0, medium: 1, low: 2 };
194
+ risks.sort((a, b) => levelOrder[a.level] - levelOrder[b.level]);
195
+ return risks;
196
+ }
197
+ function generateRecommendations(risks, insights, commitQuality) {
198
+ const recommendations = [];
199
+ // Based on risks
200
+ for (const risk of risks) {
201
+ if (risk.category === 'Knowledge Distribution') {
202
+ recommendations.push('Schedule knowledge sharing sessions to reduce bus factor risk');
203
+ }
204
+ else if (risk.category === 'Productivity') {
205
+ recommendations.push('Investigate root causes of activity gaps - potential blockers or process issues');
206
+ }
207
+ else if (risk.category === 'Code Quality') {
208
+ recommendations.push('Implement commit message guidelines and consider pre-commit hooks');
209
+ }
210
+ else if (risk.category === 'Team Health') {
211
+ recommendations.push('Review workload distribution and consider work-life balance initiatives');
212
+ }
213
+ }
214
+ // Quality-based recommendations
215
+ if (commitQuality.conventionalCommits.adherence < 50) {
216
+ recommendations.push('Adopt conventional commits standard for better changelog generation');
217
+ }
218
+ if (commitQuality.codeHygiene.workInProgress > commitQuality.totalCommits * 0.1) {
219
+ recommendations.push('Reduce WIP commits by encouraging atomic, complete changes');
220
+ }
221
+ // Ensure we have at least one recommendation
222
+ if (recommendations.length === 0) {
223
+ recommendations.push('Continue current practices - metrics are within healthy ranges');
224
+ }
225
+ return [...new Set(recommendations)].slice(0, 4);
226
+ }
227
+ /**
228
+ * Format executive summary for terminal display
229
+ */
230
+ function formatExecutiveSummary(summary) {
231
+ const lines = [];
232
+ lines.push('═══════════════════════════════════════════════════════════════');
233
+ lines.push(` 📊 EXECUTIVE SUMMARY: ${summary.repoName}`);
234
+ lines.push(` Period: ${(0, date_fns_1.format)(summary.period.start, 'MMM d, yyyy')} - ${(0, date_fns_1.format)(summary.period.end, 'MMM d, yyyy')}`);
235
+ lines.push('═══════════════════════════════════════════════════════════════');
236
+ lines.push('');
237
+ // KPIs
238
+ lines.push('📈 KEY PERFORMANCE INDICATORS');
239
+ lines.push('───────────────────────────────────────────────────────────────');
240
+ for (const kpi of summary.kpis) {
241
+ const statusEmoji = kpi.status === 'green' ? '🟢' : kpi.status === 'yellow' ? '🟡' : '🔴';
242
+ const trendEmoji = kpi.trend === 'up' ? '↑' : kpi.trend === 'down' ? '↓' : '→';
243
+ lines.push(` ${statusEmoji} ${kpi.name}: ${kpi.value} ${trendEmoji} ${kpi.trendValue}`);
244
+ }
245
+ lines.push('');
246
+ // Key Insights
247
+ lines.push('💡 KEY INSIGHTS');
248
+ lines.push('───────────────────────────────────────────────────────────────');
249
+ for (const insight of summary.keyInsights) {
250
+ const emoji = insight.type === 'positive' ? '✅' : insight.type === 'negative' ? '⚠️' : 'ℹ️';
251
+ lines.push(` ${emoji} ${insight.insight}`);
252
+ }
253
+ lines.push('');
254
+ // Risks
255
+ if (summary.risks.length > 0) {
256
+ lines.push('🚨 RISK ASSESSMENT');
257
+ lines.push('───────────────────────────────────────────────────────────────');
258
+ for (const risk of summary.risks) {
259
+ const levelEmoji = risk.level === 'high' ? '🔴' : risk.level === 'medium' ? '🟡' : '🟢';
260
+ lines.push(` ${levelEmoji} [${risk.level.toUpperCase()}] ${risk.category}: ${risk.description}`);
261
+ }
262
+ lines.push('');
263
+ }
264
+ // Recommendations
265
+ lines.push('📋 RECOMMENDATIONS');
266
+ lines.push('───────────────────────────────────────────────────────────────');
267
+ for (let i = 0; i < summary.recommendations.length; i++) {
268
+ lines.push(` ${i + 1}. ${summary.recommendations[i]}`);
269
+ }
270
+ lines.push('');
271
+ lines.push('═══════════════════════════════════════════════════════════════');
272
+ lines.push(` Generated: ${(0, date_fns_1.format)(summary.generatedAt, 'MMM d, yyyy HH:mm')}`);
273
+ lines.push('═══════════════════════════════════════════════════════════════');
274
+ return lines;
275
+ }
@@ -4,6 +4,7 @@ exports.analyzeFileHotspots = analyzeFileHotspots;
4
4
  exports.getRiskLevelColor = getRiskLevelColor;
5
5
  exports.getOwnershipIcon = getOwnershipIcon;
6
6
  const child_process_1 = require("child_process");
7
+ const emptyResults_1 = require("./emptyResults");
7
8
  function analyzeFileHotspots(repoPath, startDate, endDate) {
8
9
  try {
9
10
  // Get file changes with author info from git log
@@ -15,10 +16,10 @@ function analyzeFileHotspots(repoPath, startDate, endDate) {
15
16
  }
16
17
  catch (gitError) {
17
18
  // Git command failed, likely not in a git repo or no commits in range
18
- return getEmptyAnalysis();
19
+ return (0, emptyResults_1.getEmptyFileHotspotAnalysis)();
19
20
  }
20
21
  if (!gitLog) {
21
- return getEmptyAnalysis();
22
+ return (0, emptyResults_1.getEmptyFileHotspotAnalysis)();
22
23
  }
23
24
  // Parse git log output
24
25
  const fileMap = new Map();
@@ -91,7 +92,7 @@ function analyzeFileHotspots(repoPath, startDate, endDate) {
91
92
  }
92
93
  catch (error) {
93
94
  console.error('Error analyzing file hotspots:', error);
94
- return getEmptyAnalysis();
95
+ return (0, emptyResults_1.getEmptyFileHotspotAnalysis)();
95
96
  }
96
97
  }
97
98
  function shouldIgnoreFile(path) {
@@ -241,15 +242,6 @@ function analyzeOwnership(files) {
241
242
  .sort((a, b) => b.busFactorRisk - a.busFactorRisk)
242
243
  .slice(0, 10);
243
244
  }
244
- function getEmptyAnalysis() {
245
- return {
246
- topFiles: [],
247
- topDirectories: [],
248
- technicalDebt: [],
249
- ownershipRisks: [],
250
- totalFilesAnalyzed: 0
251
- };
252
- }
253
245
  function getRiskLevelColor(level) {
254
246
  switch (level) {
255
247
  case 'critical': return '🔴';
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeGaps = analyzeGaps;
4
+ exports.formatGapInsights = formatGapInsights;
5
+ const date_fns_1 = require("date-fns");
6
+ const emptyResults_1 = require("./emptyResults");
7
+ const analyzerContextBuilder_1 = require("./analyzerContextBuilder");
8
+ function analyzeGaps(ctxOrCommits, startDate, endDate, thresholdDays = 3, excludeWeekends = false) {
9
+ // Normalize to internal variables
10
+ let commits;
11
+ let start;
12
+ let end;
13
+ let threshold;
14
+ let skipWeekends;
15
+ if ((0, analyzerContextBuilder_1.isAnalyzerContext)(ctxOrCommits)) {
16
+ commits = ctxOrCommits.commits;
17
+ start = ctxOrCommits.dateRange.start;
18
+ end = ctxOrCommits.dateRange.end;
19
+ threshold = (0, analyzerContextBuilder_1.getOption)(ctxOrCommits, 'thresholdDays', 3);
20
+ skipWeekends = (0, analyzerContextBuilder_1.getOption)(ctxOrCommits, 'excludeWeekends', false);
21
+ }
22
+ else {
23
+ commits = ctxOrCommits;
24
+ start = startDate;
25
+ end = endDate;
26
+ threshold = thresholdDays;
27
+ skipWeekends = excludeWeekends;
28
+ }
29
+ if (commits.length === 0) {
30
+ return (0, emptyResults_1.getEmptyGapAnalysis)(start, end);
31
+ }
32
+ // Sort commits by date
33
+ const sortedCommits = [...commits].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
34
+ // Find all gaps
35
+ const gaps = findGaps(sortedCommits, start, end, threshold, skipWeekends);
36
+ // Calculate statistics
37
+ const totalGapDays = gaps.reduce((sum, gap) => sum + gap.durationDays, 0);
38
+ const totalPeriodDays = (0, date_fns_1.differenceInDays)(end, start) + 1;
39
+ const percentageOfPeriodInGaps = totalPeriodDays > 0
40
+ ? Math.round((totalGapDays / totalPeriodDays) * 1000) / 10
41
+ : 0;
42
+ const longestGap = gaps.length > 0
43
+ ? gaps.reduce((max, gap) => gap.durationDays > max.durationDays ? gap : max, gaps[0])
44
+ : null;
45
+ const averageGapLength = gaps.length > 0
46
+ ? Math.round((totalGapDays / gaps.length) * 10) / 10
47
+ : 0;
48
+ // Calculate gap frequency (gaps per month)
49
+ const months = totalPeriodDays / 30;
50
+ const gapFrequency = months > 0 ? Math.round((gaps.length / months) * 10) / 10 : 0;
51
+ // Detect patterns
52
+ const patterns = detectGapPatterns(gaps);
53
+ // Assess risk
54
+ const { riskLevel, riskFactors } = assessGapRisk(gaps, percentageOfPeriodInGaps, longestGap);
55
+ return {
56
+ gaps,
57
+ totalGapDays,
58
+ percentageOfPeriodInGaps,
59
+ longestGap,
60
+ averageGapLength,
61
+ gapFrequency,
62
+ patterns,
63
+ riskLevel,
64
+ riskFactors,
65
+ };
66
+ }
67
+ function findGaps(sortedCommits, startDate, endDate, thresholdDays, excludeWeekends) {
68
+ const gaps = [];
69
+ // Check gap from startDate to first commit
70
+ const firstCommitDate = new Date(sortedCommits[0].date);
71
+ const daysToFirstCommit = calculateDaysBetween(startDate, firstCommitDate, excludeWeekends);
72
+ if (daysToFirstCommit >= thresholdDays) {
73
+ gaps.push({
74
+ start: startDate,
75
+ end: (0, date_fns_1.addDays)(firstCommitDate, -1),
76
+ durationDays: daysToFirstCommit,
77
+ commitBefore: null,
78
+ commitAfter: {
79
+ date: firstCommitDate,
80
+ message: sortedCommits[0].message,
81
+ author: sortedCommits[0].author,
82
+ },
83
+ possibleType: classifyGap(startDate, (0, date_fns_1.addDays)(firstCommitDate, -1), daysToFirstCommit),
84
+ });
85
+ }
86
+ // Check gaps between commits
87
+ for (let i = 0; i < sortedCommits.length - 1; i++) {
88
+ const currentCommit = sortedCommits[i];
89
+ const nextCommit = sortedCommits[i + 1];
90
+ const currentDate = new Date(currentCommit.date);
91
+ const nextDate = new Date(nextCommit.date);
92
+ const daysBetween = calculateDaysBetween(currentDate, nextDate, excludeWeekends);
93
+ if (daysBetween >= thresholdDays) {
94
+ const gapStart = (0, date_fns_1.addDays)(currentDate, 1);
95
+ const gapEnd = (0, date_fns_1.addDays)(nextDate, -1);
96
+ gaps.push({
97
+ start: gapStart,
98
+ end: gapEnd,
99
+ durationDays: daysBetween,
100
+ commitBefore: {
101
+ date: currentDate,
102
+ message: currentCommit.message,
103
+ author: currentCommit.author,
104
+ },
105
+ commitAfter: {
106
+ date: nextDate,
107
+ message: nextCommit.message,
108
+ author: nextCommit.author,
109
+ },
110
+ possibleType: classifyGap(gapStart, gapEnd, daysBetween),
111
+ });
112
+ }
113
+ }
114
+ // Check gap from last commit to endDate
115
+ const lastCommit = sortedCommits[sortedCommits.length - 1];
116
+ const lastCommitDate = new Date(lastCommit.date);
117
+ const daysAfterLast = calculateDaysBetween(lastCommitDate, endDate, excludeWeekends);
118
+ if (daysAfterLast >= thresholdDays) {
119
+ gaps.push({
120
+ start: (0, date_fns_1.addDays)(lastCommitDate, 1),
121
+ end: endDate,
122
+ durationDays: daysAfterLast,
123
+ commitBefore: {
124
+ date: lastCommitDate,
125
+ message: lastCommit.message,
126
+ author: lastCommit.author,
127
+ },
128
+ commitAfter: null,
129
+ possibleType: classifyGap((0, date_fns_1.addDays)(lastCommitDate, 1), endDate, daysAfterLast),
130
+ });
131
+ }
132
+ return gaps;
133
+ }
134
+ function calculateDaysBetween(start, end, excludeWeekends) {
135
+ if (!excludeWeekends) {
136
+ return (0, date_fns_1.differenceInDays)(end, start);
137
+ }
138
+ let days = 0;
139
+ let current = new Date(start);
140
+ while (current < end) {
141
+ if (!(0, date_fns_1.isWeekend)(current)) {
142
+ days++;
143
+ }
144
+ current = (0, date_fns_1.addDays)(current, 1);
145
+ }
146
+ return days;
147
+ }
148
+ function classifyGap(start, end, durationDays) {
149
+ // Check if it's just a weekend
150
+ if (durationDays <= 2) {
151
+ const startIsWeekend = (0, date_fns_1.isWeekend)(start);
152
+ const endIsWeekend = (0, date_fns_1.isWeekend)(end);
153
+ if (startIsWeekend || endIsWeekend) {
154
+ return 'weekend';
155
+ }
156
+ }
157
+ // Check for common holiday periods (rough heuristics)
158
+ const startMonth = start.getMonth();
159
+ const startDay = start.getDate();
160
+ // Christmas/New Year period
161
+ if ((startMonth === 11 && startDay >= 20) || (startMonth === 0 && startDay <= 5)) {
162
+ return 'holiday';
163
+ }
164
+ // Summer vacation period (July-August) with longer gaps
165
+ if ((startMonth === 6 || startMonth === 7) && durationDays >= 5) {
166
+ return 'vacation';
167
+ }
168
+ // Longer gaps are more likely blockers
169
+ if (durationDays >= 7) {
170
+ return 'blocker';
171
+ }
172
+ return 'unknown';
173
+ }
174
+ function detectGapPatterns(gaps) {
175
+ const patterns = [];
176
+ if (gaps.length < 2) {
177
+ return patterns;
178
+ }
179
+ // Check for recurring patterns
180
+ const gapsByDayOfWeek = new Map();
181
+ for (const gap of gaps) {
182
+ const dayOfWeek = gap.start.getDay();
183
+ gapsByDayOfWeek.set(dayOfWeek, (gapsByDayOfWeek.get(dayOfWeek) || 0) + 1);
184
+ }
185
+ // Check if gaps tend to start on specific days (e.g., Mondays = after weekend extension)
186
+ for (const [day, count] of gapsByDayOfWeek) {
187
+ if (count >= 3 && count >= gaps.length * 0.4) {
188
+ const dayName = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][day];
189
+ patterns.push({
190
+ type: 'recurring-weekly',
191
+ description: `Gaps frequently start on ${dayName}s`,
192
+ occurrences: count,
193
+ });
194
+ }
195
+ }
196
+ // Check for blocker pattern (multiple unknown/blocker gaps)
197
+ const blockerGaps = gaps.filter(g => g.possibleType === 'blocker' || g.possibleType === 'unknown');
198
+ if (blockerGaps.length >= 3) {
199
+ patterns.push({
200
+ type: 'random',
201
+ description: `${blockerGaps.length} unexplained gaps detected - potential recurring blockers`,
202
+ occurrences: blockerGaps.length,
203
+ });
204
+ }
205
+ return patterns;
206
+ }
207
+ function assessGapRisk(gaps, percentageInGaps, longestGap) {
208
+ const riskFactors = [];
209
+ let riskScore = 0;
210
+ // Factor 1: Percentage of time in gaps
211
+ if (percentageInGaps >= 30) {
212
+ riskScore += 3;
213
+ riskFactors.push(`${percentageInGaps}% of time spent in activity gaps`);
214
+ }
215
+ else if (percentageInGaps >= 20) {
216
+ riskScore += 2;
217
+ riskFactors.push(`${percentageInGaps}% of time spent in activity gaps`);
218
+ }
219
+ else if (percentageInGaps >= 10) {
220
+ riskScore += 1;
221
+ riskFactors.push(`${percentageInGaps}% of time spent in activity gaps`);
222
+ }
223
+ // Factor 2: Longest gap duration
224
+ if (longestGap) {
225
+ if (longestGap.durationDays >= 21) {
226
+ riskScore += 3;
227
+ riskFactors.push(`Longest gap: ${longestGap.durationDays} days (3+ weeks)`);
228
+ }
229
+ else if (longestGap.durationDays >= 14) {
230
+ riskScore += 2;
231
+ riskFactors.push(`Longest gap: ${longestGap.durationDays} days (2+ weeks)`);
232
+ }
233
+ else if (longestGap.durationDays >= 7) {
234
+ riskScore += 1;
235
+ riskFactors.push(`Longest gap: ${longestGap.durationDays} days (1+ week)`);
236
+ }
237
+ }
238
+ // Factor 3: Number of blocker-type gaps
239
+ const blockerGaps = gaps.filter(g => g.possibleType === 'blocker');
240
+ if (blockerGaps.length >= 5) {
241
+ riskScore += 2;
242
+ riskFactors.push(`${blockerGaps.length} potential blocker gaps identified`);
243
+ }
244
+ else if (blockerGaps.length >= 2) {
245
+ riskScore += 1;
246
+ riskFactors.push(`${blockerGaps.length} potential blocker gaps identified`);
247
+ }
248
+ // Determine risk level
249
+ let riskLevel;
250
+ if (riskScore >= 6) {
251
+ riskLevel = 'critical';
252
+ }
253
+ else if (riskScore >= 4) {
254
+ riskLevel = 'high';
255
+ }
256
+ else if (riskScore >= 2) {
257
+ riskLevel = 'medium';
258
+ }
259
+ else {
260
+ riskLevel = 'low';
261
+ }
262
+ if (riskFactors.length === 0) {
263
+ riskFactors.push('No significant gap patterns detected');
264
+ }
265
+ return { riskLevel, riskFactors };
266
+ }
267
+ /**
268
+ * Format gap analysis for display
269
+ */
270
+ function formatGapInsights(analysis) {
271
+ const insights = [];
272
+ if (analysis.gaps.length === 0) {
273
+ insights.push('✅ No significant activity gaps detected');
274
+ return insights;
275
+ }
276
+ // Summary
277
+ insights.push(`🕳️ ${analysis.gaps.length} activity gap(s) detected`);
278
+ insights.push(`📅 ${analysis.totalGapDays} total gap days (${analysis.percentageOfPeriodInGaps}% of period)`);
279
+ // Longest gap
280
+ if (analysis.longestGap) {
281
+ const startStr = (0, date_fns_1.format)(analysis.longestGap.start, 'MMM d');
282
+ const endStr = (0, date_fns_1.format)(analysis.longestGap.end, 'MMM d, yyyy');
283
+ insights.push(`⏱️ Longest gap: ${analysis.longestGap.durationDays} days (${startStr} - ${endStr})`);
284
+ }
285
+ // Risk assessment
286
+ const riskEmoji = {
287
+ low: '🟢',
288
+ medium: '🟡',
289
+ high: '🟠',
290
+ critical: '🔴',
291
+ }[analysis.riskLevel];
292
+ insights.push(`${riskEmoji} Risk level: ${analysis.riskLevel.toUpperCase()}`);
293
+ // Top risk factors
294
+ for (const factor of analysis.riskFactors.slice(0, 3)) {
295
+ insights.push(` • ${factor}`);
296
+ }
297
+ return insights;
298
+ }