repo-wrapped 0.0.6 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/agents/complete.agent.md +257 -0
- package/.github/agents/feature-scaffold.agent.md +248 -0
- package/.github/agents/jsdoc.agent.md +243 -0
- package/.github/agents/plan.agent.md +202 -0
- package/.github/agents/spec-writer.agent.md +169 -0
- package/.github/agents/test-writer.agent.md +169 -0
- package/.stylelintrc.json +27 -0
- package/README.md +94 -94
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +446 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +446 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +7039 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/commands/generate.js +262 -5
- package/dist/config/defaults.js +158 -0
- package/dist/config/index.js +10 -0
- package/dist/features/achievements/data/achievements.json +284 -0
- package/dist/features/achievements/engine.js +140 -0
- package/dist/features/achievements/evaluators.js +246 -0
- package/dist/features/achievements/helpers.js +58 -0
- package/dist/features/achievements/index.js +57 -0
- package/dist/features/achievements/loader.js +88 -0
- package/dist/features/achievements/template.js +155 -0
- package/dist/features/achievements/types.js +7 -0
- package/dist/features/commit-quality/analyzer.js +378 -0
- package/dist/features/commit-quality/analyzer.test.js +484 -0
- package/dist/features/commit-quality/index.js +28 -0
- package/dist/features/commit-quality/template.js +114 -0
- package/dist/features/commit-quality/types.js +2 -0
- package/dist/features/comparison/analyzer.js +222 -0
- package/dist/features/comparison/index.js +28 -0
- package/dist/features/comparison/template.js +119 -0
- package/dist/features/comparison/types.js +2 -0
- package/dist/features/contribution-graph/index.js +9 -0
- package/dist/features/contribution-graph/template.js +89 -0
- package/dist/features/events/index.js +31 -0
- package/dist/features/events/parser.js +253 -0
- package/dist/features/events/template.js +113 -0
- package/dist/features/events/types.js +2 -0
- package/dist/features/executive-summary/generator.js +275 -0
- package/dist/features/executive-summary/index.js +27 -0
- package/dist/features/executive-summary/template.js +80 -0
- package/dist/features/executive-summary/types.js +2 -0
- package/dist/features/gaps/analyzer.js +298 -0
- package/dist/features/gaps/analyzer.test.js +517 -0
- package/dist/features/gaps/index.js +27 -0
- package/dist/features/gaps/template.js +190 -0
- package/dist/features/gaps/types.js +2 -0
- package/dist/features/impact/analyzer.js +248 -0
- package/dist/features/impact/index.js +26 -0
- package/dist/features/impact/template.js +118 -0
- package/dist/features/impact/types.js +2 -0
- package/dist/features/index.js +40 -0
- package/dist/features/knowledge/analyzer.js +385 -0
- package/dist/features/knowledge/index.js +26 -0
- package/dist/features/knowledge/template.js +239 -0
- package/dist/features/knowledge/types.js +2 -0
- package/dist/features/streaks/calculator.js +184 -0
- package/dist/features/streaks/calculator.test.js +366 -0
- package/dist/features/streaks/index.js +36 -0
- package/dist/features/streaks/template.js +41 -0
- package/dist/features/streaks/types.js +9 -0
- package/dist/features/team/analyzer.js +316 -0
- package/dist/features/team/index.js +30 -0
- package/dist/features/team/template.js +146 -0
- package/dist/features/team/types.js +2 -0
- package/dist/features/time-patterns/analyzer.js +319 -0
- package/dist/features/time-patterns/analyzer.test.js +278 -0
- package/dist/features/time-patterns/index.js +37 -0
- package/dist/features/time-patterns/template.js +109 -0
- package/dist/features/time-patterns/types.js +9 -0
- package/dist/features/velocity/analyzer.js +257 -0
- package/dist/features/velocity/analyzer.test.js +383 -0
- package/dist/features/velocity/index.js +27 -0
- package/dist/features/velocity/template.js +189 -0
- package/dist/features/velocity/types.js +2 -0
- package/dist/generators/html/scripts/knowledge.js +17 -0
- package/dist/generators/html/styles/base.css +10 -6
- package/dist/generators/html/styles/components.css +121 -1
- package/dist/generators/html/styles/knowledge.css +21 -0
- package/dist/generators/html/styles/leaddev.css +1335 -0
- package/dist/generators/html/styles/strategic-insights.css +1337 -0
- package/dist/generators/html/templates/commitQualitySection.js +28 -2
- package/dist/generators/html/templates/comparisonSection.js +119 -0
- package/dist/generators/html/templates/eventsSection.js +113 -0
- package/dist/generators/html/templates/executiveSummarySection.js +80 -0
- package/dist/generators/html/templates/gapSection.js +190 -0
- package/dist/generators/html/templates/impactSection.js +8 -6
- package/dist/generators/html/templates/knowledgeSection.js +16 -2
- package/dist/generators/html/templates/teamSection.js +146 -0
- package/dist/generators/html/templates/velocitySection.js +189 -0
- package/dist/generators/html/types.js +7 -0
- package/dist/generators/html/utils/analysisRunner.js +93 -0
- package/dist/generators/html/utils/cardBuilder.js +47 -0
- package/dist/generators/html/utils/contextBuilder.js +54 -0
- package/dist/generators/html/utils/htmlDocumentBuilder.js +396 -0
- package/dist/generators/html/utils/kpiBuilder.js +76 -0
- package/dist/generators/html/utils/sectionWrapper.js +71 -0
- package/dist/generators/html/utils/styleLoader.js +2 -1
- package/dist/html/analysisRunner.js +93 -0
- package/dist/html/htmlDocumentBuilder.js +396 -0
- package/dist/html/index.js +29 -0
- package/dist/html/shared/colorUtils.js +61 -0
- package/dist/html/shared/commitMapBuilder.js +23 -0
- package/dist/html/shared/components/cardBuilder.js +47 -0
- package/dist/html/shared/components/index.js +18 -0
- package/dist/html/shared/components/kpiBuilder.js +76 -0
- package/dist/html/shared/components/sectionWrapper.js +71 -0
- package/dist/html/shared/contextBuilder.js +54 -0
- package/dist/html/shared/dateRangeCalculator.js +56 -0
- package/dist/html/shared/developerStatsCalculator.js +28 -0
- package/dist/html/shared/index.js +39 -0
- package/dist/html/shared/scriptLoader.js +15 -0
- package/dist/html/shared/scripts/export.js +125 -0
- package/dist/html/shared/scripts/knowledge.js +137 -0
- package/dist/html/shared/scripts/modal.js +68 -0
- package/dist/html/shared/scripts/navigation.js +156 -0
- package/dist/html/shared/scripts/tabs.js +18 -0
- package/dist/html/shared/scripts/tooltip.js +21 -0
- package/dist/html/shared/styleLoader.js +18 -0
- package/dist/html/shared/styles/achievements.css +387 -0
- package/dist/html/shared/styles/base.css +822 -0
- package/dist/html/shared/styles/components.css +1511 -0
- package/dist/html/shared/styles/knowledge.css +242 -0
- package/dist/html/shared/styles/strategic-insights.css +1337 -0
- package/dist/html/shared/weekGrouper.js +27 -0
- package/dist/html/types.js +7 -0
- package/dist/index.js +54 -21
- package/dist/test/helpers/commitFactory.js +166 -0
- package/dist/test/helpers/dateUtils.js +101 -0
- package/dist/test/helpers/index.js +29 -0
- package/dist/test/setup.js +17 -0
- package/dist/test/smoke.test.js +94 -0
- package/dist/types/achievements.js +7 -0
- package/dist/types/analysis.js +7 -0
- package/dist/types/core.js +7 -0
- package/dist/types/index.js +38 -0
- package/dist/types/options.js +7 -0
- package/dist/types/shared.js +7 -0
- package/dist/types/strategic.js +7 -0
- package/dist/types/summary.js +7 -0
- package/dist/utils/achievementDefinitions.js +22 -22
- package/dist/utils/analyzerContextBuilder.js +124 -0
- package/dist/utils/commitQualityAnalyzer.js +13 -2
- package/dist/utils/emptyResults.js +95 -0
- package/dist/utils/eventAnnotationParser.js +253 -0
- package/dist/utils/executiveSummaryGenerator.js +275 -0
- package/dist/utils/fileHotspotAnalyzer.js +4 -12
- package/dist/utils/gapAnalyzer.js +298 -0
- package/dist/utils/gitParser.test.js +363 -0
- package/dist/utils/htmlGenerator.js +126 -450
- package/dist/utils/impactAnalyzer.js +20 -19
- package/dist/utils/knowledgeDistributionAnalyzer.js +32 -27
- package/dist/utils/matrixGenerator.js +13 -13
- package/dist/utils/rangeComparisonAnalyzer.js +222 -0
- package/dist/utils/streakCalculator.js +77 -27
- package/dist/utils/teamAnalyzer.js +316 -0
- package/dist/utils/timePatternAnalyzer.js +18 -3
- package/dist/utils/velocityAnalyzer.js +257 -0
- package/dist/utils/wrappedGenerator.js +8 -8
- package/package.json +74 -55
- package/vitest.config.ts +46 -0
- package/dist/cli.js +0 -24
- package/dist/commands/index.js +0 -24
|
@@ -0,0 +1,316 @@
|
|
|
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.filterCommitsByAuthor = filterCommitsByAuthor;
|
|
37
|
+
exports.parseAuthorFilter = parseAuthorFilter;
|
|
38
|
+
exports.loadTeamFile = loadTeamFile;
|
|
39
|
+
exports.analyzeTeam = analyzeTeam;
|
|
40
|
+
exports.formatTeamInsights = formatTeamInsights;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const commit_quality_1 = require("../commit-quality");
|
|
43
|
+
/**
|
|
44
|
+
* Filters commits by author(s) based on the provided filter
|
|
45
|
+
*/
|
|
46
|
+
function filterCommitsByAuthor(commits, filter) {
|
|
47
|
+
return commits.filter(commit => {
|
|
48
|
+
const authorLower = commit.author.toLowerCase();
|
|
49
|
+
const matches = filter.authors.some(filterAuthor => {
|
|
50
|
+
const filterLower = filterAuthor.toLowerCase();
|
|
51
|
+
if (filter.matchType === 'exact') {
|
|
52
|
+
return authorLower === filterLower;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
return authorLower.includes(filterLower);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return filter.mode === 'include' ? matches : !matches;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Parse author filter from CLI options
|
|
63
|
+
*/
|
|
64
|
+
function parseAuthorFilter(author, authors, excludeAuthor, excludeAuthors) {
|
|
65
|
+
// Include filters take precedence
|
|
66
|
+
if (author) {
|
|
67
|
+
return {
|
|
68
|
+
mode: 'include',
|
|
69
|
+
authors: [author],
|
|
70
|
+
matchType: 'partial',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (authors) {
|
|
74
|
+
return {
|
|
75
|
+
mode: 'include',
|
|
76
|
+
authors: authors.split(',').map(a => a.trim()).filter(a => a.length > 0),
|
|
77
|
+
matchType: 'partial',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Exclude filters
|
|
81
|
+
if (excludeAuthor) {
|
|
82
|
+
return {
|
|
83
|
+
mode: 'exclude',
|
|
84
|
+
authors: [excludeAuthor],
|
|
85
|
+
matchType: 'partial',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (excludeAuthors) {
|
|
89
|
+
return {
|
|
90
|
+
mode: 'exclude',
|
|
91
|
+
authors: excludeAuthors.split(',').map(a => a.trim()).filter(a => a.length > 0),
|
|
92
|
+
matchType: 'partial',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Load team members from a file (one name per line)
|
|
99
|
+
*/
|
|
100
|
+
function loadTeamFile(filePath) {
|
|
101
|
+
try {
|
|
102
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
103
|
+
return content
|
|
104
|
+
.split('\n')
|
|
105
|
+
.map(line => line.trim())
|
|
106
|
+
.filter(line => line.length > 0 && !line.startsWith('#'));
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
throw new Error(`Failed to load team file: ${error.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Analyzes commit patterns and distribution for a team of contributors.
|
|
114
|
+
*
|
|
115
|
+
* Calculates team-level metrics including:
|
|
116
|
+
* - Commit distribution across members (Gini coefficient)
|
|
117
|
+
* - Individual member statistics and rankings
|
|
118
|
+
* - Quality scores and active days
|
|
119
|
+
* - Load balancing insights
|
|
120
|
+
*
|
|
121
|
+
* @param commits - Array of all commit data (will be filtered to team members)
|
|
122
|
+
* @param teamMembers - Array of team member names/patterns to include
|
|
123
|
+
* @param teamName - Optional team name for display
|
|
124
|
+
* @param skipBodyCheck - Whether to skip commit body quality checks (default: true)
|
|
125
|
+
* @returns Team analysis with member breakdown, distribution metrics, and insights
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* const team = analyzeTeam(commits, ['Alice', 'Bob', 'Charlie'], 'Backend Team');
|
|
130
|
+
* console.log(`Team commits: ${team.stats.totalCommits}`);
|
|
131
|
+
* console.log(`Distribution: ${team.loadDistribution.assessment}`);
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
function analyzeTeam(commits, teamMembers, teamName, skipBodyCheck = true) {
|
|
135
|
+
// Filter commits to team members only
|
|
136
|
+
const teamFilter = {
|
|
137
|
+
mode: 'include',
|
|
138
|
+
authors: teamMembers,
|
|
139
|
+
matchType: 'partial',
|
|
140
|
+
};
|
|
141
|
+
const teamCommits = filterCommitsByAuthor(commits, teamFilter);
|
|
142
|
+
// Calculate team metrics
|
|
143
|
+
const totalCommits = teamCommits.length;
|
|
144
|
+
const commitsPerMember = teamMembers.length > 0
|
|
145
|
+
? Math.round((totalCommits / teamMembers.length) * 10) / 10
|
|
146
|
+
: 0;
|
|
147
|
+
// Active days
|
|
148
|
+
const activeDatesSet = new Set(teamCommits.map(c => {
|
|
149
|
+
const d = new Date(c.date);
|
|
150
|
+
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
151
|
+
}));
|
|
152
|
+
const activeDays = activeDatesSet.size;
|
|
153
|
+
// Quality score
|
|
154
|
+
const qualityResult = (0, commit_quality_1.analyzeCommitQuality)(teamCommits, { skipBodyCheck });
|
|
155
|
+
const qualityScore = Math.round(qualityResult.overallScore * 10) / 10;
|
|
156
|
+
// Member breakdown
|
|
157
|
+
const memberBreakdown = calculateMemberBreakdown(teamCommits, teamMembers, skipBodyCheck);
|
|
158
|
+
// Load distribution
|
|
159
|
+
const loadDistribution = calculateLoadDistribution(memberBreakdown, totalCommits);
|
|
160
|
+
return {
|
|
161
|
+
teamName,
|
|
162
|
+
members: teamMembers,
|
|
163
|
+
teamMetrics: {
|
|
164
|
+
totalCommits,
|
|
165
|
+
commitsPerMember,
|
|
166
|
+
activeDays,
|
|
167
|
+
qualityScore,
|
|
168
|
+
},
|
|
169
|
+
memberBreakdown,
|
|
170
|
+
loadDistribution,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function calculateMemberBreakdown(commits, teamMembers, skipBodyCheck) {
|
|
174
|
+
const breakdown = [];
|
|
175
|
+
const totalCommits = commits.length;
|
|
176
|
+
// Build a map of author -> commits
|
|
177
|
+
const commitsByAuthor = new Map();
|
|
178
|
+
for (const commit of commits) {
|
|
179
|
+
const authorLower = commit.author.toLowerCase();
|
|
180
|
+
// Find the matching team member
|
|
181
|
+
const matchedMember = teamMembers.find(member => authorLower.includes(member.toLowerCase()));
|
|
182
|
+
if (matchedMember) {
|
|
183
|
+
if (!commitsByAuthor.has(matchedMember)) {
|
|
184
|
+
commitsByAuthor.set(matchedMember, []);
|
|
185
|
+
}
|
|
186
|
+
commitsByAuthor.get(matchedMember).push(commit);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Calculate stats for each team member
|
|
190
|
+
for (const member of teamMembers) {
|
|
191
|
+
const memberCommits = commitsByAuthor.get(member) || [];
|
|
192
|
+
const commitCount = memberCommits.length;
|
|
193
|
+
const percentage = totalCommits > 0
|
|
194
|
+
? Math.round((commitCount / totalCommits) * 1000) / 10
|
|
195
|
+
: 0;
|
|
196
|
+
// Active days for this member
|
|
197
|
+
const memberActiveDates = new Set(memberCommits.map(c => {
|
|
198
|
+
const d = new Date(c.date);
|
|
199
|
+
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
200
|
+
}));
|
|
201
|
+
// Quality score for this member
|
|
202
|
+
const qualityResult = memberCommits.length > 0
|
|
203
|
+
? (0, commit_quality_1.analyzeCommitQuality)(memberCommits, { skipBodyCheck })
|
|
204
|
+
: { overallScore: 0 };
|
|
205
|
+
// Top areas (directories)
|
|
206
|
+
const areaCounts = new Map();
|
|
207
|
+
for (const commit of memberCommits) {
|
|
208
|
+
// Extract directory from commit message if available (heuristic)
|
|
209
|
+
// In a real implementation, this would use file change data
|
|
210
|
+
const match = commit.message.match(/^(?:feat|fix|docs|refactor|test|chore)\(([^)]+)\)/i);
|
|
211
|
+
if (match) {
|
|
212
|
+
const area = match[1];
|
|
213
|
+
areaCounts.set(area, (areaCounts.get(area) || 0) + 1);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const topAreas = [...areaCounts.entries()]
|
|
217
|
+
.sort((a, b) => b[1] - a[1])
|
|
218
|
+
.slice(0, 3)
|
|
219
|
+
.map(([area]) => area);
|
|
220
|
+
breakdown.push({
|
|
221
|
+
author: member,
|
|
222
|
+
commits: commitCount,
|
|
223
|
+
percentage,
|
|
224
|
+
activeDays: memberActiveDates.size,
|
|
225
|
+
qualityScore: Math.round(qualityResult.overallScore * 10) / 10,
|
|
226
|
+
topAreas,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// Sort by commits descending
|
|
230
|
+
breakdown.sort((a, b) => b.commits - a.commits);
|
|
231
|
+
return breakdown;
|
|
232
|
+
}
|
|
233
|
+
function calculateLoadDistribution(memberBreakdown, totalCommits) {
|
|
234
|
+
const insights = [];
|
|
235
|
+
if (memberBreakdown.length === 0 || totalCommits === 0) {
|
|
236
|
+
return {
|
|
237
|
+
giniCoefficient: 0,
|
|
238
|
+
assessment: 'balanced',
|
|
239
|
+
insights: ['No team data available'],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Calculate Gini coefficient for commit distribution
|
|
243
|
+
const commitCounts = memberBreakdown.map(m => m.commits).sort((a, b) => a - b);
|
|
244
|
+
const n = commitCounts.length;
|
|
245
|
+
let giniNumerator = 0;
|
|
246
|
+
for (let i = 0; i < n; i++) {
|
|
247
|
+
giniNumerator += (2 * (i + 1) - n - 1) * commitCounts[i];
|
|
248
|
+
}
|
|
249
|
+
const giniCoefficient = n > 1
|
|
250
|
+
? Math.round((giniNumerator / (n * totalCommits)) * 100) / 100
|
|
251
|
+
: 0;
|
|
252
|
+
// Determine assessment
|
|
253
|
+
let assessment;
|
|
254
|
+
if (giniCoefficient <= 0.3) {
|
|
255
|
+
assessment = 'balanced';
|
|
256
|
+
insights.push('Work is distributed fairly evenly across team members');
|
|
257
|
+
}
|
|
258
|
+
else if (giniCoefficient <= 0.5) {
|
|
259
|
+
assessment = 'moderate-imbalance';
|
|
260
|
+
insights.push('Some team members are contributing significantly more than others');
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
assessment = 'high-imbalance';
|
|
264
|
+
insights.push('Work is heavily concentrated among a few team members');
|
|
265
|
+
}
|
|
266
|
+
// Additional insights
|
|
267
|
+
const topContributor = memberBreakdown[0];
|
|
268
|
+
if (topContributor && topContributor.percentage > 50) {
|
|
269
|
+
insights.push(`${topContributor.author} accounts for ${topContributor.percentage}% of all commits`);
|
|
270
|
+
}
|
|
271
|
+
const inactiveMembers = memberBreakdown.filter(m => m.commits === 0);
|
|
272
|
+
if (inactiveMembers.length > 0) {
|
|
273
|
+
insights.push(`${inactiveMembers.length} team member(s) have no commits in this period`);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
giniCoefficient,
|
|
277
|
+
assessment,
|
|
278
|
+
insights,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Format team analysis for display
|
|
283
|
+
*/
|
|
284
|
+
function formatTeamInsights(analysis) {
|
|
285
|
+
const insights = [];
|
|
286
|
+
const teamLabel = analysis.teamName || 'Team';
|
|
287
|
+
insights.push(`👥 ${teamLabel} Analysis (${analysis.members.length} members)`);
|
|
288
|
+
insights.push('');
|
|
289
|
+
// Team metrics
|
|
290
|
+
insights.push(`📊 Total commits: ${analysis.teamMetrics.totalCommits}`);
|
|
291
|
+
insights.push(`📅 Active days: ${analysis.teamMetrics.activeDays}`);
|
|
292
|
+
insights.push(`⚡ Avg commits/member: ${analysis.teamMetrics.commitsPerMember}`);
|
|
293
|
+
insights.push(`✨ Quality score: ${analysis.teamMetrics.qualityScore}/10`);
|
|
294
|
+
insights.push('');
|
|
295
|
+
// Load distribution
|
|
296
|
+
const loadEmoji = {
|
|
297
|
+
'balanced': '🟢',
|
|
298
|
+
'moderate-imbalance': '🟡',
|
|
299
|
+
'high-imbalance': '🔴',
|
|
300
|
+
}[analysis.loadDistribution.assessment];
|
|
301
|
+
insights.push(`${loadEmoji} Load distribution: ${analysis.loadDistribution.assessment} (Gini: ${analysis.loadDistribution.giniCoefficient})`);
|
|
302
|
+
for (const insight of analysis.loadDistribution.insights) {
|
|
303
|
+
insights.push(` • ${insight}`);
|
|
304
|
+
}
|
|
305
|
+
insights.push('');
|
|
306
|
+
// Member breakdown (top 5)
|
|
307
|
+
insights.push('👤 Member breakdown:');
|
|
308
|
+
for (const member of analysis.memberBreakdown.slice(0, 5)) {
|
|
309
|
+
const areas = member.topAreas.length > 0 ? ` [${member.topAreas.join(', ')}]` : '';
|
|
310
|
+
insights.push(` • ${member.author}: ${member.commits} commits (${member.percentage}%)${areas}`);
|
|
311
|
+
}
|
|
312
|
+
if (analysis.memberBreakdown.length > 5) {
|
|
313
|
+
insights.push(` ... and ${analysis.memberBreakdown.length - 5} more`);
|
|
314
|
+
}
|
|
315
|
+
return insights;
|
|
316
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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.buildTeamSection = exports.formatTeamInsights = exports.loadTeamFile = exports.parseAuthorFilter = exports.filterCommitsByAuthor = exports.analyzeTeam = void 0;
|
|
18
|
+
/**
|
|
19
|
+
* Team Feature
|
|
20
|
+
* @module features/team
|
|
21
|
+
*/
|
|
22
|
+
__exportStar(require("./types"), exports);
|
|
23
|
+
var analyzer_1 = require("./analyzer");
|
|
24
|
+
Object.defineProperty(exports, "analyzeTeam", { enumerable: true, get: function () { return analyzer_1.analyzeTeam; } });
|
|
25
|
+
Object.defineProperty(exports, "filterCommitsByAuthor", { enumerable: true, get: function () { return analyzer_1.filterCommitsByAuthor; } });
|
|
26
|
+
Object.defineProperty(exports, "parseAuthorFilter", { enumerable: true, get: function () { return analyzer_1.parseAuthorFilter; } });
|
|
27
|
+
Object.defineProperty(exports, "loadTeamFile", { enumerable: true, get: function () { return analyzer_1.loadTeamFile; } });
|
|
28
|
+
Object.defineProperty(exports, "formatTeamInsights", { enumerable: true, get: function () { return analyzer_1.formatTeamInsights; } });
|
|
29
|
+
var template_1 = require("./template");
|
|
30
|
+
Object.defineProperty(exports, "buildTeamSection", { enumerable: true, get: function () { return template_1.buildTeamSection; } });
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildTeamSection = buildTeamSection;
|
|
4
|
+
/**
|
|
5
|
+
* Builds the HTML section for team analysis visualization
|
|
6
|
+
*/
|
|
7
|
+
function buildTeamSection(analysis) {
|
|
8
|
+
if (!analysis || analysis.members.length === 0) {
|
|
9
|
+
return '<p class="no-data">No team data available</p>';
|
|
10
|
+
}
|
|
11
|
+
const { teamMetrics, memberBreakdown, loadDistribution } = analysis;
|
|
12
|
+
// Build summary cards
|
|
13
|
+
const summaryHtml = buildSummaryCards(teamMetrics, analysis.members.length);
|
|
14
|
+
// Build load distribution indicator
|
|
15
|
+
const loadDistHtml = buildLoadDistribution(loadDistribution);
|
|
16
|
+
// Build member breakdown
|
|
17
|
+
const membersHtml = buildMemberList(memberBreakdown);
|
|
18
|
+
// Build insights
|
|
19
|
+
const insightsHtml = buildInsights(loadDistribution.insights);
|
|
20
|
+
return `
|
|
21
|
+
<div class="team-section">
|
|
22
|
+
${summaryHtml}
|
|
23
|
+
${loadDistHtml}
|
|
24
|
+
${membersHtml}
|
|
25
|
+
${insightsHtml}
|
|
26
|
+
</div>
|
|
27
|
+
`;
|
|
28
|
+
}
|
|
29
|
+
function buildSummaryCards(metrics, memberCount) {
|
|
30
|
+
return `
|
|
31
|
+
<div class="team-summary">
|
|
32
|
+
<div class="team-stat-card">
|
|
33
|
+
<span class="stat-value">${metrics.totalCommits.toLocaleString()}</span>
|
|
34
|
+
<span class="stat-label">Total Commits</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="team-stat-card">
|
|
37
|
+
<span class="stat-value">${memberCount}</span>
|
|
38
|
+
<span class="stat-label">Team Members</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="team-stat-card">
|
|
41
|
+
<span class="stat-value">${metrics.commitsPerMember.toFixed(1)}</span>
|
|
42
|
+
<span class="stat-label">Commits/Member</span>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="team-stat-card">
|
|
45
|
+
<span class="stat-value">${metrics.activeDays}</span>
|
|
46
|
+
<span class="stat-label">Active Days</span>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="team-stat-card">
|
|
49
|
+
<span class="stat-value">${metrics.qualityScore.toFixed(1)}</span>
|
|
50
|
+
<span class="stat-label">Avg Quality</span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
function buildLoadDistribution(loadDist) {
|
|
56
|
+
const gini = loadDist.giniCoefficient;
|
|
57
|
+
const percentage = Math.round(gini * 100);
|
|
58
|
+
// Color based on assessment
|
|
59
|
+
let colorClass = 'balanced';
|
|
60
|
+
let statusIcon = '✅';
|
|
61
|
+
if (loadDist.assessment === 'moderate-imbalance') {
|
|
62
|
+
colorClass = 'moderate';
|
|
63
|
+
statusIcon = '⚠️';
|
|
64
|
+
}
|
|
65
|
+
else if (loadDist.assessment === 'high-imbalance') {
|
|
66
|
+
colorClass = 'imbalanced';
|
|
67
|
+
statusIcon = '🚨';
|
|
68
|
+
}
|
|
69
|
+
const assessmentLabel = loadDist.assessment.replace('-', ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
70
|
+
return `
|
|
71
|
+
<div class="load-distribution">
|
|
72
|
+
<h3>Load Distribution</h3>
|
|
73
|
+
<div class="gini-container">
|
|
74
|
+
<div class="gini-bar-bg">
|
|
75
|
+
<div class="gini-bar ${colorClass}" style="width: ${percentage}%"></div>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="gini-info">
|
|
78
|
+
<span class="gini-value">Gini: ${gini.toFixed(2)}</span>
|
|
79
|
+
<span class="gini-assessment ${colorClass}">${statusIcon} ${assessmentLabel}</span>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<p class="gini-explanation">
|
|
83
|
+
${gini < 0.3 ? 'Work is evenly distributed across the team.' :
|
|
84
|
+
gini < 0.5 ? 'Some team members are contributing more than others.' :
|
|
85
|
+
'Work is concentrated among a few contributors. Consider redistributing.'}
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
function buildMemberList(members) {
|
|
91
|
+
// Sort by commits descending
|
|
92
|
+
const sorted = [...members].sort((a, b) => b.commits - a.commits);
|
|
93
|
+
const maxCommits = sorted[0]?.commits || 1;
|
|
94
|
+
const membersHtml = sorted.map((member, index) => {
|
|
95
|
+
const barWidth = (member.commits / maxCommits) * 100;
|
|
96
|
+
const rankBadge = index === 0 ? '👑' : index < 3 ? '⭐' : '';
|
|
97
|
+
return `
|
|
98
|
+
<div class="member-card">
|
|
99
|
+
<div class="member-rank">${index + 1}</div>
|
|
100
|
+
<div class="member-info">
|
|
101
|
+
<div class="member-name">${rankBadge} ${escapeHtml(member.author)}</div>
|
|
102
|
+
<div class="member-areas">${member.topAreas.slice(0, 3).map(a => `<span class="area-tag">${escapeHtml(a)}</span>`).join('')}</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="member-bar-container">
|
|
105
|
+
<div class="member-bar" style="width: ${barWidth}%"></div>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="member-stats">
|
|
108
|
+
<span class="member-commits">${member.commits}</span>
|
|
109
|
+
<span class="member-percentage">(${member.percentage.toFixed(1)}%)</span>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="member-quality" title="Commit quality score">
|
|
112
|
+
<span class="quality-badge">${member.qualityScore.toFixed(1)}</span>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
`;
|
|
116
|
+
}).join('');
|
|
117
|
+
return `
|
|
118
|
+
<div class="member-breakdown">
|
|
119
|
+
<h3>Team Members</h3>
|
|
120
|
+
<div class="member-list">
|
|
121
|
+
${membersHtml}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
function buildInsights(insights) {
|
|
127
|
+
if (!insights || insights.length === 0) {
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
return `
|
|
131
|
+
<div class="team-insights">
|
|
132
|
+
<h3>Insights</h3>
|
|
133
|
+
<ul class="insights-list">
|
|
134
|
+
${insights.map(insight => `<li class="insight-item">💡 ${escapeHtml(insight)}</li>`).join('')}
|
|
135
|
+
</ul>
|
|
136
|
+
</div>
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
function escapeHtml(str) {
|
|
140
|
+
return str
|
|
141
|
+
.replace(/&/g, '&')
|
|
142
|
+
.replace(/</g, '<')
|
|
143
|
+
.replace(/>/g, '>')
|
|
144
|
+
.replace(/"/g, '"')
|
|
145
|
+
.replace(/'/g, ''');
|
|
146
|
+
}
|