repo-wrapped 0.0.2 → 0.0.4

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 (38) hide show
  1. package/dist/commands/generate.js +104 -95
  2. package/dist/constants/chronotypes.js +23 -23
  3. package/dist/constants/colors.js +18 -18
  4. package/dist/constants/index.js +18 -18
  5. package/dist/formatters/index.js +17 -17
  6. package/dist/formatters/timeFormatter.js +28 -29
  7. package/dist/generators/html/templates/achievementsSection.js +42 -43
  8. package/dist/generators/html/templates/commitQualitySection.js +25 -26
  9. package/dist/generators/html/templates/contributionGraph.js +47 -48
  10. package/dist/generators/html/templates/impactSection.js +19 -20
  11. package/dist/generators/html/templates/knowledgeSection.js +86 -87
  12. package/dist/generators/html/templates/streakSection.js +8 -9
  13. package/dist/generators/html/templates/timePatternsSection.js +45 -46
  14. package/dist/generators/html/utils/colorUtils.js +21 -21
  15. package/dist/generators/html/utils/commitMapBuilder.js +23 -24
  16. package/dist/generators/html/utils/dateRangeCalculator.js +56 -57
  17. package/dist/generators/html/utils/developerStatsCalculator.js +28 -29
  18. package/dist/generators/html/utils/scriptLoader.js +15 -16
  19. package/dist/generators/html/utils/styleLoader.js +17 -18
  20. package/dist/generators/html/utils/weekGrouper.js +27 -28
  21. package/dist/index.js +99 -77
  22. package/dist/types/index.js +2 -2
  23. package/dist/utils/achievementDefinitions.js +433 -433
  24. package/dist/utils/achievementEngine.js +169 -170
  25. package/dist/utils/commitQualityAnalyzer.js +367 -368
  26. package/dist/utils/fileHotspotAnalyzer.js +269 -270
  27. package/dist/utils/gitParser.js +136 -125
  28. package/dist/utils/htmlGenerator.js +232 -233
  29. package/dist/utils/impactAnalyzer.js +247 -248
  30. package/dist/utils/knowledgeDistributionAnalyzer.js +373 -374
  31. package/dist/utils/matrixGenerator.js +349 -350
  32. package/dist/utils/slideGenerator.js +170 -171
  33. package/dist/utils/streakCalculator.js +134 -135
  34. package/dist/utils/timePatternAnalyzer.js +304 -305
  35. package/dist/utils/wrappedDisplay.js +124 -115
  36. package/dist/utils/wrappedGenerator.js +376 -377
  37. package/dist/utils/wrappedHtmlGenerator.js +105 -106
  38. package/package.json +10 -10
@@ -1,374 +1,373 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.analyzeKnowledgeDistribution = void 0;
4
- const child_process_1 = require("child_process");
5
- /**
6
- * Extract directory from file path
7
- */
8
- function getDirectory(filePath) {
9
- const parts = filePath.replace(/\\/g, '/').split('/');
10
- if (parts.length <= 1)
11
- return '/';
12
- return parts.slice(0, -1).join('/');
13
- }
14
- /**
15
- * Calculate bus factor risk score
16
- * 1 owner = 10 (critical), 2 = 7 (high), 3 = 4 (medium), 4+ = 1 (low)
17
- */
18
- function calculateRiskScore(significantOwners) {
19
- if (significantOwners === 0)
20
- return 10;
21
- if (significantOwners === 1)
22
- return 10;
23
- if (significantOwners === 2)
24
- return 7;
25
- if (significantOwners === 3)
26
- return 4;
27
- return 1;
28
- }
29
- /**
30
- * Determine ownership type based on contribution percentages
31
- */
32
- function determineOwnershipType(owners) {
33
- if (owners.length === 0)
34
- return 'solo';
35
- const topOwnerPercentage = owners[0]?.percentage || 0;
36
- if (topOwnerPercentage >= 100)
37
- return 'solo';
38
- if (topOwnerPercentage >= 70)
39
- return 'primary';
40
- if (topOwnerPercentage >= 40)
41
- return 'shared';
42
- return 'collaborative';
43
- }
44
- /**
45
- * Get risk level from score
46
- */
47
- function getRiskLevel(score) {
48
- if (score >= 9)
49
- return 'critical';
50
- if (score >= 6)
51
- return 'high';
52
- if (score >= 3)
53
- return 'medium';
54
- return 'low';
55
- }
56
- /**
57
- * Calculate knowledge age in human-readable format
58
- */
59
- function calculateKnowledgeAge(lastActive) {
60
- const now = new Date();
61
- const diffMs = now.getTime() - lastActive.getTime();
62
- const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
63
- if (diffDays < 7)
64
- return `${diffDays}d`;
65
- if (diffDays < 30)
66
- return `${Math.floor(diffDays / 7)}w`;
67
- if (diffDays < 365)
68
- return `${Math.floor(diffDays / 30)}mo`;
69
- return `${Math.floor(diffDays / 365)}y`;
70
- }
71
- /**
72
- * Determine if an area is high risk
73
- * High risk = bus factor 1 OR (bus factor ≤ 2 AND knowledge age > 3 months)
74
- */
75
- function isHighRisk(busFactorRisk, lastActive) {
76
- const daysSinceActive = Math.floor((Date.now() - lastActive.getTime()) / (1000 * 60 * 60 * 24));
77
- const threeMonths = 90;
78
- if (busFactorRisk >= 10)
79
- return true; // Bus factor 1
80
- if (busFactorRisk >= 7 && daysSinceActive > threeMonths)
81
- return true; // Bus factor 2 + stale
82
- return false;
83
- }
84
- /**
85
- * Parse git log with file names for deep analysis
86
- */
87
- function parseGitLogWithFiles(repoPath) {
88
- const fileStats = new Map();
89
- try {
90
- // Get git log with files: format is "hash|author|date" followed by file names
91
- const gitLog = (0, child_process_1.execSync)('git log --name-only --pretty=format:"%H|%an|%ad" --date=iso', { cwd: repoPath, encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
92
- const lines = gitLog.split('\n');
93
- let currentAuthor = '';
94
- let currentDate = new Date();
95
- for (const line of lines) {
96
- if (line.includes('|')) {
97
- // This is a commit header line
98
- const parts = line.split('|');
99
- currentAuthor = parts[1]?.trim() || 'Unknown';
100
- currentDate = new Date(parts[2]?.trim() || Date.now());
101
- }
102
- else if (line.trim()) {
103
- // This is a file path
104
- const filePath = line.trim().replace(/\\/g, '/');
105
- // Skip common non-code files
106
- if (filePath.includes('node_modules/') ||
107
- filePath.includes('.git/') ||
108
- filePath.endsWith('.lock') ||
109
- filePath.endsWith('.log')) {
110
- continue;
111
- }
112
- if (!fileStats.has(filePath)) {
113
- fileStats.set(filePath, { authors: new Map() });
114
- }
115
- const fileData = fileStats.get(filePath);
116
- const authorData = fileData.authors.get(currentAuthor) || { commits: 0, lastActive: currentDate };
117
- fileData.authors.set(currentAuthor, {
118
- commits: authorData.commits + 1,
119
- lastActive: currentDate > authorData.lastActive ? currentDate : authorData.lastActive
120
- });
121
- }
122
- }
123
- }
124
- catch (error) {
125
- // Silent fail - return empty map if git log fails
126
- }
127
- return fileStats;
128
- }
129
- /**
130
- * Build file ownership data for a directory
131
- */
132
- function buildFileOwnership(fileStats, directoryPath) {
133
- const files = [];
134
- fileStats.forEach((data, filePath) => {
135
- const dir = getDirectory(filePath);
136
- if (dir !== directoryPath && !filePath.startsWith(directoryPath + '/')) {
137
- return;
138
- }
139
- const totalCommits = Array.from(data.authors.values()).reduce((sum, a) => sum + a.commits, 0);
140
- let primaryOwner = 'Unknown';
141
- let maxCommits = 0;
142
- let lastModified = new Date(0);
143
- data.authors.forEach((authorData, author) => {
144
- if (authorData.commits > maxCommits) {
145
- maxCommits = authorData.commits;
146
- primaryOwner = author;
147
- }
148
- if (authorData.lastActive > lastModified) {
149
- lastModified = authorData.lastActive;
150
- }
151
- });
152
- const ownershipPercentage = Math.round((maxCommits / totalCommits) * 100);
153
- const knowledgeAge = calculateKnowledgeAge(lastModified);
154
- const significantOwners = Array.from(data.authors.values()).filter(a => (a.commits / totalCommits) >= 0.2).length;
155
- const busFactorRiskScore = calculateRiskScore(significantOwners);
156
- const riskLevel = getRiskLevel(busFactorRiskScore);
157
- files.push({
158
- path: filePath,
159
- primaryOwner,
160
- ownershipPercentage,
161
- lastModified,
162
- knowledgeAge,
163
- riskLevel,
164
- busFactorRisk: busFactorRiskScore >= 7
165
- });
166
- });
167
- // Sort by risk (high risk first), then by ownership percentage (high percentage = more concentrated = more risk)
168
- return files
169
- .filter(f => f.path.startsWith(directoryPath + '/') || getDirectory(f.path) === directoryPath)
170
- .sort((a, b) => {
171
- if (a.busFactorRisk !== b.busFactorRisk)
172
- return a.busFactorRisk ? -1 : 1;
173
- return b.ownershipPercentage - a.ownershipPercentage;
174
- })
175
- .slice(0, 20); // Limit to 20 files per directory
176
- }
177
- /**
178
- * Analyze knowledge distribution across the codebase
179
- */
180
- function analyzeKnowledgeDistribution(commits, repoPath, deepAnalysis = false) {
181
- if (commits.length === 0) {
182
- return getEmptyKnowledgeDistribution();
183
- }
184
- // For deep analysis, parse git log with file names
185
- let fileStats = null;
186
- if (deepAnalysis && repoPath) {
187
- fileStats = parseGitLogWithFiles(repoPath);
188
- }
189
- // Build directory → author → commits mapping
190
- const directoryStats = new Map();
191
- if (fileStats && fileStats.size > 0) {
192
- // Use actual file data for directory stats
193
- fileStats.forEach((data, filePath) => {
194
- const dir = getDirectory(filePath);
195
- if (!directoryStats.has(dir)) {
196
- directoryStats.set(dir, new Map());
197
- }
198
- const dirAuthors = directoryStats.get(dir);
199
- data.authors.forEach((authorData, author) => {
200
- const existing = dirAuthors.get(author) || { commits: 0, lastActive: authorData.lastActive };
201
- dirAuthors.set(author, {
202
- commits: existing.commits + authorData.commits,
203
- lastActive: authorData.lastActive > existing.lastActive ? authorData.lastActive : existing.lastActive
204
- });
205
- });
206
- });
207
- }
208
- else {
209
- // Fallback: use heuristics based on commit messages
210
- commits.forEach(commit => {
211
- const author = commit.author;
212
- const commitDate = new Date(commit.date);
213
- const directories = new Set();
214
- // Try to extract directories from commit message (simplified)
215
- const pathMatch = commit.message.match(/(?:in|for|to)\s+([a-zA-Z0-9_\-./]+)/g);
216
- if (pathMatch) {
217
- pathMatch.forEach(match => {
218
- const path = match.replace(/^(?:in|for|to)\s+/, '');
219
- if (path.includes('/')) {
220
- directories.add(getDirectory(path));
221
- }
222
- });
223
- }
224
- // Default directory based on commit type
225
- if (directories.size === 0) {
226
- const type = commit.message.match(/^(feat|fix|docs|test|refactor)/i)?.[1]?.toLowerCase();
227
- switch (type) {
228
- case 'feat':
229
- case 'fix':
230
- case 'refactor':
231
- directories.add('src');
232
- break;
233
- case 'docs':
234
- directories.add('docs');
235
- break;
236
- case 'test':
237
- directories.add('test');
238
- break;
239
- default:
240
- directories.add('src');
241
- }
242
- }
243
- directories.forEach(dir => {
244
- if (!directoryStats.has(dir)) {
245
- directoryStats.set(dir, new Map());
246
- }
247
- const dirAuthors = directoryStats.get(dir);
248
- const existing = dirAuthors.get(author) || { commits: 0, lastActive: commitDate };
249
- dirAuthors.set(author, {
250
- commits: existing.commits + 1,
251
- lastActive: commitDate > existing.lastActive ? commitDate : existing.lastActive
252
- });
253
- });
254
- });
255
- }
256
- // Build directory ownership array
257
- const directories = [];
258
- const knowledgeSilos = [];
259
- const sharedKnowledge = [];
260
- const criticalPaths = [];
261
- let totalRiskScore = 0;
262
- let dirCount = 0;
263
- directoryStats.forEach((authorStats, dirPath) => {
264
- const totalCommits = Array.from(authorStats.values()).reduce((sum, a) => sum + a.commits, 0);
265
- // Get the latest activity date for the directory
266
- let latestActivity = new Date(0);
267
- authorStats.forEach(stats => {
268
- if (stats.lastActive > latestActivity) {
269
- latestActivity = stats.lastActive;
270
- }
271
- });
272
- const owners = Array.from(authorStats.entries())
273
- .map(([author, stats]) => ({
274
- author,
275
- commits: stats.commits,
276
- percentage: Math.round((stats.commits / totalCommits) * 100),
277
- lastActive: stats.lastActive,
278
- knowledgeAge: calculateKnowledgeAge(stats.lastActive)
279
- }))
280
- .sort((a, b) => b.commits - a.commits);
281
- // Calculate significant owners (>= 20% contribution)
282
- const significantOwners = owners.filter(o => o.percentage >= 20).length;
283
- const busFactorRisk = calculateRiskScore(significantOwners);
284
- const ownershipType = determineOwnershipType(owners);
285
- // Build high-risk files list if deep analysis is enabled
286
- let highRiskFiles;
287
- if (deepAnalysis && fileStats && isHighRisk(busFactorRisk, latestActivity)) {
288
- highRiskFiles = buildFileOwnership(fileStats, dirPath);
289
- }
290
- directories.push({
291
- path: dirPath,
292
- totalCommits,
293
- owners,
294
- ownershipType,
295
- busFactorRisk,
296
- highRiskFiles
297
- });
298
- // Track silos and shared knowledge
299
- if (ownershipType === 'solo') {
300
- knowledgeSilos.push(`${dirPath} — ${owners[0]?.author || 'Unknown'} (100%)`);
301
- criticalPaths.push(dirPath);
302
- }
303
- else if (ownershipType === 'collaborative' || significantOwners >= 3) {
304
- sharedKnowledge.push(dirPath);
305
- }
306
- totalRiskScore += busFactorRisk;
307
- dirCount++;
308
- });
309
- // Calculate overall bus factor risk
310
- const overallRisk = dirCount > 0 ? Math.round(totalRiskScore / dirCount) : 0;
311
- // Generate recommendations
312
- const recommendations = generateRecommendations({
313
- knowledgeSilos,
314
- sharedKnowledge,
315
- directories,
316
- overallRisk
317
- });
318
- return {
319
- directories: directories.sort((a, b) => b.busFactorRisk - a.busFactorRisk).slice(0, 15),
320
- busFactorRisk: {
321
- overall: overallRisk,
322
- level: getRiskLevel(overallRisk),
323
- criticalPaths
324
- },
325
- knowledgeSilos: knowledgeSilos.slice(0, 10),
326
- sharedKnowledge: sharedKnowledge.slice(0, 10),
327
- recommendations,
328
- isDeepAnalysis: deepAnalysis
329
- };
330
- }
331
- exports.analyzeKnowledgeDistribution = analyzeKnowledgeDistribution;
332
- /**
333
- * Generate recommendations based on knowledge distribution
334
- */
335
- function generateRecommendations(data) {
336
- const recommendations = [];
337
- if (data.knowledgeSilos.length > 0) {
338
- recommendations.push(`Consider cross-training on ${data.knowledgeSilos.length} knowledge silo(s) to reduce bus factor risk`);
339
- }
340
- if (data.overallRisk >= 7) {
341
- recommendations.push('High bus factor risk detected — prioritize knowledge sharing sessions');
342
- }
343
- const soloDirectories = data.directories.filter(d => d.ownershipType === 'solo');
344
- if (soloDirectories.length > 3) {
345
- recommendations.push(`${soloDirectories.length} directories have single owners — pair programming recommended`);
346
- }
347
- if (data.sharedKnowledge.length > 0) {
348
- recommendations.push(`Good knowledge distribution in ${data.sharedKnowledge.length} area(s) — maintain this pattern`);
349
- }
350
- const inactiveOwners = data.directories.flatMap(d => d.owners.filter(o => {
351
- const daysSinceActive = Math.floor((Date.now() - o.lastActive.getTime()) / (1000 * 60 * 60 * 24));
352
- return daysSinceActive > 60 && o.percentage > 30;
353
- }));
354
- if (inactiveOwners.length > 0) {
355
- recommendations.push('Some significant contributors have been inactive — ensure knowledge transfer');
356
- }
357
- return recommendations.slice(0, 5);
358
- }
359
- /**
360
- * Return empty distribution for no commits
361
- */
362
- function getEmptyKnowledgeDistribution() {
363
- return {
364
- directories: [],
365
- busFactorRisk: {
366
- overall: 0,
367
- level: 'low',
368
- criticalPaths: []
369
- },
370
- knowledgeSilos: [],
371
- sharedKnowledge: [],
372
- recommendations: ['No commit data available for analysis']
373
- };
374
- }
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeKnowledgeDistribution = analyzeKnowledgeDistribution;
4
+ const child_process_1 = require("child_process");
5
+ /**
6
+ * Extract directory from file path
7
+ */
8
+ function getDirectory(filePath) {
9
+ const parts = filePath.replace(/\\/g, '/').split('/');
10
+ if (parts.length <= 1)
11
+ return '/';
12
+ return parts.slice(0, -1).join('/');
13
+ }
14
+ /**
15
+ * Calculate bus factor risk score
16
+ * 1 owner = 10 (critical), 2 = 7 (high), 3 = 4 (medium), 4+ = 1 (low)
17
+ */
18
+ function calculateRiskScore(significantOwners) {
19
+ if (significantOwners === 0)
20
+ return 10;
21
+ if (significantOwners === 1)
22
+ return 10;
23
+ if (significantOwners === 2)
24
+ return 7;
25
+ if (significantOwners === 3)
26
+ return 4;
27
+ return 1;
28
+ }
29
+ /**
30
+ * Determine ownership type based on contribution percentages
31
+ */
32
+ function determineOwnershipType(owners) {
33
+ if (owners.length === 0)
34
+ return 'solo';
35
+ const topOwnerPercentage = owners[0]?.percentage || 0;
36
+ if (topOwnerPercentage >= 100)
37
+ return 'solo';
38
+ if (topOwnerPercentage >= 70)
39
+ return 'primary';
40
+ if (topOwnerPercentage >= 40)
41
+ return 'shared';
42
+ return 'collaborative';
43
+ }
44
+ /**
45
+ * Get risk level from score
46
+ */
47
+ function getRiskLevel(score) {
48
+ if (score >= 9)
49
+ return 'critical';
50
+ if (score >= 6)
51
+ return 'high';
52
+ if (score >= 3)
53
+ return 'medium';
54
+ return 'low';
55
+ }
56
+ /**
57
+ * Calculate knowledge age in human-readable format
58
+ */
59
+ function calculateKnowledgeAge(lastActive) {
60
+ const now = new Date();
61
+ const diffMs = now.getTime() - lastActive.getTime();
62
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
63
+ if (diffDays < 7)
64
+ return `${diffDays}d`;
65
+ if (diffDays < 30)
66
+ return `${Math.floor(diffDays / 7)}w`;
67
+ if (diffDays < 365)
68
+ return `${Math.floor(diffDays / 30)}mo`;
69
+ return `${Math.floor(diffDays / 365)}y`;
70
+ }
71
+ /**
72
+ * Determine if an area is high risk
73
+ * High risk = bus factor 1 OR (bus factor ≤ 2 AND knowledge age > 3 months)
74
+ */
75
+ function isHighRisk(busFactorRisk, lastActive) {
76
+ const daysSinceActive = Math.floor((Date.now() - lastActive.getTime()) / (1000 * 60 * 60 * 24));
77
+ const threeMonths = 90;
78
+ if (busFactorRisk >= 10)
79
+ return true; // Bus factor 1
80
+ if (busFactorRisk >= 7 && daysSinceActive > threeMonths)
81
+ return true; // Bus factor 2 + stale
82
+ return false;
83
+ }
84
+ /**
85
+ * Parse git log with file names for deep analysis
86
+ */
87
+ function parseGitLogWithFiles(repoPath) {
88
+ const fileStats = new Map();
89
+ try {
90
+ // Get git log with files: format is "hash|author|date" followed by file names
91
+ const gitLog = (0, child_process_1.execSync)('git log --name-only --pretty=format:"%H|%an|%ad" --date=iso', { cwd: repoPath, encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
92
+ const lines = gitLog.split('\n');
93
+ let currentAuthor = '';
94
+ let currentDate = new Date();
95
+ for (const line of lines) {
96
+ if (line.includes('|')) {
97
+ // This is a commit header line
98
+ const parts = line.split('|');
99
+ currentAuthor = parts[1]?.trim() || 'Unknown';
100
+ currentDate = new Date(parts[2]?.trim() || Date.now());
101
+ }
102
+ else if (line.trim()) {
103
+ // This is a file path
104
+ const filePath = line.trim().replace(/\\/g, '/');
105
+ // Skip common non-code files
106
+ if (filePath.includes('node_modules/') ||
107
+ filePath.includes('.git/') ||
108
+ filePath.endsWith('.lock') ||
109
+ filePath.endsWith('.log')) {
110
+ continue;
111
+ }
112
+ if (!fileStats.has(filePath)) {
113
+ fileStats.set(filePath, { authors: new Map() });
114
+ }
115
+ const fileData = fileStats.get(filePath);
116
+ const authorData = fileData.authors.get(currentAuthor) || { commits: 0, lastActive: currentDate };
117
+ fileData.authors.set(currentAuthor, {
118
+ commits: authorData.commits + 1,
119
+ lastActive: currentDate > authorData.lastActive ? currentDate : authorData.lastActive
120
+ });
121
+ }
122
+ }
123
+ }
124
+ catch (error) {
125
+ // Silent fail - return empty map if git log fails
126
+ }
127
+ return fileStats;
128
+ }
129
+ /**
130
+ * Build file ownership data for a directory
131
+ */
132
+ function buildFileOwnership(fileStats, directoryPath) {
133
+ const files = [];
134
+ fileStats.forEach((data, filePath) => {
135
+ const dir = getDirectory(filePath);
136
+ if (dir !== directoryPath && !filePath.startsWith(directoryPath + '/')) {
137
+ return;
138
+ }
139
+ const totalCommits = Array.from(data.authors.values()).reduce((sum, a) => sum + a.commits, 0);
140
+ let primaryOwner = 'Unknown';
141
+ let maxCommits = 0;
142
+ let lastModified = new Date(0);
143
+ data.authors.forEach((authorData, author) => {
144
+ if (authorData.commits > maxCommits) {
145
+ maxCommits = authorData.commits;
146
+ primaryOwner = author;
147
+ }
148
+ if (authorData.lastActive > lastModified) {
149
+ lastModified = authorData.lastActive;
150
+ }
151
+ });
152
+ const ownershipPercentage = Math.round((maxCommits / totalCommits) * 100);
153
+ const knowledgeAge = calculateKnowledgeAge(lastModified);
154
+ const significantOwners = Array.from(data.authors.values()).filter(a => (a.commits / totalCommits) >= 0.2).length;
155
+ const busFactorRiskScore = calculateRiskScore(significantOwners);
156
+ const riskLevel = getRiskLevel(busFactorRiskScore);
157
+ files.push({
158
+ path: filePath,
159
+ primaryOwner,
160
+ ownershipPercentage,
161
+ lastModified,
162
+ knowledgeAge,
163
+ riskLevel,
164
+ busFactorRisk: busFactorRiskScore >= 7
165
+ });
166
+ });
167
+ // Sort by risk (high risk first), then by ownership percentage (high percentage = more concentrated = more risk)
168
+ return files
169
+ .filter(f => f.path.startsWith(directoryPath + '/') || getDirectory(f.path) === directoryPath)
170
+ .sort((a, b) => {
171
+ if (a.busFactorRisk !== b.busFactorRisk)
172
+ return a.busFactorRisk ? -1 : 1;
173
+ return b.ownershipPercentage - a.ownershipPercentage;
174
+ })
175
+ .slice(0, 20); // Limit to 20 files per directory
176
+ }
177
+ /**
178
+ * Analyze knowledge distribution across the codebase
179
+ */
180
+ function analyzeKnowledgeDistribution(commits, repoPath, deepAnalysis = false) {
181
+ if (commits.length === 0) {
182
+ return getEmptyKnowledgeDistribution();
183
+ }
184
+ // For deep analysis, parse git log with file names
185
+ let fileStats = null;
186
+ if (deepAnalysis && repoPath) {
187
+ fileStats = parseGitLogWithFiles(repoPath);
188
+ }
189
+ // Build directory → author → commits mapping
190
+ const directoryStats = new Map();
191
+ if (fileStats && fileStats.size > 0) {
192
+ // Use actual file data for directory stats
193
+ fileStats.forEach((data, filePath) => {
194
+ const dir = getDirectory(filePath);
195
+ if (!directoryStats.has(dir)) {
196
+ directoryStats.set(dir, new Map());
197
+ }
198
+ const dirAuthors = directoryStats.get(dir);
199
+ data.authors.forEach((authorData, author) => {
200
+ const existing = dirAuthors.get(author) || { commits: 0, lastActive: authorData.lastActive };
201
+ dirAuthors.set(author, {
202
+ commits: existing.commits + authorData.commits,
203
+ lastActive: authorData.lastActive > existing.lastActive ? authorData.lastActive : existing.lastActive
204
+ });
205
+ });
206
+ });
207
+ }
208
+ else {
209
+ // Fallback: use heuristics based on commit messages
210
+ commits.forEach(commit => {
211
+ const author = commit.author;
212
+ const commitDate = new Date(commit.date);
213
+ const directories = new Set();
214
+ // Try to extract directories from commit message (simplified)
215
+ const pathMatch = commit.message.match(/(?:in|for|to)\s+([a-zA-Z0-9_\-./]+)/g);
216
+ if (pathMatch) {
217
+ pathMatch.forEach(match => {
218
+ const path = match.replace(/^(?:in|for|to)\s+/, '');
219
+ if (path.includes('/')) {
220
+ directories.add(getDirectory(path));
221
+ }
222
+ });
223
+ }
224
+ // Default directory based on commit type
225
+ if (directories.size === 0) {
226
+ const type = commit.message.match(/^(feat|fix|docs|test|refactor)/i)?.[1]?.toLowerCase();
227
+ switch (type) {
228
+ case 'feat':
229
+ case 'fix':
230
+ case 'refactor':
231
+ directories.add('src');
232
+ break;
233
+ case 'docs':
234
+ directories.add('docs');
235
+ break;
236
+ case 'test':
237
+ directories.add('test');
238
+ break;
239
+ default:
240
+ directories.add('src');
241
+ }
242
+ }
243
+ directories.forEach(dir => {
244
+ if (!directoryStats.has(dir)) {
245
+ directoryStats.set(dir, new Map());
246
+ }
247
+ const dirAuthors = directoryStats.get(dir);
248
+ const existing = dirAuthors.get(author) || { commits: 0, lastActive: commitDate };
249
+ dirAuthors.set(author, {
250
+ commits: existing.commits + 1,
251
+ lastActive: commitDate > existing.lastActive ? commitDate : existing.lastActive
252
+ });
253
+ });
254
+ });
255
+ }
256
+ // Build directory ownership array
257
+ const directories = [];
258
+ const knowledgeSilos = [];
259
+ const sharedKnowledge = [];
260
+ const criticalPaths = [];
261
+ let totalRiskScore = 0;
262
+ let dirCount = 0;
263
+ directoryStats.forEach((authorStats, dirPath) => {
264
+ const totalCommits = Array.from(authorStats.values()).reduce((sum, a) => sum + a.commits, 0);
265
+ // Get the latest activity date for the directory
266
+ let latestActivity = new Date(0);
267
+ authorStats.forEach(stats => {
268
+ if (stats.lastActive > latestActivity) {
269
+ latestActivity = stats.lastActive;
270
+ }
271
+ });
272
+ const owners = Array.from(authorStats.entries())
273
+ .map(([author, stats]) => ({
274
+ author,
275
+ commits: stats.commits,
276
+ percentage: Math.round((stats.commits / totalCommits) * 100),
277
+ lastActive: stats.lastActive,
278
+ knowledgeAge: calculateKnowledgeAge(stats.lastActive)
279
+ }))
280
+ .sort((a, b) => b.commits - a.commits);
281
+ // Calculate significant owners (>= 20% contribution)
282
+ const significantOwners = owners.filter(o => o.percentage >= 20).length;
283
+ const busFactorRisk = calculateRiskScore(significantOwners);
284
+ const ownershipType = determineOwnershipType(owners);
285
+ // Build high-risk files list if deep analysis is enabled
286
+ let highRiskFiles;
287
+ if (deepAnalysis && fileStats && isHighRisk(busFactorRisk, latestActivity)) {
288
+ highRiskFiles = buildFileOwnership(fileStats, dirPath);
289
+ }
290
+ directories.push({
291
+ path: dirPath,
292
+ totalCommits,
293
+ owners,
294
+ ownershipType,
295
+ busFactorRisk,
296
+ highRiskFiles
297
+ });
298
+ // Track silos and shared knowledge
299
+ if (ownershipType === 'solo') {
300
+ knowledgeSilos.push(`${dirPath} — ${owners[0]?.author || 'Unknown'} (100%)`);
301
+ criticalPaths.push(dirPath);
302
+ }
303
+ else if (ownershipType === 'collaborative' || significantOwners >= 3) {
304
+ sharedKnowledge.push(dirPath);
305
+ }
306
+ totalRiskScore += busFactorRisk;
307
+ dirCount++;
308
+ });
309
+ // Calculate overall bus factor risk
310
+ const overallRisk = dirCount > 0 ? Math.round(totalRiskScore / dirCount) : 0;
311
+ // Generate recommendations
312
+ const recommendations = generateRecommendations({
313
+ knowledgeSilos,
314
+ sharedKnowledge,
315
+ directories,
316
+ overallRisk
317
+ });
318
+ return {
319
+ directories: directories.sort((a, b) => b.busFactorRisk - a.busFactorRisk).slice(0, 15),
320
+ busFactorRisk: {
321
+ overall: overallRisk,
322
+ level: getRiskLevel(overallRisk),
323
+ criticalPaths
324
+ },
325
+ knowledgeSilos: knowledgeSilos.slice(0, 10),
326
+ sharedKnowledge: sharedKnowledge.slice(0, 10),
327
+ recommendations,
328
+ isDeepAnalysis: deepAnalysis
329
+ };
330
+ }
331
+ /**
332
+ * Generate recommendations based on knowledge distribution
333
+ */
334
+ function generateRecommendations(data) {
335
+ const recommendations = [];
336
+ if (data.knowledgeSilos.length > 0) {
337
+ recommendations.push(`Consider cross-training on ${data.knowledgeSilos.length} knowledge silo(s) to reduce bus factor risk`);
338
+ }
339
+ if (data.overallRisk >= 7) {
340
+ recommendations.push('High bus factor risk detected — prioritize knowledge sharing sessions');
341
+ }
342
+ const soloDirectories = data.directories.filter(d => d.ownershipType === 'solo');
343
+ if (soloDirectories.length > 3) {
344
+ recommendations.push(`${soloDirectories.length} directories have single owners — pair programming recommended`);
345
+ }
346
+ if (data.sharedKnowledge.length > 0) {
347
+ recommendations.push(`Good knowledge distribution in ${data.sharedKnowledge.length} area(s) — maintain this pattern`);
348
+ }
349
+ const inactiveOwners = data.directories.flatMap(d => d.owners.filter(o => {
350
+ const daysSinceActive = Math.floor((Date.now() - o.lastActive.getTime()) / (1000 * 60 * 60 * 24));
351
+ return daysSinceActive > 60 && o.percentage > 30;
352
+ }));
353
+ if (inactiveOwners.length > 0) {
354
+ recommendations.push('Some significant contributors have been inactive — ensure knowledge transfer');
355
+ }
356
+ return recommendations.slice(0, 5);
357
+ }
358
+ /**
359
+ * Return empty distribution for no commits
360
+ */
361
+ function getEmptyKnowledgeDistribution() {
362
+ return {
363
+ directories: [],
364
+ busFactorRisk: {
365
+ overall: 0,
366
+ level: 'low',
367
+ criticalPaths: []
368
+ },
369
+ knowledgeSilos: [],
370
+ sharedKnowledge: [],
371
+ recommendations: ['No commit data available for analysis']
372
+ };
373
+ }