@entro314labs/ai-changelog-generator 3.2.1 → 3.6.0
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/CHANGELOG.md +42 -2
- package/README.md +21 -1
- package/ai-changelog-mcp.sh +0 -0
- package/ai-changelog.sh +0 -0
- package/bin/ai-changelog-dxt.js +6 -3
- package/manifest.json +177 -0
- package/package.json +76 -81
- package/src/ai-changelog-generator.js +5 -4
- package/src/application/orchestrators/changelog.orchestrator.js +19 -203
- package/src/cli.js +16 -5
- package/src/domains/ai/ai-analysis.service.js +2 -0
- package/src/domains/analysis/analysis.engine.js +714 -37
- package/src/domains/changelog/changelog.service.js +623 -32
- package/src/domains/changelog/workspace-changelog.service.js +445 -622
- package/src/domains/git/commit-tagger.js +552 -0
- package/src/domains/git/git-manager.js +357 -0
- package/src/domains/git/git.service.js +865 -16
- package/src/infrastructure/cli/cli.controller.js +14 -9
- package/src/infrastructure/config/configuration.manager.js +25 -11
- package/src/infrastructure/interactive/interactive-workflow.service.js +8 -1
- package/src/infrastructure/mcp/mcp-server.service.js +105 -32
- package/src/infrastructure/providers/core/base-provider.js +1 -1
- package/src/infrastructure/providers/implementations/anthropic.js +16 -173
- package/src/infrastructure/providers/implementations/azure.js +16 -63
- package/src/infrastructure/providers/implementations/dummy.js +13 -16
- package/src/infrastructure/providers/implementations/mock.js +13 -26
- package/src/infrastructure/providers/implementations/ollama.js +12 -4
- package/src/infrastructure/providers/implementations/openai.js +13 -165
- package/src/infrastructure/providers/provider-management.service.js +126 -412
- package/src/infrastructure/providers/utils/base-provider-helpers.js +11 -0
- package/src/shared/utils/cli-ui.js +8 -10
- package/src/shared/utils/diff-processor.js +21 -19
- package/src/shared/utils/error-classes.js +33 -0
- package/src/shared/utils/utils.js +83 -63
- package/types/index.d.ts +61 -68
- package/src/domains/git/git-repository.analyzer.js +0 -678
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import {
|
|
2
|
+
analyzeSemanticChanges,
|
|
3
|
+
assessBusinessRelevance,
|
|
4
|
+
assessFileImportance,
|
|
5
|
+
categorizeFile,
|
|
6
|
+
detectLanguage,
|
|
7
|
+
} from '../../shared/utils/utils.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* CommitTagger - Intelligent commit analysis and categorization
|
|
11
|
+
*
|
|
12
|
+
* This class provides sophisticated commit analysis including:
|
|
13
|
+
* - Conventional commit parsing
|
|
14
|
+
* - Semantic change detection
|
|
15
|
+
* - Breaking change identification
|
|
16
|
+
* - Impact assessment
|
|
17
|
+
* - Tag generation for categorization
|
|
18
|
+
*/
|
|
19
|
+
export class CommitTagger {
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.options = {
|
|
22
|
+
strictConventionalCommits: false,
|
|
23
|
+
enableSemanticAnalysis: true,
|
|
24
|
+
includeFileAnalysis: true,
|
|
25
|
+
...options,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Conventional commit types with their properties
|
|
29
|
+
this.commitTypes = {
|
|
30
|
+
feat: {
|
|
31
|
+
category: 'feature',
|
|
32
|
+
impact: 'minor',
|
|
33
|
+
userFacing: true,
|
|
34
|
+
description: 'New features',
|
|
35
|
+
},
|
|
36
|
+
fix: {
|
|
37
|
+
category: 'bugfix',
|
|
38
|
+
impact: 'patch',
|
|
39
|
+
userFacing: true,
|
|
40
|
+
description: 'Bug fixes',
|
|
41
|
+
},
|
|
42
|
+
docs: {
|
|
43
|
+
category: 'documentation',
|
|
44
|
+
impact: 'patch',
|
|
45
|
+
userFacing: false,
|
|
46
|
+
description: 'Documentation changes',
|
|
47
|
+
},
|
|
48
|
+
style: {
|
|
49
|
+
category: 'style',
|
|
50
|
+
impact: 'patch',
|
|
51
|
+
userFacing: false,
|
|
52
|
+
description: 'Code style changes',
|
|
53
|
+
},
|
|
54
|
+
refactor: {
|
|
55
|
+
category: 'refactor',
|
|
56
|
+
impact: 'patch',
|
|
57
|
+
userFacing: false,
|
|
58
|
+
description: 'Code refactoring',
|
|
59
|
+
},
|
|
60
|
+
perf: {
|
|
61
|
+
category: 'performance',
|
|
62
|
+
impact: 'minor',
|
|
63
|
+
userFacing: true,
|
|
64
|
+
description: 'Performance improvements',
|
|
65
|
+
},
|
|
66
|
+
test: {
|
|
67
|
+
category: 'test',
|
|
68
|
+
impact: 'patch',
|
|
69
|
+
userFacing: false,
|
|
70
|
+
description: 'Test changes',
|
|
71
|
+
},
|
|
72
|
+
build: {
|
|
73
|
+
category: 'build',
|
|
74
|
+
impact: 'patch',
|
|
75
|
+
userFacing: false,
|
|
76
|
+
description: 'Build system changes',
|
|
77
|
+
},
|
|
78
|
+
ci: {
|
|
79
|
+
category: 'ci',
|
|
80
|
+
impact: 'patch',
|
|
81
|
+
userFacing: false,
|
|
82
|
+
description: 'CI/CD changes',
|
|
83
|
+
},
|
|
84
|
+
chore: {
|
|
85
|
+
category: 'chore',
|
|
86
|
+
impact: 'patch',
|
|
87
|
+
userFacing: false,
|
|
88
|
+
description: 'Maintenance tasks',
|
|
89
|
+
},
|
|
90
|
+
revert: {
|
|
91
|
+
category: 'revert',
|
|
92
|
+
impact: 'patch',
|
|
93
|
+
userFacing: true,
|
|
94
|
+
description: 'Reverts',
|
|
95
|
+
},
|
|
96
|
+
merge: {
|
|
97
|
+
category: 'merge',
|
|
98
|
+
impact: 'patch',
|
|
99
|
+
userFacing: false,
|
|
100
|
+
description: 'Merge commits',
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Breaking change indicators
|
|
105
|
+
this.breakingChangePatterns = [
|
|
106
|
+
/BREAKING\s*CHANGE/i,
|
|
107
|
+
/!:/,
|
|
108
|
+
/breaking/i,
|
|
109
|
+
/incompatible/i,
|
|
110
|
+
/remove.*api/i,
|
|
111
|
+
/drop.*support/i,
|
|
112
|
+
/major.*change/i,
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
// High-impact file patterns
|
|
116
|
+
this.criticalFilePatterns = [
|
|
117
|
+
/package\.json$/,
|
|
118
|
+
/\.env/,
|
|
119
|
+
/docker/i,
|
|
120
|
+
/migration/i,
|
|
121
|
+
/schema/i,
|
|
122
|
+
/config/i,
|
|
123
|
+
/security/i,
|
|
124
|
+
/auth/i,
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Analyze a commit and generate comprehensive tagging information
|
|
130
|
+
* @param {Object} commit - Commit object with message, files, and stats
|
|
131
|
+
* @returns {Object} Analysis results with tags, categories, and metadata
|
|
132
|
+
*/
|
|
133
|
+
analyzeCommit(commit) {
|
|
134
|
+
const analysis = {
|
|
135
|
+
semanticChanges: [],
|
|
136
|
+
breakingChanges: [],
|
|
137
|
+
categories: [],
|
|
138
|
+
tags: [],
|
|
139
|
+
importance: 'medium',
|
|
140
|
+
impact: 'patch',
|
|
141
|
+
userFacing: false,
|
|
142
|
+
scope: null,
|
|
143
|
+
type: null,
|
|
144
|
+
conventional: false,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Parse conventional commit structure
|
|
148
|
+
const conventionalParsing = this.parseConventionalCommit(commit.message)
|
|
149
|
+
if (conventionalParsing.isConventional) {
|
|
150
|
+
analysis.conventional = true
|
|
151
|
+
analysis.type = conventionalParsing.type
|
|
152
|
+
analysis.scope = conventionalParsing.scope
|
|
153
|
+
analysis.categories.push(this.commitTypes[conventionalParsing.type]?.category || 'other')
|
|
154
|
+
analysis.impact = this.commitTypes[conventionalParsing.type]?.impact || 'patch'
|
|
155
|
+
analysis.userFacing = this.commitTypes[conventionalParsing.type]?.userFacing
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Detect breaking changes
|
|
159
|
+
analysis.breakingChanges = this.detectBreakingChanges(commit.message, commit.files)
|
|
160
|
+
if (analysis.breakingChanges.length > 0) {
|
|
161
|
+
analysis.categories.push('breaking')
|
|
162
|
+
analysis.tags.push('breaking')
|
|
163
|
+
analysis.impact = 'major'
|
|
164
|
+
analysis.importance = 'critical'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Analyze file changes if available
|
|
168
|
+
if (this.options.includeFileAnalysis && commit.files) {
|
|
169
|
+
const fileAnalysis = this.analyzeFileChanges(commit.files)
|
|
170
|
+
analysis.semanticChanges.push(...fileAnalysis.semanticChanges)
|
|
171
|
+
analysis.categories.push(...fileAnalysis.categories)
|
|
172
|
+
analysis.tags.push(...fileAnalysis.tags)
|
|
173
|
+
|
|
174
|
+
// Upgrade importance based on file analysis
|
|
175
|
+
if (fileAnalysis.hasCriticalFiles) {
|
|
176
|
+
analysis.importance = analysis.importance === 'critical' ? 'critical' : 'high'
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Semantic analysis of commit message
|
|
181
|
+
if (this.options.enableSemanticAnalysis) {
|
|
182
|
+
const semanticAnalysis = this.performSemanticAnalysis(commit.message)
|
|
183
|
+
analysis.semanticChanges.push(...semanticAnalysis.changes)
|
|
184
|
+
analysis.tags.push(...semanticAnalysis.tags)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Business relevance assessment
|
|
188
|
+
const businessRelevance = assessBusinessRelevance(
|
|
189
|
+
commit.message,
|
|
190
|
+
commit.files?.map((f) => f.path) || []
|
|
191
|
+
)
|
|
192
|
+
if (businessRelevance.isBusinessCritical) {
|
|
193
|
+
analysis.importance = 'critical'
|
|
194
|
+
analysis.tags.push('business-critical')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Size-based analysis
|
|
198
|
+
if (commit.stats) {
|
|
199
|
+
const sizeAnalysis = this.analyzeSizeImpact(commit.stats)
|
|
200
|
+
if (sizeAnalysis.isLarge) {
|
|
201
|
+
analysis.tags.push('large-change')
|
|
202
|
+
analysis.importance = analysis.importance === 'low' ? 'medium' : analysis.importance
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Clean up and deduplicate
|
|
207
|
+
analysis.categories = [...new Set(analysis.categories)].filter(Boolean)
|
|
208
|
+
analysis.tags = [...new Set(analysis.tags)].filter(Boolean)
|
|
209
|
+
|
|
210
|
+
// Fallback categorization if no categories found
|
|
211
|
+
if (analysis.categories.length === 0) {
|
|
212
|
+
analysis.categories.push(this.inferCategoryFromMessage(commit.message))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return analysis
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Parse conventional commit message format
|
|
220
|
+
* @param {string} message - Commit message
|
|
221
|
+
* @returns {Object} Parsed commit information
|
|
222
|
+
*/
|
|
223
|
+
parseConventionalCommit(message) {
|
|
224
|
+
// Conventional commit regex: type(scope): description
|
|
225
|
+
const conventionalRegex = /^(\w+)(\(([^)]+)\))?(!)?:\s*(.+)$/
|
|
226
|
+
|
|
227
|
+
const match = message.match(conventionalRegex)
|
|
228
|
+
if (!match) {
|
|
229
|
+
return { isConventional: false }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const [, type, , scope, breaking, description] = match
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
isConventional: true,
|
|
236
|
+
type: type.toLowerCase(),
|
|
237
|
+
scope: scope || null,
|
|
238
|
+
hasBreaking: Boolean(breaking),
|
|
239
|
+
description: description.trim(),
|
|
240
|
+
isValidType: type.toLowerCase() in this.commitTypes,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Detect breaking changes in commit message and files
|
|
246
|
+
* @param {string} message - Commit message
|
|
247
|
+
* @param {Array} files - Changed files
|
|
248
|
+
* @returns {Array} Array of breaking change descriptions
|
|
249
|
+
*/
|
|
250
|
+
detectBreakingChanges(message, files = []) {
|
|
251
|
+
const breakingChanges = []
|
|
252
|
+
|
|
253
|
+
// Check commit message for breaking change indicators
|
|
254
|
+
for (const pattern of this.breakingChangePatterns) {
|
|
255
|
+
if (pattern.test(message)) {
|
|
256
|
+
breakingChanges.push(
|
|
257
|
+
`Breaking change detected in commit message: "${message.match(pattern)[0]}"`
|
|
258
|
+
)
|
|
259
|
+
break
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Check for API/interface changes in files
|
|
264
|
+
if (files) {
|
|
265
|
+
const apiFiles = files.filter(
|
|
266
|
+
(file) =>
|
|
267
|
+
file.path &&
|
|
268
|
+
(file.path.includes('api') ||
|
|
269
|
+
file.path.includes('interface') ||
|
|
270
|
+
file.path.includes('types') ||
|
|
271
|
+
file.path.includes('schema'))
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if (apiFiles.length > 0) {
|
|
275
|
+
const deletions = apiFiles.filter((f) => f.status === 'D')
|
|
276
|
+
const majorModifications = apiFiles.filter(
|
|
277
|
+
(f) => f.diff && (f.diff.includes('- export') || f.diff.includes('- function'))
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if (deletions.length > 0) {
|
|
281
|
+
breakingChanges.push(`API files deleted: ${deletions.map((f) => f.path).join(', ')}`)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (majorModifications.length > 0) {
|
|
285
|
+
breakingChanges.push(
|
|
286
|
+
`Potential API changes in: ${majorModifications.map((f) => f.path).join(', ')}`
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return breakingChanges
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Analyze file changes for semantic patterns
|
|
297
|
+
* @param {Array} files - Array of file change objects
|
|
298
|
+
* @returns {Object} File analysis results
|
|
299
|
+
*/
|
|
300
|
+
analyzeFileChanges(files) {
|
|
301
|
+
const analysis = {
|
|
302
|
+
semanticChanges: [],
|
|
303
|
+
categories: [],
|
|
304
|
+
tags: [],
|
|
305
|
+
hasCriticalFiles: false,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const categoryCounts = {}
|
|
309
|
+
let hasDatabaseChanges = false
|
|
310
|
+
let hasConfigChanges = false
|
|
311
|
+
let hasTestChanges = false
|
|
312
|
+
|
|
313
|
+
for (const file of files) {
|
|
314
|
+
if (!file.path) continue
|
|
315
|
+
|
|
316
|
+
// Categorize file
|
|
317
|
+
const category = categorizeFile(file.path)
|
|
318
|
+
categoryCounts[category] = (categoryCounts[category] || 0) + 1
|
|
319
|
+
|
|
320
|
+
// Detect file importance
|
|
321
|
+
const importance = assessFileImportance(file.path, file.status)
|
|
322
|
+
if (importance === 'critical') {
|
|
323
|
+
analysis.hasCriticalFiles = true
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check for critical file patterns
|
|
327
|
+
for (const pattern of this.criticalFilePatterns) {
|
|
328
|
+
if (pattern.test(file.path)) {
|
|
329
|
+
analysis.hasCriticalFiles = true
|
|
330
|
+
break
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Specific pattern detection
|
|
335
|
+
if (file.path.includes('migration') || file.path.includes('schema')) {
|
|
336
|
+
hasDatabaseChanges = true
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (file.path.includes('config') || file.path.includes('.env')) {
|
|
340
|
+
hasConfigChanges = true
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (file.path.includes('test') || file.path.includes('spec')) {
|
|
344
|
+
hasTestChanges = true
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Language-specific analysis
|
|
348
|
+
const language = detectLanguage(file.path)
|
|
349
|
+
if (file.diff && this.options.enableSemanticAnalysis) {
|
|
350
|
+
const semanticChanges = analyzeSemanticChanges(file.diff, file.path)
|
|
351
|
+
if (semanticChanges.patterns.length > 0) {
|
|
352
|
+
analysis.semanticChanges.push(...semanticChanges.patterns)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Determine primary categories
|
|
358
|
+
const sortedCategories = Object.entries(categoryCounts)
|
|
359
|
+
.sort(([, a], [, b]) => b - a)
|
|
360
|
+
.map(([category]) => category)
|
|
361
|
+
|
|
362
|
+
analysis.categories = sortedCategories.slice(0, 2) // Top 2 categories
|
|
363
|
+
|
|
364
|
+
// Add specific tags based on detected patterns
|
|
365
|
+
if (hasDatabaseChanges) {
|
|
366
|
+
analysis.tags.push('database-changes')
|
|
367
|
+
}
|
|
368
|
+
if (hasConfigChanges) {
|
|
369
|
+
analysis.tags.push('configuration')
|
|
370
|
+
}
|
|
371
|
+
if (hasTestChanges) {
|
|
372
|
+
analysis.tags.push('tests')
|
|
373
|
+
}
|
|
374
|
+
if (analysis.hasCriticalFiles) {
|
|
375
|
+
analysis.tags.push('critical-files')
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return analysis
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Perform semantic analysis on commit message
|
|
383
|
+
* @param {string} message - Commit message
|
|
384
|
+
* @returns {Object} Semantic analysis results
|
|
385
|
+
*/
|
|
386
|
+
performSemanticAnalysis(message) {
|
|
387
|
+
const analysis = {
|
|
388
|
+
changes: [],
|
|
389
|
+
tags: [],
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const lowerMessage = message.toLowerCase()
|
|
393
|
+
|
|
394
|
+
// Action-based detection
|
|
395
|
+
const actionPatterns = {
|
|
396
|
+
add: /\b(add|added|adding|new|create|created|creating)\b/,
|
|
397
|
+
remove: /\b(remove|removed|removing|delete|deleted|deleting)\b/,
|
|
398
|
+
update: /\b(update|updated|updating|modify|modified|modifying|change|changed|changing)\b/,
|
|
399
|
+
fix: /\b(fix|fixed|fixing|resolve|resolved|resolving|solve|solved|solving)\b/,
|
|
400
|
+
improve:
|
|
401
|
+
/\b(improve|improved|improving|enhance|enhanced|enhancing|optimize|optimized|optimizing)\b/,
|
|
402
|
+
refactor: /\b(refactor|refactored|refactoring|restructure|restructured|restructuring)\b/,
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
for (const [action, pattern] of Object.entries(actionPatterns)) {
|
|
406
|
+
if (pattern.test(lowerMessage)) {
|
|
407
|
+
analysis.changes.push(action)
|
|
408
|
+
analysis.tags.push(action)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Technology/framework detection
|
|
413
|
+
const techPatterns = {
|
|
414
|
+
react: /\breact\b/i,
|
|
415
|
+
vue: /\bvue\b/i,
|
|
416
|
+
angular: /\bangular\b/i,
|
|
417
|
+
node: /\bnode\b/i,
|
|
418
|
+
typescript: /\btypescript\b/i,
|
|
419
|
+
javascript: /\bjavascript\b/i,
|
|
420
|
+
database: /\b(database|db|sql|mysql|postgres|mongodb)\b/i,
|
|
421
|
+
api: /\b(api|rest|graphql|endpoint)\b/i,
|
|
422
|
+
docker: /\bdocker\b/i,
|
|
423
|
+
kubernetes: /\bkubernetes\b/i,
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
for (const [tech, pattern] of Object.entries(techPatterns)) {
|
|
427
|
+
if (pattern.test(message)) {
|
|
428
|
+
analysis.tags.push(tech)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return analysis
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Analyze size impact of commit based on statistics
|
|
437
|
+
* @param {Object} stats - Commit statistics (files, insertions, deletions)
|
|
438
|
+
* @returns {Object} Size analysis results
|
|
439
|
+
*/
|
|
440
|
+
analyzeSizeImpact(stats) {
|
|
441
|
+
const totalLines = (stats.insertions || 0) + (stats.deletions || 0)
|
|
442
|
+
const filesCount = stats.files || 0
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
isLarge: totalLines > 500 || filesCount > 20,
|
|
446
|
+
isSmall: totalLines < 10 && filesCount <= 2,
|
|
447
|
+
totalLines,
|
|
448
|
+
filesCount,
|
|
449
|
+
magnitude:
|
|
450
|
+
totalLines > 1000
|
|
451
|
+
? 'huge'
|
|
452
|
+
: totalLines > 500
|
|
453
|
+
? 'large'
|
|
454
|
+
: totalLines > 100
|
|
455
|
+
? 'medium'
|
|
456
|
+
: totalLines > 10
|
|
457
|
+
? 'small'
|
|
458
|
+
: 'tiny',
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Infer category from commit message when conventional commit parsing fails
|
|
464
|
+
* @param {string} message - Commit message
|
|
465
|
+
* @returns {string} Inferred category
|
|
466
|
+
*/
|
|
467
|
+
inferCategoryFromMessage(message) {
|
|
468
|
+
const lowerMessage = message.toLowerCase()
|
|
469
|
+
|
|
470
|
+
// Keyword-based category inference
|
|
471
|
+
const categoryKeywords = {
|
|
472
|
+
feature: ['feature', 'add', 'new', 'implement', 'create'],
|
|
473
|
+
bugfix: ['fix', 'bug', 'issue', 'problem', 'error', 'resolve'],
|
|
474
|
+
documentation: ['doc', 'readme', 'comment', 'guide', 'manual'],
|
|
475
|
+
test: ['test', 'spec', 'coverage', 'unit', 'integration'],
|
|
476
|
+
refactor: ['refactor', 'cleanup', 'restructure', 'reorganize'],
|
|
477
|
+
performance: ['performance', 'optimize', 'speed', 'fast', 'efficient'],
|
|
478
|
+
security: ['security', 'auth', 'permission', 'vulnerability'],
|
|
479
|
+
build: ['build', 'compile', 'bundle', 'package', 'deploy'],
|
|
480
|
+
style: ['style', 'format', 'lint', 'prettier', 'whitespace'],
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
for (const [category, keywords] of Object.entries(categoryKeywords)) {
|
|
484
|
+
if (keywords.some((keyword) => lowerMessage.includes(keyword))) {
|
|
485
|
+
return category
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return 'other'
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Get supported commit types
|
|
494
|
+
* @returns {Object} Commit types configuration
|
|
495
|
+
*/
|
|
496
|
+
getCommitTypes() {
|
|
497
|
+
return this.commitTypes
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Validate commit message format
|
|
502
|
+
* @param {string} message - Commit message to validate
|
|
503
|
+
* @returns {Object} Validation results
|
|
504
|
+
*/
|
|
505
|
+
validateCommitMessage(message) {
|
|
506
|
+
const parsed = this.parseConventionalCommit(message)
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
isValid: parsed.isConventional && parsed.isValidType,
|
|
510
|
+
isConventional: parsed.isConventional,
|
|
511
|
+
hasValidType: parsed.isValidType,
|
|
512
|
+
type: parsed.type,
|
|
513
|
+
scope: parsed.scope,
|
|
514
|
+
suggestions: this.generateCommitSuggestions(message, parsed),
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Generate suggestions for improving commit messages
|
|
520
|
+
* @param {string} message - Original commit message
|
|
521
|
+
* @param {Object} parsed - Parsed commit information
|
|
522
|
+
* @returns {Array} Array of suggestion strings
|
|
523
|
+
*/
|
|
524
|
+
generateCommitSuggestions(message, parsed) {
|
|
525
|
+
const suggestions = []
|
|
526
|
+
|
|
527
|
+
if (!parsed.isConventional) {
|
|
528
|
+
const inferredCategory = this.inferCategoryFromMessage(message)
|
|
529
|
+
const suggestedType =
|
|
530
|
+
Object.entries(this.commitTypes).find(
|
|
531
|
+
([, config]) => config.category === inferredCategory
|
|
532
|
+
)?.[0] || 'feat'
|
|
533
|
+
|
|
534
|
+
suggestions.push(`Consider using conventional format: "${suggestedType}: ${message}"`)
|
|
535
|
+
} else if (!parsed.isValidType) {
|
|
536
|
+
const validTypes = Object.keys(this.commitTypes).join(', ')
|
|
537
|
+
suggestions.push(`"${parsed.type}" is not a recognized type. Valid types: ${validTypes}`)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (message.length > 72) {
|
|
541
|
+
suggestions.push(
|
|
542
|
+
'Consider shortening the commit message (72 characters or less is recommended)'
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (message.length < 10) {
|
|
547
|
+
suggestions.push('Consider adding more descriptive information to the commit message')
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return suggestions
|
|
551
|
+
}
|
|
552
|
+
}
|