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,368 +1,367 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getQualityLevel = exports.getQualityRating = exports.analyzeCommitQuality = void 0;
4
- const CONVENTIONAL_COMMIT_REGEX = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?!?:\s.+/;
5
- const CODE_HYGIENE_PATTERNS = {
6
- quickFix: /fix(ed)?\s+(typo|spelling|grammar)/i,
7
- workInProgress: /\bwip\b|work in progress/i,
8
- oops: /oops|whoops|my bad/i,
9
- profanity: /\b(damn|shit|fuck|crap)\b/i,
10
- vague: /^(test|testing|fix|update|change|refactor)s?$/i,
11
- singleChar: /^.$/
12
- };
13
- const ISSUE_PATTERNS = [
14
- /#\d+/,
15
- /[A-Z]+-\d+/,
16
- /closes?\s+#\d+/i,
17
- /fixes?\s+#\d+/i,
18
- /resolves?\s+#\d+/i // Resolves #123
19
- ];
20
- const IMPERATIVE_STARTERS = [
21
- 'add', 'fix', 'update', 'remove', 'delete', 'create', 'implement', 'refactor',
22
- 'improve', 'optimize', 'enhance', 'change', 'move', 'rename', 'extract',
23
- 'merge', 'bump', 'revert', 'document', 'test', 'upgrade', 'downgrade'
24
- ];
25
- function analyzeCommitQuality(commits, options = {}) {
26
- const { skipBodyCheck = false } = options;
27
- if (commits.length === 0) {
28
- return getEmptyQuality();
29
- }
30
- let totalSubjectLength = 0;
31
- let tooShort = 0;
32
- let tooLong = 0;
33
- let withPeriod = 0;
34
- let imperativeMood = 0;
35
- let withBody = 0;
36
- let totalBodyLength = 0;
37
- let wellFormattedBody = 0;
38
- let conventionalCount = 0;
39
- let withIssueRef = 0;
40
- let totalIssueRefs = 0;
41
- const typeMap = new Map();
42
- const scopeSet = new Set();
43
- let breakingChanges = 0;
44
- // Code hygiene metrics
45
- let quickFix = 0;
46
- let workInProgress = 0;
47
- let oops = 0;
48
- let profanity = 0;
49
- let vague = 0;
50
- let singleChar = 0;
51
- commits.forEach(commit => {
52
- const lines = commit.message.split('\n').map(l => l.trim()).filter(l => l);
53
- const subject = lines[0] || '';
54
- const body = lines.slice(1).join('\n').trim();
55
- // Subject analysis
56
- totalSubjectLength += subject.length;
57
- if (subject.length < 10)
58
- tooShort++;
59
- if (subject.length > 72)
60
- tooLong++;
61
- if (subject.endsWith('.'))
62
- withPeriod++;
63
- if (startsWithImperative(subject))
64
- imperativeMood++;
65
- // Body analysis
66
- if (body.length > 20) {
67
- withBody++;
68
- totalBodyLength += body.length;
69
- if (isWellFormatted(body))
70
- wellFormattedBody++;
71
- }
72
- // Conventional commits
73
- const conventionalMatch = subject.match(CONVENTIONAL_COMMIT_REGEX);
74
- if (conventionalMatch) {
75
- conventionalCount++;
76
- const type = conventionalMatch[1];
77
- typeMap.set(type, (typeMap.get(type) || 0) + 1);
78
- const scope = conventionalMatch[2];
79
- if (scope) {
80
- scopeSet.add(scope.replace(/[()]/g, ''));
81
- }
82
- if (conventionalMatch[0].includes('!') || body.toLowerCase().includes('breaking change')) {
83
- breakingChanges++;
84
- }
85
- }
86
- // Issue references
87
- const issueRefs = countIssueReferences(commit.message);
88
- if (issueRefs > 0) {
89
- withIssueRef++;
90
- totalIssueRefs += issueRefs;
91
- }
92
- // Code hygiene patterns
93
- if (CODE_HYGIENE_PATTERNS.quickFix.test(commit.message))
94
- quickFix++;
95
- if (CODE_HYGIENE_PATTERNS.workInProgress.test(commit.message))
96
- workInProgress++;
97
- if (CODE_HYGIENE_PATTERNS.oops.test(commit.message))
98
- oops++;
99
- if (CODE_HYGIENE_PATTERNS.profanity.test(commit.message))
100
- profanity++;
101
- if (CODE_HYGIENE_PATTERNS.vague.test(subject))
102
- vague++;
103
- if (CODE_HYGIENE_PATTERNS.singleChar.test(subject))
104
- singleChar++;
105
- });
106
- // Calculate scores
107
- const avgSubjectLength = totalSubjectLength / commits.length;
108
- const subjectScore = calculateSubjectScore(avgSubjectLength, tooShort, tooLong, withPeriod, imperativeMood, commits.length);
109
- const avgBodyLength = withBody > 0 ? totalBodyLength / withBody : 0;
110
- const bodyScore = skipBodyCheck ? 10 : calculateBodyScore(withBody, wellFormattedBody, commits.length);
111
- const conventionAdherence = (conventionalCount / commits.length) * 100;
112
- const conventionScore = (conventionAdherence / 10);
113
- const issueRefScore = (withIssueRef / commits.length) * 10;
114
- const overallScore = (subjectScore + bodyScore + conventionScore + issueRefScore) / 4;
115
- // Type distribution
116
- const typeDistribution = {
117
- features: typeMap.get('feat') || 0,
118
- fixes: typeMap.get('fix') || 0,
119
- docs: typeMap.get('docs') || 0,
120
- refactors: typeMap.get('refactor') || 0,
121
- tests: typeMap.get('test') || 0,
122
- chores: typeMap.get('chore') || 0,
123
- other: commits.length - conventionalCount
124
- };
125
- // Generate improvements
126
- const improvements = generateRecommendations({
127
- totalCommits: commits.length,
128
- withBody,
129
- conventionAdherence,
130
- tooLong,
131
- withIssueRef,
132
- typeDistribution,
133
- skipBodyCheck
134
- });
135
- return {
136
- overallScore,
137
- totalCommits: commits.length,
138
- subjectQuality: {
139
- score: subjectScore,
140
- avgLength: Math.round(avgSubjectLength),
141
- tooShort,
142
- tooLong,
143
- withPeriod,
144
- imperativeMood
145
- },
146
- bodyQuality: {
147
- score: bodyScore,
148
- withBody,
149
- avgBodyLength: Math.round(avgBodyLength),
150
- wellFormatted: wellFormattedBody
151
- },
152
- conventionalCommits: {
153
- adherence: conventionAdherence,
154
- types: Object.fromEntries(typeMap),
155
- scopes: Array.from(scopeSet),
156
- breakingChanges
157
- },
158
- issueReferences: {
159
- withReferences: withIssueRef,
160
- patterns: extractIssuePatterns(commits),
161
- avgReferencesPerCommit: totalIssueRefs / commits.length
162
- },
163
- codeHygiene: {
164
- quickFixes: quickFix,
165
- workInProgress,
166
- oops,
167
- profanity,
168
- vague,
169
- singleChar
170
- },
171
- typeDistribution,
172
- improvements
173
- };
174
- }
175
- exports.analyzeCommitQuality = analyzeCommitQuality;
176
- function startsWithImperative(subject) {
177
- const firstWord = subject.toLowerCase().split(/[\s:(]/)[0];
178
- // Check conventional commit prefix
179
- if (CONVENTIONAL_COMMIT_REGEX.test(subject)) {
180
- const afterColon = subject.split(':')[1]?.trim().toLowerCase();
181
- if (afterColon) {
182
- const afterColonFirstWord = afterColon.split(/[\s:(]/)[0];
183
- return IMPERATIVE_STARTERS.includes(afterColonFirstWord);
184
- }
185
- }
186
- return IMPERATIVE_STARTERS.includes(firstWord);
187
- }
188
- function isWellFormatted(body) {
189
- const lines = body.split('\n');
190
- // Check if most lines are under 80 characters
191
- const longLines = lines.filter(l => l.length > 80).length;
192
- const wellFormattedRatio = 1 - (longLines / lines.length);
193
- // Check for proper paragraphs (blank lines between sections)
194
- const hasProperParagraphs = body.includes('\n\n');
195
- return wellFormattedRatio >= 0.8 || hasProperParagraphs;
196
- }
197
- function countIssueReferences(message) {
198
- let count = 0;
199
- ISSUE_PATTERNS.forEach(pattern => {
200
- const matches = message.match(new RegExp(pattern, 'g'));
201
- if (matches)
202
- count += matches.length;
203
- });
204
- return count;
205
- }
206
- function extractIssuePatterns(commits) {
207
- const patterns = new Set();
208
- commits.forEach(commit => {
209
- ISSUE_PATTERNS.forEach(pattern => {
210
- const matches = commit.message.match(new RegExp(pattern, 'g'));
211
- if (matches) {
212
- matches.forEach(match => patterns.add(match));
213
- }
214
- });
215
- });
216
- return Array.from(patterns).slice(0, 10); // Return top 10 patterns
217
- }
218
- function calculateSubjectScore(avgLength, tooShort, tooLong, withPeriod, imperativeMood, total) {
219
- let score = 10;
220
- // Length penalty
221
- if (avgLength < 20)
222
- score -= 1;
223
- if (avgLength > 60)
224
- score -= 1;
225
- // Too short/long penalty
226
- const tooShortRatio = tooShort / total;
227
- const tooLongRatio = tooLong / total;
228
- score -= tooShortRatio * 2;
229
- score -= tooLongRatio * 2;
230
- // Period penalty
231
- const periodRatio = withPeriod / total;
232
- score -= periodRatio * 1;
233
- // Imperative mood bonus
234
- const imperativeRatio = imperativeMood / total;
235
- if (imperativeRatio < 0.5)
236
- score -= 2;
237
- else if (imperativeRatio < 0.7)
238
- score -= 1;
239
- return Math.max(0, Math.min(10, score));
240
- }
241
- function calculateBodyScore(withBody, wellFormatted, total) {
242
- const bodyRatio = withBody / total;
243
- const formattedRatio = withBody > 0 ? wellFormatted / withBody : 0;
244
- let score = 0;
245
- // Body presence (up to 6 points)
246
- score += bodyRatio * 6;
247
- // Formatting quality (up to 4 points)
248
- score += formattedRatio * 4;
249
- return Math.max(0, Math.min(10, score));
250
- }
251
- function generateRecommendations(data) {
252
- const tips = [];
253
- // Body recommendations (skip if body check is disabled)
254
- if (!data.skipBodyCheck) {
255
- const bodyRatio = data.withBody / data.totalCommits;
256
- if (bodyRatio < 0.3) {
257
- tips.push('Add commit bodies to explain "why" not just "what"');
258
- }
259
- else if (bodyRatio < 0.5) {
260
- tips.push('Try adding more detailed commit descriptions');
261
- }
262
- }
263
- // Convention recommendations
264
- if (data.conventionAdherence < 50) {
265
- tips.push('Consider adopting Conventional Commits format');
266
- }
267
- else if (data.conventionAdherence < 70) {
268
- tips.push('Increase conventional commit usage for better changelog generation');
269
- }
270
- // Length recommendations
271
- if (data.tooLong > data.totalCommits * 0.1) {
272
- tips.push('Keep subject lines under 72 characters');
273
- }
274
- // Issue reference recommendations
275
- const issueRefRatio = data.withIssueRef / data.totalCommits;
276
- if (issueRefRatio < 0.3) {
277
- tips.push('Link commits to issues/tickets for better traceability');
278
- }
279
- // Test recommendations
280
- const testRatio = data.typeDistribution.tests / data.totalCommits;
281
- if (testRatio < 0.05) {
282
- tips.push('Add more test commits - aim for 10-15% test coverage');
283
- }
284
- // Refactor recommendations
285
- const refactorRatio = data.typeDistribution.refactors / data.totalCommits;
286
- if (refactorRatio < 0.05) {
287
- tips.push('Schedule regular refactoring - aim for 10-15% refactor commits');
288
- }
289
- // Balance recommendations
290
- const featureFixRatio = data.typeDistribution.features / (data.typeDistribution.fixes || 1);
291
- if (featureFixRatio > 3) {
292
- tips.push('High feature/fix ratio - consider addressing technical debt');
293
- }
294
- return tips;
295
- }
296
- function getEmptyQuality() {
297
- return {
298
- overallScore: 0,
299
- totalCommits: 0,
300
- subjectQuality: {
301
- score: 0,
302
- avgLength: 0,
303
- tooShort: 0,
304
- tooLong: 0,
305
- withPeriod: 0,
306
- imperativeMood: 0
307
- },
308
- bodyQuality: {
309
- score: 0,
310
- withBody: 0,
311
- avgBodyLength: 0,
312
- wellFormatted: 0
313
- },
314
- conventionalCommits: {
315
- adherence: 0,
316
- types: {},
317
- scopes: [],
318
- breakingChanges: 0
319
- },
320
- issueReferences: {
321
- withReferences: 0,
322
- patterns: [],
323
- avgReferencesPerCommit: 0
324
- },
325
- codeHygiene: {
326
- quickFixes: 0,
327
- workInProgress: 0,
328
- oops: 0,
329
- profanity: 0,
330
- vague: 0,
331
- singleChar: 0
332
- },
333
- typeDistribution: {
334
- features: 0,
335
- fixes: 0,
336
- docs: 0,
337
- refactors: 0,
338
- tests: 0,
339
- chores: 0,
340
- other: 0
341
- },
342
- improvements: []
343
- };
344
- }
345
- function getQualityRating(score) {
346
- if (score >= 9)
347
- return '⭐⭐⭐⭐⭐';
348
- if (score >= 7.5)
349
- return '⭐⭐⭐⭐';
350
- if (score >= 6)
351
- return '⭐⭐⭐';
352
- if (score >= 4)
353
- return '⭐⭐';
354
- return '';
355
- }
356
- exports.getQualityRating = getQualityRating;
357
- function getQualityLevel(score) {
358
- if (score >= 9)
359
- return 'Excellent';
360
- if (score >= 7.5)
361
- return 'Good';
362
- if (score >= 6)
363
- return 'Fair';
364
- if (score >= 4)
365
- return 'Needs Improvement';
366
- return 'Poor';
367
- }
368
- exports.getQualityLevel = getQualityLevel;
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeCommitQuality = analyzeCommitQuality;
4
+ exports.getQualityRating = getQualityRating;
5
+ exports.getQualityLevel = getQualityLevel;
6
+ const CONVENTIONAL_COMMIT_REGEX = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?!?:\s.+/;
7
+ const CODE_HYGIENE_PATTERNS = {
8
+ quickFix: /fix(ed)?\s+(typo|spelling|grammar)/i,
9
+ workInProgress: /\bwip\b|work in progress/i,
10
+ oops: /oops|whoops|my bad/i,
11
+ profanity: /\b(damn|shit|fuck|crap)\b/i,
12
+ vague: /^(test|testing|fix|update|change|refactor)s?$/i,
13
+ singleChar: /^.$/
14
+ };
15
+ const ISSUE_PATTERNS = [
16
+ /#\d+/, // GitHub: #123
17
+ /[A-Z]+-\d+/, // JIRA: PROJ-123
18
+ /closes?\s+#\d+/i, // Closes #123
19
+ /fixes?\s+#\d+/i, // Fixes #123
20
+ /resolves?\s+#\d+/i // Resolves #123
21
+ ];
22
+ const IMPERATIVE_STARTERS = [
23
+ 'add', 'fix', 'update', 'remove', 'delete', 'create', 'implement', 'refactor',
24
+ 'improve', 'optimize', 'enhance', 'change', 'move', 'rename', 'extract',
25
+ 'merge', 'bump', 'revert', 'document', 'test', 'upgrade', 'downgrade'
26
+ ];
27
+ function analyzeCommitQuality(commits, options = {}) {
28
+ const { skipBodyCheck = false } = options;
29
+ if (commits.length === 0) {
30
+ return getEmptyQuality();
31
+ }
32
+ let totalSubjectLength = 0;
33
+ let tooShort = 0;
34
+ let tooLong = 0;
35
+ let withPeriod = 0;
36
+ let imperativeMood = 0;
37
+ let withBody = 0;
38
+ let totalBodyLength = 0;
39
+ let wellFormattedBody = 0;
40
+ let conventionalCount = 0;
41
+ let withIssueRef = 0;
42
+ let totalIssueRefs = 0;
43
+ const typeMap = new Map();
44
+ const scopeSet = new Set();
45
+ let breakingChanges = 0;
46
+ // Code hygiene metrics
47
+ let quickFix = 0;
48
+ let workInProgress = 0;
49
+ let oops = 0;
50
+ let profanity = 0;
51
+ let vague = 0;
52
+ let singleChar = 0;
53
+ commits.forEach(commit => {
54
+ const lines = commit.message.split('\n').map(l => l.trim()).filter(l => l);
55
+ const subject = lines[0] || '';
56
+ const body = lines.slice(1).join('\n').trim();
57
+ // Subject analysis
58
+ totalSubjectLength += subject.length;
59
+ if (subject.length < 10)
60
+ tooShort++;
61
+ if (subject.length > 72)
62
+ tooLong++;
63
+ if (subject.endsWith('.'))
64
+ withPeriod++;
65
+ if (startsWithImperative(subject))
66
+ imperativeMood++;
67
+ // Body analysis
68
+ if (body.length > 20) {
69
+ withBody++;
70
+ totalBodyLength += body.length;
71
+ if (isWellFormatted(body))
72
+ wellFormattedBody++;
73
+ }
74
+ // Conventional commits
75
+ const conventionalMatch = subject.match(CONVENTIONAL_COMMIT_REGEX);
76
+ if (conventionalMatch) {
77
+ conventionalCount++;
78
+ const type = conventionalMatch[1];
79
+ typeMap.set(type, (typeMap.get(type) || 0) + 1);
80
+ const scope = conventionalMatch[2];
81
+ if (scope) {
82
+ scopeSet.add(scope.replace(/[()]/g, ''));
83
+ }
84
+ if (conventionalMatch[0].includes('!') || body.toLowerCase().includes('breaking change')) {
85
+ breakingChanges++;
86
+ }
87
+ }
88
+ // Issue references
89
+ const issueRefs = countIssueReferences(commit.message);
90
+ if (issueRefs > 0) {
91
+ withIssueRef++;
92
+ totalIssueRefs += issueRefs;
93
+ }
94
+ // Code hygiene patterns
95
+ if (CODE_HYGIENE_PATTERNS.quickFix.test(commit.message))
96
+ quickFix++;
97
+ if (CODE_HYGIENE_PATTERNS.workInProgress.test(commit.message))
98
+ workInProgress++;
99
+ if (CODE_HYGIENE_PATTERNS.oops.test(commit.message))
100
+ oops++;
101
+ if (CODE_HYGIENE_PATTERNS.profanity.test(commit.message))
102
+ profanity++;
103
+ if (CODE_HYGIENE_PATTERNS.vague.test(subject))
104
+ vague++;
105
+ if (CODE_HYGIENE_PATTERNS.singleChar.test(subject))
106
+ singleChar++;
107
+ });
108
+ // Calculate scores
109
+ const avgSubjectLength = totalSubjectLength / commits.length;
110
+ const subjectScore = calculateSubjectScore(avgSubjectLength, tooShort, tooLong, withPeriod, imperativeMood, commits.length);
111
+ const avgBodyLength = withBody > 0 ? totalBodyLength / withBody : 0;
112
+ const bodyScore = skipBodyCheck ? 10 : calculateBodyScore(withBody, wellFormattedBody, commits.length);
113
+ const conventionAdherence = (conventionalCount / commits.length) * 100;
114
+ const conventionScore = (conventionAdherence / 10);
115
+ const issueRefScore = (withIssueRef / commits.length) * 10;
116
+ const overallScore = (subjectScore + bodyScore + conventionScore + issueRefScore) / 4;
117
+ // Type distribution
118
+ const typeDistribution = {
119
+ features: typeMap.get('feat') || 0,
120
+ fixes: typeMap.get('fix') || 0,
121
+ docs: typeMap.get('docs') || 0,
122
+ refactors: typeMap.get('refactor') || 0,
123
+ tests: typeMap.get('test') || 0,
124
+ chores: typeMap.get('chore') || 0,
125
+ other: commits.length - conventionalCount
126
+ };
127
+ // Generate improvements
128
+ const improvements = generateRecommendations({
129
+ totalCommits: commits.length,
130
+ withBody,
131
+ conventionAdherence,
132
+ tooLong,
133
+ withIssueRef,
134
+ typeDistribution,
135
+ skipBodyCheck
136
+ });
137
+ return {
138
+ overallScore,
139
+ totalCommits: commits.length,
140
+ subjectQuality: {
141
+ score: subjectScore,
142
+ avgLength: Math.round(avgSubjectLength),
143
+ tooShort,
144
+ tooLong,
145
+ withPeriod,
146
+ imperativeMood
147
+ },
148
+ bodyQuality: {
149
+ score: bodyScore,
150
+ withBody,
151
+ avgBodyLength: Math.round(avgBodyLength),
152
+ wellFormatted: wellFormattedBody
153
+ },
154
+ conventionalCommits: {
155
+ adherence: conventionAdherence,
156
+ types: Object.fromEntries(typeMap),
157
+ scopes: Array.from(scopeSet),
158
+ breakingChanges
159
+ },
160
+ issueReferences: {
161
+ withReferences: withIssueRef,
162
+ patterns: extractIssuePatterns(commits),
163
+ avgReferencesPerCommit: totalIssueRefs / commits.length
164
+ },
165
+ codeHygiene: {
166
+ quickFixes: quickFix,
167
+ workInProgress,
168
+ oops,
169
+ profanity,
170
+ vague,
171
+ singleChar
172
+ },
173
+ typeDistribution,
174
+ improvements
175
+ };
176
+ }
177
+ function startsWithImperative(subject) {
178
+ const firstWord = subject.toLowerCase().split(/[\s:(]/)[0];
179
+ // Check conventional commit prefix
180
+ if (CONVENTIONAL_COMMIT_REGEX.test(subject)) {
181
+ const afterColon = subject.split(':')[1]?.trim().toLowerCase();
182
+ if (afterColon) {
183
+ const afterColonFirstWord = afterColon.split(/[\s:(]/)[0];
184
+ return IMPERATIVE_STARTERS.includes(afterColonFirstWord);
185
+ }
186
+ }
187
+ return IMPERATIVE_STARTERS.includes(firstWord);
188
+ }
189
+ function isWellFormatted(body) {
190
+ const lines = body.split('\n');
191
+ // Check if most lines are under 80 characters
192
+ const longLines = lines.filter(l => l.length > 80).length;
193
+ const wellFormattedRatio = 1 - (longLines / lines.length);
194
+ // Check for proper paragraphs (blank lines between sections)
195
+ const hasProperParagraphs = body.includes('\n\n');
196
+ return wellFormattedRatio >= 0.8 || hasProperParagraphs;
197
+ }
198
+ function countIssueReferences(message) {
199
+ let count = 0;
200
+ ISSUE_PATTERNS.forEach(pattern => {
201
+ const matches = message.match(new RegExp(pattern, 'g'));
202
+ if (matches)
203
+ count += matches.length;
204
+ });
205
+ return count;
206
+ }
207
+ function extractIssuePatterns(commits) {
208
+ const patterns = new Set();
209
+ commits.forEach(commit => {
210
+ ISSUE_PATTERNS.forEach(pattern => {
211
+ const matches = commit.message.match(new RegExp(pattern, 'g'));
212
+ if (matches) {
213
+ matches.forEach(match => patterns.add(match));
214
+ }
215
+ });
216
+ });
217
+ return Array.from(patterns).slice(0, 10); // Return top 10 patterns
218
+ }
219
+ function calculateSubjectScore(avgLength, tooShort, tooLong, withPeriod, imperativeMood, total) {
220
+ let score = 10;
221
+ // Length penalty
222
+ if (avgLength < 20)
223
+ score -= 1;
224
+ if (avgLength > 60)
225
+ score -= 1;
226
+ // Too short/long penalty
227
+ const tooShortRatio = tooShort / total;
228
+ const tooLongRatio = tooLong / total;
229
+ score -= tooShortRatio * 2;
230
+ score -= tooLongRatio * 2;
231
+ // Period penalty
232
+ const periodRatio = withPeriod / total;
233
+ score -= periodRatio * 1;
234
+ // Imperative mood bonus
235
+ const imperativeRatio = imperativeMood / total;
236
+ if (imperativeRatio < 0.5)
237
+ score -= 2;
238
+ else if (imperativeRatio < 0.7)
239
+ score -= 1;
240
+ return Math.max(0, Math.min(10, score));
241
+ }
242
+ function calculateBodyScore(withBody, wellFormatted, total) {
243
+ const bodyRatio = withBody / total;
244
+ const formattedRatio = withBody > 0 ? wellFormatted / withBody : 0;
245
+ let score = 0;
246
+ // Body presence (up to 6 points)
247
+ score += bodyRatio * 6;
248
+ // Formatting quality (up to 4 points)
249
+ score += formattedRatio * 4;
250
+ return Math.max(0, Math.min(10, score));
251
+ }
252
+ function generateRecommendations(data) {
253
+ const tips = [];
254
+ // Body recommendations (skip if body check is disabled)
255
+ if (!data.skipBodyCheck) {
256
+ const bodyRatio = data.withBody / data.totalCommits;
257
+ if (bodyRatio < 0.3) {
258
+ tips.push('Add commit bodies to explain "why" not just "what"');
259
+ }
260
+ else if (bodyRatio < 0.5) {
261
+ tips.push('Try adding more detailed commit descriptions');
262
+ }
263
+ }
264
+ // Convention recommendations
265
+ if (data.conventionAdherence < 50) {
266
+ tips.push('Consider adopting Conventional Commits format');
267
+ }
268
+ else if (data.conventionAdherence < 70) {
269
+ tips.push('Increase conventional commit usage for better changelog generation');
270
+ }
271
+ // Length recommendations
272
+ if (data.tooLong > data.totalCommits * 0.1) {
273
+ tips.push('Keep subject lines under 72 characters');
274
+ }
275
+ // Issue reference recommendations
276
+ const issueRefRatio = data.withIssueRef / data.totalCommits;
277
+ if (issueRefRatio < 0.3) {
278
+ tips.push('Link commits to issues/tickets for better traceability');
279
+ }
280
+ // Test recommendations
281
+ const testRatio = data.typeDistribution.tests / data.totalCommits;
282
+ if (testRatio < 0.05) {
283
+ tips.push('Add more test commits - aim for 10-15% test coverage');
284
+ }
285
+ // Refactor recommendations
286
+ const refactorRatio = data.typeDistribution.refactors / data.totalCommits;
287
+ if (refactorRatio < 0.05) {
288
+ tips.push('Schedule regular refactoring - aim for 10-15% refactor commits');
289
+ }
290
+ // Balance recommendations
291
+ const featureFixRatio = data.typeDistribution.features / (data.typeDistribution.fixes || 1);
292
+ if (featureFixRatio > 3) {
293
+ tips.push('High feature/fix ratio - consider addressing technical debt');
294
+ }
295
+ return tips;
296
+ }
297
+ function getEmptyQuality() {
298
+ return {
299
+ overallScore: 0,
300
+ totalCommits: 0,
301
+ subjectQuality: {
302
+ score: 0,
303
+ avgLength: 0,
304
+ tooShort: 0,
305
+ tooLong: 0,
306
+ withPeriod: 0,
307
+ imperativeMood: 0
308
+ },
309
+ bodyQuality: {
310
+ score: 0,
311
+ withBody: 0,
312
+ avgBodyLength: 0,
313
+ wellFormatted: 0
314
+ },
315
+ conventionalCommits: {
316
+ adherence: 0,
317
+ types: {},
318
+ scopes: [],
319
+ breakingChanges: 0
320
+ },
321
+ issueReferences: {
322
+ withReferences: 0,
323
+ patterns: [],
324
+ avgReferencesPerCommit: 0
325
+ },
326
+ codeHygiene: {
327
+ quickFixes: 0,
328
+ workInProgress: 0,
329
+ oops: 0,
330
+ profanity: 0,
331
+ vague: 0,
332
+ singleChar: 0
333
+ },
334
+ typeDistribution: {
335
+ features: 0,
336
+ fixes: 0,
337
+ docs: 0,
338
+ refactors: 0,
339
+ tests: 0,
340
+ chores: 0,
341
+ other: 0
342
+ },
343
+ improvements: []
344
+ };
345
+ }
346
+ function getQualityRating(score) {
347
+ if (score >= 9)
348
+ return '⭐⭐⭐⭐⭐';
349
+ if (score >= 7.5)
350
+ return '⭐⭐⭐⭐';
351
+ if (score >= 6)
352
+ return '⭐⭐⭐';
353
+ if (score >= 4)
354
+ return '⭐⭐';
355
+ return '⭐';
356
+ }
357
+ function getQualityLevel(score) {
358
+ if (score >= 9)
359
+ return 'Excellent';
360
+ if (score >= 7.5)
361
+ return 'Good';
362
+ if (score >= 6)
363
+ return 'Fair';
364
+ if (score >= 4)
365
+ return 'Needs Improvement';
366
+ return 'Poor';
367
+ }