@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +42 -2
  2. package/README.md +21 -1
  3. package/ai-changelog-mcp.sh +0 -0
  4. package/ai-changelog.sh +0 -0
  5. package/bin/ai-changelog-dxt.js +6 -3
  6. package/manifest.json +177 -0
  7. package/package.json +76 -81
  8. package/src/ai-changelog-generator.js +5 -4
  9. package/src/application/orchestrators/changelog.orchestrator.js +19 -203
  10. package/src/cli.js +16 -5
  11. package/src/domains/ai/ai-analysis.service.js +2 -0
  12. package/src/domains/analysis/analysis.engine.js +714 -37
  13. package/src/domains/changelog/changelog.service.js +623 -32
  14. package/src/domains/changelog/workspace-changelog.service.js +445 -622
  15. package/src/domains/git/commit-tagger.js +552 -0
  16. package/src/domains/git/git-manager.js +357 -0
  17. package/src/domains/git/git.service.js +865 -16
  18. package/src/infrastructure/cli/cli.controller.js +14 -9
  19. package/src/infrastructure/config/configuration.manager.js +25 -11
  20. package/src/infrastructure/interactive/interactive-workflow.service.js +8 -1
  21. package/src/infrastructure/mcp/mcp-server.service.js +105 -32
  22. package/src/infrastructure/providers/core/base-provider.js +1 -1
  23. package/src/infrastructure/providers/implementations/anthropic.js +16 -173
  24. package/src/infrastructure/providers/implementations/azure.js +16 -63
  25. package/src/infrastructure/providers/implementations/dummy.js +13 -16
  26. package/src/infrastructure/providers/implementations/mock.js +13 -26
  27. package/src/infrastructure/providers/implementations/ollama.js +12 -4
  28. package/src/infrastructure/providers/implementations/openai.js +13 -165
  29. package/src/infrastructure/providers/provider-management.service.js +126 -412
  30. package/src/infrastructure/providers/utils/base-provider-helpers.js +11 -0
  31. package/src/shared/utils/cli-ui.js +8 -10
  32. package/src/shared/utils/diff-processor.js +21 -19
  33. package/src/shared/utils/error-classes.js +33 -0
  34. package/src/shared/utils/utils.js +83 -63
  35. package/types/index.d.ts +61 -68
  36. 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
+ }