@entro314labs/ai-changelog-generator 3.0.5 → 3.2.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 (51) hide show
  1. package/CHANGELOG.md +383 -785
  2. package/README.md +30 -3
  3. package/ai-changelog-mcp.sh +0 -0
  4. package/ai-changelog.sh +0 -0
  5. package/bin/ai-changelog-dxt.js +9 -9
  6. package/bin/ai-changelog-mcp.js +19 -17
  7. package/bin/ai-changelog.js +6 -6
  8. package/package.json +84 -52
  9. package/src/ai-changelog-generator.js +83 -81
  10. package/src/application/orchestrators/changelog.orchestrator.js +1040 -296
  11. package/src/application/services/application.service.js +145 -123
  12. package/src/cli.js +76 -57
  13. package/src/domains/ai/ai-analysis.service.js +289 -209
  14. package/src/domains/analysis/analysis.engine.js +253 -193
  15. package/src/domains/changelog/changelog.service.js +1062 -784
  16. package/src/domains/changelog/workspace-changelog.service.js +420 -249
  17. package/src/domains/git/git-repository.analyzer.js +348 -258
  18. package/src/domains/git/git.service.js +132 -112
  19. package/src/infrastructure/cli/cli.controller.js +415 -247
  20. package/src/infrastructure/config/configuration.manager.js +220 -190
  21. package/src/infrastructure/interactive/interactive-staging.service.js +332 -0
  22. package/src/infrastructure/interactive/interactive-workflow.service.js +200 -159
  23. package/src/infrastructure/mcp/mcp-server.service.js +208 -207
  24. package/src/infrastructure/metrics/metrics.collector.js +140 -123
  25. package/src/infrastructure/providers/core/base-provider.js +87 -40
  26. package/src/infrastructure/providers/implementations/anthropic.js +101 -99
  27. package/src/infrastructure/providers/implementations/azure.js +124 -101
  28. package/src/infrastructure/providers/implementations/bedrock.js +136 -126
  29. package/src/infrastructure/providers/implementations/dummy.js +23 -23
  30. package/src/infrastructure/providers/implementations/google.js +123 -114
  31. package/src/infrastructure/providers/implementations/huggingface.js +94 -87
  32. package/src/infrastructure/providers/implementations/lmstudio.js +75 -60
  33. package/src/infrastructure/providers/implementations/mock.js +69 -73
  34. package/src/infrastructure/providers/implementations/ollama.js +89 -66
  35. package/src/infrastructure/providers/implementations/openai.js +88 -89
  36. package/src/infrastructure/providers/implementations/vertex.js +227 -197
  37. package/src/infrastructure/providers/provider-management.service.js +245 -207
  38. package/src/infrastructure/providers/provider-manager.service.js +145 -125
  39. package/src/infrastructure/providers/utils/base-provider-helpers.js +308 -302
  40. package/src/infrastructure/providers/utils/model-config.js +220 -195
  41. package/src/infrastructure/providers/utils/provider-utils.js +105 -100
  42. package/src/infrastructure/validation/commit-message-validation.service.js +556 -0
  43. package/src/shared/constants/colors.js +467 -172
  44. package/src/shared/utils/cli-demo.js +285 -0
  45. package/src/shared/utils/cli-entry-utils.js +257 -249
  46. package/src/shared/utils/cli-ui.js +447 -0
  47. package/src/shared/utils/diff-processor.js +513 -0
  48. package/src/shared/utils/error-classes.js +125 -156
  49. package/src/shared/utils/json-utils.js +93 -89
  50. package/src/shared/utils/utils.js +1299 -775
  51. package/types/index.d.ts +353 -344
@@ -0,0 +1,513 @@
1
+ /**
2
+ * DiffProcessor - Central utility for intelligent diff compression and processing
3
+ *
4
+ * Handles large diffs by applying smart reduction strategies while preserving
5
+ * semantic meaning for AI analysis.
6
+ */
7
+
8
+ export class DiffProcessor {
9
+ constructor(options = {}) {
10
+ this.analysisMode = options.analysisMode || 'standard'
11
+ this.maxTotalSize = options.maxTotalSize || this.getDefaultMaxSize()
12
+ this.priorityFiles = options.priorityFiles || this.getDefaultPriorityFiles()
13
+ this.enableFiltering = options.enableFiltering !== false
14
+ this.enablePatternDetection = options.enablePatternDetection !== false
15
+ }
16
+
17
+ getDefaultMaxSize() {
18
+ const sizes = {
19
+ standard: 12000,
20
+ detailed: 20000,
21
+ enterprise: 30000,
22
+ }
23
+ return sizes[this.analysisMode] || sizes.standard
24
+ }
25
+
26
+ getDefaultPriorityFiles() {
27
+ const counts = {
28
+ standard: 15,
29
+ detailed: 25,
30
+ enterprise: 40,
31
+ }
32
+ return counts[this.analysisMode] || counts.standard
33
+ }
34
+
35
+ /**
36
+ * Process a collection of files with diffs, applying intelligent compression
37
+ */
38
+ processFiles(files, _options = {}) {
39
+ if (!(files && Array.isArray(files)) || files.length === 0) {
40
+ return {
41
+ processedFiles: [],
42
+ totalSize: 0,
43
+ patterns: {},
44
+ filesProcessed: 0,
45
+ filesSkipped: 0,
46
+ }
47
+ }
48
+
49
+ // Step 1: Prioritize files by importance
50
+ const prioritizedFiles = this.prioritizeFiles(files)
51
+
52
+ // Step 2: Detect bulk change patterns
53
+ const patterns = this.enablePatternDetection ? this.detectChangePatterns(files) : {}
54
+
55
+ // Step 3: Process individual files
56
+ const processedFiles = []
57
+ let totalSize = 0
58
+ let remainingBudget = this.maxTotalSize
59
+
60
+ for (let i = 0; i < Math.min(prioritizedFiles.length, this.priorityFiles); i++) {
61
+ const file = prioritizedFiles[i]
62
+
63
+ // Calculate budget for this file (remaining budget distributed across remaining files)
64
+ const remainingFiles = Math.min(prioritizedFiles.length - i, this.priorityFiles - i)
65
+ const fileBudget = Math.floor(remainingBudget / remainingFiles)
66
+
67
+ const processedFile = this.processFileDiff(file, {
68
+ budget: fileBudget,
69
+ patterns,
70
+ isHighPriority: i < 5, // First 5 files get high priority
71
+ })
72
+
73
+ processedFiles.push(processedFile)
74
+
75
+ const fileSize = this.estimateSize(processedFile.diff)
76
+ totalSize += fileSize
77
+ remainingBudget -= fileSize
78
+
79
+ if (remainingBudget <= 0) {
80
+ break
81
+ }
82
+ }
83
+
84
+ // Step 4: Add summary for remaining files
85
+ if (prioritizedFiles.length > processedFiles.length) {
86
+ const remainingFiles = prioritizedFiles.slice(processedFiles.length)
87
+ const summary = this.createRemainingFilesSummary(remainingFiles, patterns)
88
+ processedFiles.push(summary)
89
+ }
90
+
91
+ return {
92
+ processedFiles,
93
+ totalSize,
94
+ patterns,
95
+ filesProcessed: processedFiles.length,
96
+ filesSkipped: Math.max(0, files.length - processedFiles.length),
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Process a single file's diff with intelligent compression
102
+ */
103
+ processFileDiff(file, options = {}) {
104
+ const { budget = 1500, patterns = {}, isHighPriority = false } = options
105
+
106
+ if (!file.diff || file.diff === 'No diff available') {
107
+ return {
108
+ ...file,
109
+ diff: this.generateFallbackDescription(file),
110
+ processed: true,
111
+ compressionApplied: false,
112
+ }
113
+ }
114
+
115
+ let processedDiff = file.diff
116
+
117
+ // Apply filtering pipeline if enabled
118
+ if (this.enableFiltering) {
119
+ processedDiff = this.applyDiffFiltering(processedDiff, file)
120
+ }
121
+
122
+ // Check if file matches bulk patterns
123
+ const patternMatch = this.matchesBulkPattern(file, patterns)
124
+ if (patternMatch) {
125
+ return {
126
+ ...file,
127
+ diff: `[Bulk ${patternMatch.type}]: ${patternMatch.description}`,
128
+ processed: true,
129
+ compressionApplied: true,
130
+ bulkPattern: patternMatch.type,
131
+ }
132
+ }
133
+
134
+ // Apply intelligent truncation if still too large
135
+ if (processedDiff.length > budget) {
136
+ processedDiff = this.intelligentTruncation(processedDiff, budget, {
137
+ preserveStructure: isHighPriority,
138
+ filePath: file.filePath || file.path,
139
+ })
140
+ }
141
+
142
+ return {
143
+ ...file,
144
+ diff: processedDiff,
145
+ processed: true,
146
+ compressionApplied: processedDiff.length < file.diff.length,
147
+ originalSize: file.diff.length,
148
+ compressedSize: processedDiff.length,
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Prioritize files by importance for analysis
154
+ */
155
+ prioritizeFiles(files) {
156
+ return [...files].sort((a, b) => {
157
+ // Primary: Status priority (Modified > Added > Deleted)
158
+ const statusPriority = { M: 0, R: 1, A: 2, D: 3 }
159
+ const aStatus = statusPriority[a.status] ?? 4
160
+ const bStatus = statusPriority[b.status] ?? 4
161
+ if (aStatus !== bStatus) {
162
+ return aStatus - bStatus
163
+ }
164
+
165
+ // Secondary: File importance
166
+ const aImportance = this.calculateFileImportance(a)
167
+ const bImportance = this.calculateFileImportance(b)
168
+ if (aImportance !== bImportance) {
169
+ return bImportance - aImportance
170
+ }
171
+
172
+ // Tertiary: Diff size (larger changes first)
173
+ const aDiffSize = (a.diff || '').length
174
+ const bDiffSize = (b.diff || '').length
175
+ return bDiffSize - aDiffSize
176
+ })
177
+ }
178
+
179
+ /**
180
+ * Calculate file importance score
181
+ */
182
+ calculateFileImportance(file) {
183
+ const filePath = file.filePath || file.path || ''
184
+ let score = 0
185
+
186
+ // Core source files
187
+ if (filePath.includes('/src/') && (filePath.endsWith('.js') || filePath.endsWith('.ts'))) {
188
+ score += 100
189
+ }
190
+
191
+ // Important directories
192
+ if (filePath.includes('/domains/') || filePath.includes('/services/')) {
193
+ score += 50
194
+ }
195
+ if (filePath.includes('/utils/') || filePath.includes('/shared/')) {
196
+ score += 30
197
+ }
198
+ if (filePath.includes('/components/')) {
199
+ score += 40
200
+ }
201
+ if (filePath.includes('/api/') || filePath.includes('/routes/')) {
202
+ score += 60
203
+ }
204
+
205
+ // Configuration files
206
+ if (filePath.includes('package.json') || filePath.includes('config')) {
207
+ score += 20
208
+ }
209
+
210
+ // Tests (lower priority)
211
+ if (filePath.includes('/test/') || filePath.includes('.test.') || filePath.includes('.spec.')) {
212
+ score -= 20
213
+ }
214
+
215
+ // Documentation (lowest priority for code analysis)
216
+ if (filePath.endsWith('.md') || filePath.includes('/docs/')) {
217
+ score -= 30
218
+ }
219
+
220
+ // Generated/build files (very low priority)
221
+ if (
222
+ filePath.includes('/node_modules/') ||
223
+ filePath.includes('/dist/') ||
224
+ filePath.includes('/build/') ||
225
+ filePath.includes('lock')
226
+ ) {
227
+ score -= 100
228
+ }
229
+
230
+ return score
231
+ }
232
+
233
+ /**
234
+ * Detect bulk change patterns across files
235
+ */
236
+ detectChangePatterns(files) {
237
+ const patterns = {}
238
+
239
+ // Detect mass renames
240
+ const renames = files.filter((f) => f.status === 'R')
241
+ if (renames.length >= 3) {
242
+ patterns.massRename = {
243
+ type: 'massRename',
244
+ count: renames.length,
245
+ description: `${renames.length} files renamed`,
246
+ files: renames.map((f) => ({ from: f.oldPath, to: f.filePath || f.path })),
247
+ }
248
+ }
249
+
250
+ // Detect formatting changes
251
+ const formattingFiles = files.filter((f) => this.isLikelyFormattingChange(f))
252
+ if (formattingFiles.length >= 5) {
253
+ patterns.formatting = {
254
+ type: 'formatting',
255
+ count: formattingFiles.length,
256
+ description: `Formatting/linting applied to ${formattingFiles.length} files`,
257
+ files: formattingFiles.map((f) => f.filePath || f.path),
258
+ }
259
+ }
260
+
261
+ // Detect dependency updates
262
+ const packageFiles = files.filter(
263
+ (f) => (f.filePath || f.path).includes('package') || (f.filePath || f.path).includes('lock')
264
+ )
265
+ if (packageFiles.length > 0) {
266
+ patterns.dependencies = {
267
+ type: 'dependencies',
268
+ count: packageFiles.length,
269
+ description: `Package/dependency updates in ${packageFiles.length} files`,
270
+ files: packageFiles.map((f) => f.filePath || f.path),
271
+ }
272
+ }
273
+
274
+ return patterns
275
+ }
276
+
277
+ /**
278
+ * Check if a file matches a bulk pattern
279
+ */
280
+ matchesBulkPattern(file, patterns) {
281
+ const filePath = file.filePath || file.path || ''
282
+
283
+ // Check for formatting pattern
284
+ if (patterns.formatting?.files.includes(filePath)) {
285
+ return patterns.formatting
286
+ }
287
+
288
+ // Check for rename pattern
289
+ if (patterns.massRename?.files.some((r) => r.to === filePath)) {
290
+ return patterns.massRename
291
+ }
292
+
293
+ // Check for dependency pattern
294
+ if (patterns.dependencies?.files.includes(filePath)) {
295
+ return patterns.dependencies
296
+ }
297
+
298
+ return null
299
+ }
300
+
301
+ /**
302
+ * Apply diff filtering to remove noise
303
+ */
304
+ applyDiffFiltering(diff, _file) {
305
+ let filteredDiff = diff
306
+
307
+ // Remove whitespace-only changes
308
+ filteredDiff = filteredDiff.replace(/^[+-]\s*$/gm, '')
309
+
310
+ // Remove pure import/require shuffling (common in large refactors)
311
+ const importLines = filteredDiff.split('\n').filter((line) => {
312
+ const trimmed = line.replace(/^[+-]\s*/, '')
313
+ return trimmed.match(/^(import|require|from\s+['"])/)
314
+ })
315
+
316
+ if (importLines.length > 10) {
317
+ // If many import changes, summarize them
318
+ filteredDiff = filteredDiff.replace(/^[+-]\s*(import|require|from\s+['"]).*$/gm, '')
319
+ filteredDiff = `[${importLines.length} import/require changes summarized]\n${filteredDiff}`
320
+ }
321
+
322
+ // Remove console.log additions/removals (common in development)
323
+ filteredDiff = filteredDiff.replace(/^[+-]\s*console\.(log|debug|info).*$/gm, '')
324
+
325
+ // Clean up multiple empty lines
326
+ filteredDiff = filteredDiff.replace(/\n\s*\n\s*\n/g, '\n\n')
327
+
328
+ return filteredDiff.trim()
329
+ }
330
+
331
+ /**
332
+ * Check if a file appears to be mostly formatting changes
333
+ */
334
+ isLikelyFormattingChange(file) {
335
+ if (!file.diff) {
336
+ return false
337
+ }
338
+
339
+ const lines = file.diff.split('\n')
340
+ const changeLines = lines.filter((line) => line.startsWith('+') || line.startsWith('-'))
341
+
342
+ if (changeLines.length === 0) {
343
+ return false
344
+ }
345
+
346
+ // Count lines that are likely formatting (whitespace, semicolons, brackets)
347
+ const formattingLines = changeLines.filter((line) => {
348
+ const content = line.substring(1).trim()
349
+ return (
350
+ content === '' || // Empty lines
351
+ content.match(/^[\s{}[\];,]+$/) || // Brackets, semicolons, commas
352
+ content.match(/^\/\/\s*eslint/) || // ESLint comments
353
+ (content.match(/^\s*(import|from)/) && // Import reordering
354
+ changeLines.filter((l) =>
355
+ l
356
+ .substring(1)
357
+ .trim()
358
+ .includes(content.replace(/^[\s+-]*/, ''))
359
+ ).length > 1)
360
+ )
361
+ })
362
+
363
+ // If more than 80% of changes are formatting-related
364
+ return formattingLines.length / changeLines.length > 0.8
365
+ }
366
+
367
+ /**
368
+ * Apply intelligent truncation that preserves structure
369
+ */
370
+ intelligentTruncation(diff, budget, options = {}) {
371
+ const { preserveStructure = false, filePath = '' } = options
372
+
373
+ if (diff.length <= budget) {
374
+ return diff
375
+ }
376
+
377
+ const lines = diff.split('\n')
378
+
379
+ if (preserveStructure && lines.length > 20) {
380
+ // For high-priority files, show context at beginning and end
381
+ const headerLines = Math.floor((budget * 0.4) / 80) // Estimate chars per line
382
+ const footerLines = Math.floor((budget * 0.3) / 80)
383
+ const middleLines = Math.floor((budget * 0.2) / 80)
384
+
385
+ const header = lines.slice(0, headerLines).join('\n')
386
+ const footer = lines.slice(-footerLines).join('\n')
387
+
388
+ // Find important middle sections (function definitions, class declarations)
389
+ const importantMiddle = lines
390
+ .slice(headerLines, -footerLines)
391
+ .filter((line) => line.match(/^[+-]\s*(function|class|const|let|var|export|async)/))
392
+ .slice(0, middleLines)
393
+
394
+ const middle =
395
+ importantMiddle.length > 0
396
+ ? `\n... [${lines.length - headerLines - footerLines - importantMiddle.length} lines omitted] ...\n` +
397
+ importantMiddle.join('\n')
398
+ : `\n... [${lines.length - headerLines - footerLines} lines omitted] ...`
399
+
400
+ return `${header}${middle}\n${footer}`.substring(0, budget)
401
+ }
402
+
403
+ // Simple truncation with context preservation
404
+ const truncated = diff.substring(0, budget - 50)
405
+ const lastNewline = truncated.lastIndexOf('\n')
406
+
407
+ return (
408
+ (lastNewline > budget * 0.8 ? truncated.substring(0, lastNewline) : truncated) +
409
+ '\n... [truncated]'
410
+ )
411
+ }
412
+
413
+ /**
414
+ * Generate fallback description for files without diffs
415
+ */
416
+ generateFallbackDescription(file) {
417
+ const filePath = file.filePath || file.path || 'unknown file'
418
+ const status = file.status || 'modified'
419
+
420
+ const statusDescriptions = {
421
+ M: 'Modified',
422
+ A: 'Added',
423
+ D: 'Deleted',
424
+ R: 'Renamed',
425
+ '??': 'Untracked',
426
+ }
427
+
428
+ const description = statusDescriptions[status] || 'Changed'
429
+ const fileType = this.getFileTypeDescription(filePath)
430
+
431
+ return `${description} ${fileType}: ${filePath}`
432
+ }
433
+
434
+ /**
435
+ * Get file type description
436
+ */
437
+ getFileTypeDescription(filePath) {
438
+ if (filePath.endsWith('.js') || filePath.endsWith('.ts')) {
439
+ return 'JavaScript/TypeScript file'
440
+ }
441
+ if (filePath.endsWith('.json')) {
442
+ return 'JSON configuration'
443
+ }
444
+ if (filePath.endsWith('.md')) {
445
+ return 'documentation file'
446
+ }
447
+ if (filePath.includes('package')) {
448
+ return 'package configuration'
449
+ }
450
+ if (filePath.includes('test') || filePath.includes('spec')) {
451
+ return 'test file'
452
+ }
453
+ return 'file'
454
+ }
455
+
456
+ /**
457
+ * Create summary for remaining files that weren't processed
458
+ */
459
+ createRemainingFilesSummary(remainingFiles, _patterns) {
460
+ const categories = {}
461
+
462
+ remainingFiles.forEach((file) => {
463
+ const type = this.categorizeFile(file)
464
+ if (!categories[type]) {
465
+ categories[type] = []
466
+ }
467
+ categories[type].push(file)
468
+ })
469
+
470
+ const summaryParts = Object.entries(categories).map(([type, files]) => {
471
+ return `${files.length} ${type} files`
472
+ })
473
+
474
+ return {
475
+ path: '[SUMMARY]',
476
+ status: 'SUMMARY',
477
+ diff: `Additional ${remainingFiles.length} files not analyzed in detail: ${summaryParts.join(', ')}`,
478
+ processed: true,
479
+ compressionApplied: true,
480
+ isSummary: true,
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Categorize a file for summary purposes
486
+ */
487
+ categorizeFile(file) {
488
+ const filePath = file.filePath || file.path || ''
489
+
490
+ if (filePath.includes('/test/') || filePath.includes('.test.') || filePath.includes('.spec.')) {
491
+ return 'test'
492
+ }
493
+ if (filePath.endsWith('.md') || filePath.includes('/docs/')) {
494
+ return 'documentation'
495
+ }
496
+ if (filePath.includes('package') || filePath.includes('lock') || filePath.includes('config')) {
497
+ return 'configuration'
498
+ }
499
+ if (filePath.includes('/build/') || filePath.includes('/dist/')) {
500
+ return 'build'
501
+ }
502
+ return 'other'
503
+ }
504
+
505
+ /**
506
+ * Estimate size of processed content
507
+ */
508
+ estimateSize(content) {
509
+ return (content || '').length
510
+ }
511
+ }
512
+
513
+ export default DiffProcessor