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,385 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeKnowledgeDistribution = analyzeKnowledgeDistribution;
4
+ const child_process_1 = require("child_process");
5
+ const emptyResults_1 = require("../../utils/emptyResults");
6
+ /**
7
+ * Extract directory from file path
8
+ */
9
+ function getDirectory(filePath) {
10
+ const parts = filePath.replace(/\\/g, '/').split('/');
11
+ if (parts.length <= 1)
12
+ return '/';
13
+ return parts.slice(0, -1).join('/');
14
+ }
15
+ /**
16
+ * Calculate bus factor risk score
17
+ * 1 owner = 10 (critical), 2+ = 1 (low/safe)
18
+ * Adjusted for realistic team dynamics where 2+ knowledgeable owners is considered safe
19
+ */
20
+ function calculateRiskScore(significantOwners) {
21
+ if (significantOwners === 0)
22
+ return 10;
23
+ if (significantOwners === 1)
24
+ return 10;
25
+ return 1; // 2+ owners = low risk
26
+ }
27
+ /**
28
+ * Determine ownership type based on contribution percentages
29
+ */
30
+ function determineOwnershipType(owners) {
31
+ if (owners.length === 0)
32
+ return 'solo';
33
+ const topOwnerPercentage = owners[0]?.percentage || 0;
34
+ if (topOwnerPercentage >= 100)
35
+ return 'solo';
36
+ if (topOwnerPercentage >= 70)
37
+ return 'primary';
38
+ if (topOwnerPercentage >= 40)
39
+ return 'shared';
40
+ return 'collaborative';
41
+ }
42
+ /**
43
+ * Get risk level from score
44
+ */
45
+ function getRiskLevel(score) {
46
+ if (score >= 9)
47
+ return 'critical';
48
+ if (score >= 6)
49
+ return 'high';
50
+ if (score >= 3)
51
+ return 'medium';
52
+ return 'low';
53
+ }
54
+ /**
55
+ * Calculate knowledge age in human-readable format
56
+ */
57
+ function calculateKnowledgeAge(lastActive) {
58
+ const now = new Date();
59
+ const diffMs = now.getTime() - lastActive.getTime();
60
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
61
+ if (diffDays < 7)
62
+ return `${diffDays}d`;
63
+ if (diffDays < 30)
64
+ return `${Math.floor(diffDays / 7)}w`;
65
+ if (diffDays < 365)
66
+ return `${Math.floor(diffDays / 30)}mo`;
67
+ return `${Math.floor(diffDays / 365)}y`;
68
+ }
69
+ /**
70
+ * Determine if an area is high risk
71
+ * High risk = bus factor 1 OR (bus factor ≤ 2 AND knowledge age > 3 months)
72
+ */
73
+ function isHighRisk(busFactorRisk, lastActive) {
74
+ const daysSinceActive = Math.floor((Date.now() - lastActive.getTime()) / (1000 * 60 * 60 * 24));
75
+ const threeMonths = 90;
76
+ if (busFactorRisk >= 10)
77
+ return true; // Bus factor 1
78
+ if (busFactorRisk >= 7 && daysSinceActive > threeMonths)
79
+ return true; // Bus factor 2 + stale
80
+ return false;
81
+ }
82
+ /**
83
+ * Parse git log with file names for deep analysis
84
+ */
85
+ function parseGitLogWithFiles(repoPath) {
86
+ const fileStats = new Map();
87
+ try {
88
+ // Get git log with files: format is "hash|author|date" followed by file names
89
+ 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 });
90
+ const lines = gitLog.split('\n');
91
+ let currentAuthor = '';
92
+ let currentDate = new Date();
93
+ for (const line of lines) {
94
+ if (line.includes('|')) {
95
+ // This is a commit header line
96
+ const parts = line.split('|');
97
+ currentAuthor = parts[1]?.trim() || 'Unknown';
98
+ currentDate = new Date(parts[2]?.trim() || Date.now());
99
+ }
100
+ else if (line.trim()) {
101
+ // This is a file path
102
+ const filePath = line.trim().replace(/\\/g, '/');
103
+ // Skip common non-code files
104
+ if (filePath.includes('node_modules/') ||
105
+ filePath.includes('.git/') ||
106
+ filePath.endsWith('.lock') ||
107
+ filePath.endsWith('.log')) {
108
+ continue;
109
+ }
110
+ if (!fileStats.has(filePath)) {
111
+ fileStats.set(filePath, { authors: new Map() });
112
+ }
113
+ const fileData = fileStats.get(filePath);
114
+ const authorData = fileData.authors.get(currentAuthor) || { commits: 0, lastActive: currentDate };
115
+ fileData.authors.set(currentAuthor, {
116
+ commits: authorData.commits + 1,
117
+ lastActive: currentDate > authorData.lastActive ? currentDate : authorData.lastActive
118
+ });
119
+ }
120
+ }
121
+ }
122
+ catch (error) {
123
+ // Silent fail - return empty map if git log fails
124
+ }
125
+ return fileStats;
126
+ }
127
+ /**
128
+ * Build file ownership data for a directory
129
+ */
130
+ function buildFileOwnership(fileStats, directoryPath) {
131
+ const files = [];
132
+ fileStats.forEach((data, filePath) => {
133
+ const dir = getDirectory(filePath);
134
+ if (dir !== directoryPath && !filePath.startsWith(directoryPath + '/')) {
135
+ return;
136
+ }
137
+ const totalCommits = Array.from(data.authors.values()).reduce((sum, a) => sum + a.commits, 0);
138
+ let primaryOwner = 'Unknown';
139
+ let maxCommits = 0;
140
+ let lastModified = new Date(0);
141
+ data.authors.forEach((authorData, author) => {
142
+ if (authorData.commits > maxCommits) {
143
+ maxCommits = authorData.commits;
144
+ primaryOwner = author;
145
+ }
146
+ if (authorData.lastActive > lastModified) {
147
+ lastModified = authorData.lastActive;
148
+ }
149
+ });
150
+ const ownershipPercentage = Math.round((maxCommits / totalCommits) * 100);
151
+ const knowledgeAge = calculateKnowledgeAge(lastModified);
152
+ const significantOwners = Array.from(data.authors.values()).filter(a => (a.commits / totalCommits) >= 0.65).length;
153
+ const busFactorRiskScore = calculateRiskScore(significantOwners);
154
+ const riskLevel = getRiskLevel(busFactorRiskScore);
155
+ files.push({
156
+ path: filePath,
157
+ primaryOwner,
158
+ ownershipPercentage,
159
+ lastModified,
160
+ knowledgeAge,
161
+ riskLevel,
162
+ busFactorRisk: busFactorRiskScore >= 7
163
+ });
164
+ });
165
+ // Sort by risk (high risk first), then by ownership percentage (high percentage = more concentrated = more risk)
166
+ return files
167
+ .filter(f => f.path.startsWith(directoryPath + '/') || getDirectory(f.path) === directoryPath)
168
+ .sort((a, b) => {
169
+ if (a.busFactorRisk !== b.busFactorRisk)
170
+ return a.busFactorRisk ? -1 : 1;
171
+ return b.ownershipPercentage - a.ownershipPercentage;
172
+ })
173
+ .slice(0, 20); // Limit to 20 files per directory
174
+ }
175
+ /**
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
+ * ```
195
+ */
196
+ function analyzeKnowledgeDistribution(commits, repoPath, deepAnalysis = false) {
197
+ if (commits.length === 0) {
198
+ return (0, emptyResults_1.getEmptyKnowledgeDistribution)();
199
+ }
200
+ // For deep analysis, parse git log with file names
201
+ let fileStats = null;
202
+ if (deepAnalysis && repoPath) {
203
+ fileStats = parseGitLogWithFiles(repoPath);
204
+ }
205
+ // Build directory → author → commits mapping
206
+ const directoryStats = new Map();
207
+ if (fileStats && fileStats.size > 0) {
208
+ // Use actual file data for directory stats
209
+ fileStats.forEach((data, filePath) => {
210
+ const dir = getDirectory(filePath);
211
+ if (!directoryStats.has(dir)) {
212
+ directoryStats.set(dir, new Map());
213
+ }
214
+ const dirAuthors = directoryStats.get(dir);
215
+ data.authors.forEach((authorData, author) => {
216
+ const existing = dirAuthors.get(author) || { commits: 0, lastActive: authorData.lastActive };
217
+ dirAuthors.set(author, {
218
+ commits: existing.commits + authorData.commits,
219
+ lastActive: authorData.lastActive > existing.lastActive ? authorData.lastActive : existing.lastActive
220
+ });
221
+ });
222
+ });
223
+ }
224
+ else {
225
+ // Fallback: use heuristics based on commit messages
226
+ commits.forEach(commit => {
227
+ const author = commit.author;
228
+ const commitDate = new Date(commit.date);
229
+ const directories = new Set();
230
+ // Try to extract directories from commit message (simplified)
231
+ const pathMatch = commit.message.match(/(?:in|for|to)\s+([a-zA-Z0-9_\-./]+)/g);
232
+ if (pathMatch) {
233
+ pathMatch.forEach(match => {
234
+ const path = match.replace(/^(?:in|for|to)\s+/, '');
235
+ if (path.includes('/')) {
236
+ directories.add(getDirectory(path));
237
+ }
238
+ });
239
+ }
240
+ // Default directory based on commit type
241
+ if (directories.size === 0) {
242
+ const type = commit.message.match(/^(feat|fix|docs|test|refactor)/i)?.[1]?.toLowerCase();
243
+ switch (type) {
244
+ case 'feat':
245
+ case 'fix':
246
+ case 'refactor':
247
+ directories.add('src');
248
+ break;
249
+ case 'docs':
250
+ directories.add('docs');
251
+ break;
252
+ case 'test':
253
+ directories.add('test');
254
+ break;
255
+ default:
256
+ directories.add('src');
257
+ }
258
+ }
259
+ directories.forEach(dir => {
260
+ if (!directoryStats.has(dir)) {
261
+ directoryStats.set(dir, new Map());
262
+ }
263
+ const dirAuthors = directoryStats.get(dir);
264
+ const existing = dirAuthors.get(author) || { commits: 0, lastActive: commitDate };
265
+ dirAuthors.set(author, {
266
+ commits: existing.commits + 1,
267
+ lastActive: commitDate > existing.lastActive ? commitDate : existing.lastActive
268
+ });
269
+ });
270
+ });
271
+ }
272
+ // Build directory ownership array
273
+ const directories = [];
274
+ const knowledgeSilos = [];
275
+ const sharedKnowledge = [];
276
+ const criticalPaths = [];
277
+ let totalRiskScore = 0;
278
+ let dirCount = 0;
279
+ directoryStats.forEach((authorStats, dirPath) => {
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
+ }
286
+ // Get the latest and earliest activity date for the directory
287
+ let latestActivity = new Date(0);
288
+ let earliestActivity = new Date();
289
+ authorStats.forEach(stats => {
290
+ if (stats.lastActive > latestActivity) {
291
+ latestActivity = stats.lastActive;
292
+ }
293
+ if (stats.lastActive < earliestActivity) {
294
+ earliestActivity = stats.lastActive;
295
+ }
296
+ });
297
+ // Calculate directory age in days
298
+ const dirAgeInDays = Math.floor((Date.now() - earliestActivity.getTime()) / (1000 * 60 * 60 * 24));
299
+ const MIN_AGE_FOR_SILO = 14; // Don't flag new directories as silos
300
+ const owners = Array.from(authorStats.entries())
301
+ .map(([author, stats]) => ({
302
+ author,
303
+ commits: stats.commits,
304
+ percentage: Math.round((stats.commits / totalCommits) * 100),
305
+ lastActive: stats.lastActive,
306
+ knowledgeAge: calculateKnowledgeAge(stats.lastActive)
307
+ }))
308
+ .sort((a, b) => b.commits - a.commits);
309
+ // Calculate significant owners (>= 65% contribution)
310
+ const significantOwners = owners.filter(o => o.percentage >= 65).length;
311
+ const busFactorRisk = calculateRiskScore(significantOwners);
312
+ const ownershipType = determineOwnershipType(owners);
313
+ // Build high-risk files list if deep analysis is enabled
314
+ let highRiskFiles;
315
+ if (deepAnalysis && fileStats && isHighRisk(busFactorRisk, latestActivity)) {
316
+ highRiskFiles = buildFileOwnership(fileStats, dirPath);
317
+ }
318
+ directories.push({
319
+ path: dirPath,
320
+ totalCommits,
321
+ owners,
322
+ ownershipType,
323
+ busFactorRisk,
324
+ highRiskFiles
325
+ });
326
+ // Track silos and shared knowledge (only if directory is old enough)
327
+ if (ownershipType === 'solo' && dirAgeInDays >= MIN_AGE_FOR_SILO) {
328
+ knowledgeSilos.push(`${dirPath} — ${owners[0]?.author || 'Unknown'} (100%)`);
329
+ criticalPaths.push(dirPath);
330
+ }
331
+ else if (ownershipType === 'collaborative' || significantOwners >= 3) {
332
+ sharedKnowledge.push(dirPath);
333
+ }
334
+ totalRiskScore += busFactorRisk;
335
+ dirCount++;
336
+ });
337
+ // Calculate overall bus factor risk
338
+ const overallRisk = dirCount > 0 ? Math.round(totalRiskScore / dirCount) : 0;
339
+ // Generate recommendations
340
+ const recommendations = generateRecommendations({
341
+ knowledgeSilos,
342
+ sharedKnowledge,
343
+ directories,
344
+ overallRisk
345
+ });
346
+ return {
347
+ directories: directories.sort((a, b) => b.busFactorRisk - a.busFactorRisk).slice(0, 15),
348
+ busFactorRisk: {
349
+ overall: overallRisk,
350
+ level: getRiskLevel(overallRisk),
351
+ criticalPaths
352
+ },
353
+ knowledgeSilos: knowledgeSilos.slice(0, 10),
354
+ sharedKnowledge: sharedKnowledge.slice(0, 10),
355
+ recommendations,
356
+ isDeepAnalysis: deepAnalysis
357
+ };
358
+ }
359
+ /**
360
+ * Generate recommendations based on knowledge distribution
361
+ */
362
+ function generateRecommendations(data) {
363
+ const recommendations = [];
364
+ if (data.knowledgeSilos.length > 0) {
365
+ recommendations.push(`Consider cross-training on ${data.knowledgeSilos.length} knowledge silo(s) to reduce bus factor risk`);
366
+ }
367
+ if (data.overallRisk >= 7) {
368
+ recommendations.push('High bus factor risk detected — prioritize knowledge sharing sessions');
369
+ }
370
+ const soloDirectories = data.directories.filter(d => d.ownershipType === 'solo');
371
+ if (soloDirectories.length > 3) {
372
+ recommendations.push(`${soloDirectories.length} directories have single owners — pair programming recommended`);
373
+ }
374
+ if (data.sharedKnowledge.length > 0) {
375
+ recommendations.push(`Good knowledge distribution in ${data.sharedKnowledge.length} area(s) — maintain this pattern`);
376
+ }
377
+ const inactiveOwners = data.directories.flatMap(d => d.owners.filter(o => {
378
+ const daysSinceActive = Math.floor((Date.now() - o.lastActive.getTime()) / (1000 * 60 * 60 * 24));
379
+ return daysSinceActive > 60 && o.percentage > 30;
380
+ }));
381
+ if (inactiveOwners.length > 0) {
382
+ recommendations.push('Some significant contributors have been inactive — ensure knowledge transfer');
383
+ }
384
+ return recommendations.slice(0, 5);
385
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.buildKnowledgeSection = exports.analyzeKnowledgeDistribution = void 0;
18
+ /**
19
+ * Knowledge Distribution Feature
20
+ * @module features/knowledge
21
+ */
22
+ __exportStar(require("./types"), exports);
23
+ var analyzer_1 = require("./analyzer");
24
+ Object.defineProperty(exports, "analyzeKnowledgeDistribution", { enumerable: true, get: function () { return analyzer_1.analyzeKnowledgeDistribution; } });
25
+ var template_1 = require("./template");
26
+ Object.defineProperty(exports, "buildKnowledgeSection", { enumerable: true, get: function () { return template_1.buildKnowledgeSection; } });
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildKnowledgeSection = buildKnowledgeSection;
4
+ /**
5
+ * Build the Knowledge Distribution section for the dashboard
6
+ */
7
+ function buildKnowledgeSection(knowledge) {
8
+ const riskClass = `risk-${knowledge.busFactorRisk.level}`;
9
+ const isDeepAnalysis = knowledge.isDeepAnalysis || false;
10
+ return `
11
+ <div class="knowledge-section">
12
+ <h2>Knowledge Distribution ${isDeepAnalysis ? '<span class="deep-analysis-badge">Deep Analysis</span>' : ''}</h2>
13
+
14
+ <div class="bus-factor-summary">
15
+ <div class="risk-indicator ${riskClass}">
16
+ <span class="risk-value">${knowledge.busFactorRisk.overall}</span>
17
+ <span class="risk-scale">/10</span>
18
+ </div>
19
+ <div class="risk-meta">
20
+ <span class="risk-label">Bus Factor Risk</span>
21
+ <span class="risk-level">${formatRiskLevel(knowledge.busFactorRisk.level)}</span>
22
+ </div>
23
+ </div>
24
+
25
+ ${knowledge.directories.length > 0 ? `
26
+ <div class="ownership-table">
27
+ <h3>Directory Ownership</h3>
28
+ <table class="data-table knowledge-table">
29
+ <thead>
30
+ <tr>
31
+ <th ${isDeepAnalysis ? 'class="expandable-header"' : ''}>Directory</th>
32
+ <th>Primary Owner</th>
33
+ <th>Ownership</th>
34
+ <th>Last Activity</th>
35
+ <th>Type</th>
36
+ <th>Risk</th>
37
+ </tr>
38
+ </thead>
39
+ <tbody>
40
+ ${knowledge.directories.slice(0, 10).map((dir, idx) => buildDirectoryRow(dir, idx, isDeepAnalysis)).join('')}
41
+ </tbody>
42
+ </table>
43
+ </div>
44
+ ` : ''}
45
+
46
+ ${knowledge.knowledgeSilos.length > 0 ? `
47
+ <div class="knowledge-warnings">
48
+ <h3>Knowledge Silos</h3>
49
+ <p class="warning-intro">These directories have single owners — consider cross-training:</p>
50
+ <ul class="silo-list">
51
+ ${knowledge.knowledgeSilos.map(silo => `<li>${silo}</li>`).join('')}
52
+ </ul>
53
+ </div>
54
+ ` : ''}
55
+
56
+ ${knowledge.sharedKnowledge.length > 0 ? `
57
+ <div class="shared-knowledge">
58
+ <h3>Well-Distributed Knowledge</h3>
59
+ <p class="success-intro">These areas have good knowledge sharing:</p>
60
+ <ul class="shared-list">
61
+ ${knowledge.sharedKnowledge.map(area => `<li>${area}</li>`).join('')}
62
+ </ul>
63
+ </div>
64
+ ` : ''}
65
+
66
+ ${knowledge.recommendations.length > 0 ? `
67
+ <div class="recommendations">
68
+ <h3>Recommendations</h3>
69
+ <ul>
70
+ ${knowledge.recommendations.map(rec => `<li>${rec}</li>`).join('')}
71
+ </ul>
72
+ </div>
73
+ ` : ''}
74
+ </div>
75
+ `;
76
+ }
77
+ /**
78
+ * Build a directory row with optional expandable details
79
+ */
80
+ function buildDirectoryRow(dir, index, isDeepAnalysis) {
81
+ const hasExpandableContent = isDeepAnalysis && dir.highRiskFiles && dir.highRiskFiles.length > 0;
82
+ const knowledgeAge = dir.owners[0]?.knowledgeAge || '—';
83
+ const rowId = `knowledge-row-${index}`;
84
+ // Main directory row
85
+ const mainRow = `
86
+ <tr class="${hasExpandableContent ? 'expandable-row' : ''}" ${hasExpandableContent ? `data-row-id="${rowId}" role="button" aria-expanded="false"` : ''}>
87
+ <td class="dir-path">
88
+ ${hasExpandableContent ? '<span class="expand-icon">▶</span>' : ''}
89
+ ${dir.path || '/'}
90
+ </td>
91
+ <td>${dir.owners[0]?.author || 'Unknown'}</td>
92
+ <td>
93
+ <div class="ownership-bar-mini">
94
+ <div class="ownership-fill" style="width: ${dir.owners[0]?.percentage || 0}%"></div>
95
+ </div>
96
+ <span class="ownership-percent">${dir.owners[0]?.percentage || 0}%</span>
97
+ </td>
98
+ <td class="last-activity">${knowledgeAge}</td>
99
+ <td><span class="ownership-type type-${dir.ownershipType}">${formatOwnershipType(dir.ownershipType)}</span></td>
100
+ <td><span class="risk-badge risk-${getRiskClass(dir.busFactorRisk)}">${dir.busFactorRisk}</span></td>
101
+ </tr>
102
+ `;
103
+ // Expandable details row (contributor breakdown + file list)
104
+ if (hasExpandableContent) {
105
+ const detailsRow = `
106
+ <tr class="expanded-content hidden" id="${rowId}-content">
107
+ <td colspan="6">
108
+ <div class="expansion-panel">
109
+ ${buildContributorBreakdown(dir)}
110
+ ${buildFileList(dir.highRiskFiles)}
111
+ </div>
112
+ </td>
113
+ </tr>
114
+ `;
115
+ return mainRow + detailsRow;
116
+ }
117
+ return mainRow;
118
+ }
119
+ /**
120
+ * Build contributor breakdown for expanded row
121
+ */
122
+ function buildContributorBreakdown(dir) {
123
+ if (!dir.owners || dir.owners.length === 0)
124
+ return '';
125
+ return `
126
+ <div class="contributor-breakdown">
127
+ <h4>Contributors</h4>
128
+ <div class="contributors-list">
129
+ ${dir.owners.slice(0, 5).map(owner => `
130
+ <div class="contributor-item">
131
+ <span class="contributor-name">${owner.author}</span>
132
+ <div class="contributor-stats">
133
+ <span class="contributor-commits">${owner.commits} commits</span>
134
+ <span class="contributor-percent">${owner.percentage}%</span>
135
+ <span class="contributor-age" title="Knowledge age">${owner.knowledgeAge || '—'}</span>
136
+ </div>
137
+ <div class="contributor-bar">
138
+ <div class="contributor-fill" style="width: ${owner.percentage}%"></div>
139
+ </div>
140
+ </div>
141
+ `).join('')}
142
+ </div>
143
+ </div>
144
+ `;
145
+ }
146
+ /**
147
+ * Build file list for expanded row (high-risk files only)
148
+ */
149
+ function buildFileList(files) {
150
+ if (!files || files.length === 0)
151
+ return '';
152
+ const visibleCount = 10;
153
+ const hasMore = files.length > visibleCount;
154
+ const uniqueId = `files-${Math.random().toString(36).substr(2, 9)}`;
155
+ return `
156
+ <div class="file-breakdown">
157
+ <h4>High-Risk Files</h4>
158
+ <table class="file-table">
159
+ <thead>
160
+ <tr>
161
+ <th>File</th>
162
+ <th>Primary Owner</th>
163
+ <th>Ownership</th>
164
+ <th>Last Modified</th>
165
+ <th>Risk</th>
166
+ </tr>
167
+ </thead>
168
+ <tbody>
169
+ ${files.slice(0, visibleCount).map(file => `
170
+ <tr class="${file.busFactorRisk ? 'high-risk-file' : ''}">
171
+ <td class="file-path" title="${file.path}">${getFileName(file.path)}</td>
172
+ <td class="file-owner">${file.primaryOwner}</td>
173
+ <td>
174
+ <span class="file-ownership">${file.ownershipPercentage}%</span>
175
+ </td>
176
+ <td class="file-age">${file.knowledgeAge}</td>
177
+ <td><span class="risk-badge risk-${file.riskLevel}">${file.riskLevel}</span></td>
178
+ </tr>
179
+ `).join('')}
180
+ ${hasMore ? files.slice(visibleCount).map(file => `
181
+ <tr class="${file.busFactorRisk ? 'high-risk-file' : ''} hidden-file" data-group="${uniqueId}" style="display: none;">
182
+ <td class="file-path" title="${file.path}">${getFileName(file.path)}</td>
183
+ <td class="file-owner">${file.primaryOwner}</td>
184
+ <td>
185
+ <span class="file-ownership">${file.ownershipPercentage}%</span>
186
+ </td>
187
+ <td class="file-age">${file.knowledgeAge}</td>
188
+ <td><span class="risk-badge risk-${file.riskLevel}">${file.riskLevel}</span></td>
189
+ </tr>
190
+ `).join('') : ''}
191
+ </tbody>
192
+ </table>
193
+ ${hasMore ? `<button class="more-files-btn" data-target="${uniqueId}" data-count="${files.length - visibleCount}" onclick="toggleMoreFiles(this)">+ ${files.length - visibleCount} more files</button>` : ''}
194
+ </div>
195
+ `;
196
+ }
197
+ /**
198
+ * Get just the filename from a path
199
+ */
200
+ function getFileName(path) {
201
+ const parts = path.replace(/\\/g, '/').split('/');
202
+ return parts[parts.length - 1] || path;
203
+ }
204
+ /**
205
+ * Format risk level for display
206
+ */
207
+ function formatRiskLevel(level) {
208
+ const labels = {
209
+ critical: 'Critical',
210
+ high: 'High Risk',
211
+ medium: 'Moderate',
212
+ low: 'Low Risk'
213
+ };
214
+ return labels[level] || level;
215
+ }
216
+ /**
217
+ * Format ownership type for display
218
+ */
219
+ function formatOwnershipType(type) {
220
+ const labels = {
221
+ solo: 'Solo',
222
+ primary: 'Primary',
223
+ shared: 'Shared',
224
+ collaborative: 'Team'
225
+ };
226
+ return labels[type] || type;
227
+ }
228
+ /**
229
+ * Get risk class based on numeric value
230
+ */
231
+ function getRiskClass(risk) {
232
+ if (risk >= 9)
233
+ return 'critical';
234
+ if (risk >= 6)
235
+ return 'high';
236
+ if (risk >= 3)
237
+ return 'medium';
238
+ return 'low';
239
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });