@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,556 @@
1
+ import colors from '../../shared/constants/colors.js'
2
+ import { EnhancedConsole } from '../../shared/utils/cli-ui.js'
3
+
4
+ /**
5
+ * Commit Message Validation Service
6
+ *
7
+ * Provides comprehensive commit message validation based on:
8
+ * - Conventional Commits specification
9
+ * - Configuration-based rules
10
+ * - Branch intelligence context
11
+ * - Best practices for readability and maintainability
12
+ */
13
+ export class CommitMessageValidationService {
14
+ constructor(configManager) {
15
+ this.configManager = configManager
16
+ this.config = this.loadValidationConfig()
17
+ }
18
+
19
+ /**
20
+ * Load validation configuration from config files
21
+ */
22
+ loadValidationConfig() {
23
+ const config = this.configManager.getAll() || {}
24
+
25
+ // Default validation rules
26
+ const defaults = {
27
+ commitTypes: [
28
+ 'feat',
29
+ 'fix',
30
+ 'docs',
31
+ 'style',
32
+ 'refactor',
33
+ 'perf',
34
+ 'test',
35
+ 'build',
36
+ 'ci',
37
+ 'chore',
38
+ 'revert',
39
+ ],
40
+ commitScopes: [], // Empty means any scope is allowed
41
+ maxSubjectLength: 72,
42
+ minSubjectLength: 10,
43
+ requireScope: false,
44
+ requireBody: false,
45
+ requireFooter: false,
46
+ allowBreakingChanges: true,
47
+ subjectCase: 'lower', // 'lower', 'sentence', 'any'
48
+ subjectEndPunctuation: false, // Don't allow period at end
49
+ bodyLineLength: 100,
50
+ footerFormat: 'conventional', // 'conventional', 'any'
51
+ }
52
+
53
+ // Merge with config from ai-changelog.config.yaml
54
+ const yamlConfig = config.convention || {}
55
+
56
+ return {
57
+ ...defaults,
58
+ ...yamlConfig,
59
+ // Override with specific validation settings if they exist
60
+ commitTypes: yamlConfig.commitTypes || defaults.commitTypes,
61
+ commitScopes: yamlConfig.commitScopes || defaults.commitScopes,
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Comprehensive commit message validation
67
+ */
68
+ async validateCommitMessage(message, context = {}) {
69
+ if (!message || typeof message !== 'string') {
70
+ return this.createValidationResult(false, ['Commit message is required'], [])
71
+ }
72
+
73
+ const trimmedMessage = message.trim()
74
+ if (trimmedMessage.length === 0) {
75
+ return this.createValidationResult(false, ['Commit message cannot be empty'], [])
76
+ }
77
+
78
+ const lines = trimmedMessage.split('\n')
79
+ const subject = lines[0]
80
+ const body = lines.slice(2).join('\n').trim() // Skip blank line after subject
81
+ const hasBlankLineAfterSubject = lines.length > 1 && lines[1].trim() === ''
82
+
83
+ const errors = []
84
+ const warnings = []
85
+ const suggestions = []
86
+
87
+ // Parse conventional commit format
88
+ const conventionalCommit = this.parseConventionalCommit(subject)
89
+
90
+ // Subject validation
91
+ this.validateSubject(subject, conventionalCommit, errors, warnings, suggestions, context)
92
+
93
+ // Body validation
94
+ if (lines.length > 1) {
95
+ this.validateBody(body, hasBlankLineAfterSubject, lines, errors, warnings, suggestions)
96
+ }
97
+
98
+ // Footer validation
99
+ const footerLines = this.extractFooterLines(lines)
100
+ if (footerLines.length > 0) {
101
+ this.validateFooter(footerLines, errors, warnings, suggestions)
102
+ }
103
+
104
+ // Context-based validation (branch intelligence)
105
+ if (context.branchAnalysis) {
106
+ this.validateAgainstBranchContext(
107
+ conventionalCommit,
108
+ context.branchAnalysis,
109
+ warnings,
110
+ suggestions
111
+ )
112
+ }
113
+
114
+ // Configuration-based validation
115
+ this.validateAgainstConfig(conventionalCommit, errors, warnings, suggestions)
116
+
117
+ const isValid = errors.length === 0
118
+ const score = this.calculateValidationScore(errors, warnings, suggestions)
119
+
120
+ return this.createValidationResult(
121
+ isValid,
122
+ errors,
123
+ warnings,
124
+ suggestions,
125
+ score,
126
+ conventionalCommit
127
+ )
128
+ }
129
+
130
+ /**
131
+ * Parse conventional commit format
132
+ */
133
+ parseConventionalCommit(subject) {
134
+ // Enhanced pattern to capture all parts
135
+ const conventionalPattern = /^([a-z]+)(\(([^)]+)\))?(!)?: (.+)$/
136
+ const match = subject.match(conventionalPattern)
137
+
138
+ if (!match) {
139
+ return {
140
+ type: null,
141
+ scope: null,
142
+ breaking: false,
143
+ description: subject,
144
+ isConventional: false,
145
+ }
146
+ }
147
+
148
+ return {
149
+ type: match[1],
150
+ scope: match[3] || null,
151
+ breaking: !!match[4], // Breaking change indicator (!)
152
+ description: match[5],
153
+ isConventional: true,
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Validate subject line
159
+ */
160
+ validateSubject(subject, parsed, errors, warnings, suggestions, _context) {
161
+ // Length validation
162
+ if (subject.length < this.config.minSubjectLength) {
163
+ errors.push(
164
+ `Subject too short (${subject.length} chars, minimum ${this.config.minSubjectLength})`
165
+ )
166
+ suggestions.push('Add more detail about what was changed')
167
+ }
168
+
169
+ if (subject.length > this.config.maxSubjectLength) {
170
+ errors.push(
171
+ `Subject too long (${subject.length} chars, maximum ${this.config.maxSubjectLength})`
172
+ )
173
+ suggestions.push('Move additional details to the commit body')
174
+ }
175
+
176
+ // Conventional commit format validation
177
+ if (!parsed.isConventional) {
178
+ errors.push('Subject does not follow conventional commit format')
179
+ suggestions.push('Use format: type(scope): description (e.g., "feat: add new feature")')
180
+ return // Skip further validation if not conventional
181
+ }
182
+
183
+ // Type validation
184
+ if (!this.config.commitTypes.includes(parsed.type)) {
185
+ errors.push(`Invalid commit type: "${parsed.type}"`)
186
+ suggestions.push(`Use one of: ${this.config.commitTypes.join(', ')}`)
187
+ }
188
+
189
+ // Scope validation
190
+ if (
191
+ this.config.commitScopes.length > 0 &&
192
+ parsed.scope &&
193
+ !this.config.commitScopes.includes(parsed.scope)
194
+ ) {
195
+ warnings.push(`Unexpected scope: "${parsed.scope}"`)
196
+ suggestions.push(`Suggested scopes: ${this.config.commitScopes.join(', ')}`)
197
+ }
198
+
199
+ if (this.config.requireScope && !parsed.scope) {
200
+ errors.push('Scope is required for this repository')
201
+ suggestions.push('Add scope in parentheses: type(scope): description')
202
+ }
203
+
204
+ // Description validation
205
+ if (!parsed.description || parsed.description.trim().length === 0) {
206
+ errors.push('Description is required')
207
+ suggestions.push('Add a clear description of what was changed')
208
+ }
209
+
210
+ // Case validation
211
+ if (this.config.subjectCase === 'lower' && parsed.description) {
212
+ const firstChar = parsed.description.charAt(0)
213
+ if (firstChar !== firstChar.toLowerCase()) {
214
+ warnings.push('Description should start with lowercase letter')
215
+ suggestions.push(`Change "${firstChar}" to "${firstChar.toLowerCase()}"`)
216
+ }
217
+ }
218
+
219
+ // End punctuation validation
220
+ if (
221
+ !this.config.subjectEndPunctuation &&
222
+ parsed.description &&
223
+ parsed.description.endsWith('.')
224
+ ) {
225
+ warnings.push('Subject should not end with a period')
226
+ suggestions.push('Remove the trailing period')
227
+ }
228
+
229
+ // Imperative mood validation
230
+ if (parsed.description) {
231
+ this.validateImperativeMood(parsed.description, warnings, suggestions)
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Validate imperative mood
237
+ */
238
+ validateImperativeMood(description, warnings, suggestions) {
239
+ const imperativeVerbs = [
240
+ 'add',
241
+ 'remove',
242
+ 'fix',
243
+ 'update',
244
+ 'create',
245
+ 'delete',
246
+ 'implement',
247
+ 'refactor',
248
+ 'improve',
249
+ 'enhance',
250
+ 'optimize',
251
+ 'change',
252
+ 'move',
253
+ 'rename',
254
+ 'replace',
255
+ 'upgrade',
256
+ 'downgrade',
257
+ 'install',
258
+ 'uninstall',
259
+ 'configure',
260
+ 'setup',
261
+ 'initialize',
262
+ 'clean',
263
+ 'format',
264
+ 'lint',
265
+ 'test',
266
+ 'document',
267
+ ]
268
+
269
+ const nonImperativeIndicators = [
270
+ 'added',
271
+ 'removed',
272
+ 'fixed',
273
+ 'updated',
274
+ 'created',
275
+ 'deleted',
276
+ 'implemented',
277
+ 'improved',
278
+ 'enhanced',
279
+ 'optimized',
280
+ 'changed',
281
+ 'moved',
282
+ 'renamed',
283
+ 'replaced',
284
+ 'upgraded',
285
+ 'downgraded',
286
+ 'installed',
287
+ 'uninstalled',
288
+ 'configured',
289
+ 'initialized',
290
+ 'cleaned',
291
+ 'formatted',
292
+ 'linted',
293
+ 'tested',
294
+ 'documented',
295
+ ]
296
+
297
+ const firstWord = description.split(' ')[0].toLowerCase()
298
+
299
+ if (nonImperativeIndicators.includes(firstWord)) {
300
+ warnings.push('Use imperative mood in description')
301
+ // Try to suggest imperative form
302
+ const imperative = firstWord.replace(/ed$/, '').replace(/d$/, '')
303
+ if (imperativeVerbs.includes(imperative)) {
304
+ suggestions.push(`Change "${firstWord}" to "${imperative}"`)
305
+ } else {
306
+ suggestions.push('Use imperative mood (e.g., "fix bug" not "fixed bug")')
307
+ }
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Validate body
313
+ */
314
+ validateBody(body, hasBlankLine, _lines, errors, warnings, suggestions) {
315
+ // Blank line separation
316
+ if (!hasBlankLine) {
317
+ errors.push('Missing blank line between subject and body')
318
+ suggestions.push('Add a blank line after the subject')
319
+ }
320
+
321
+ // Body line length
322
+ if (body) {
323
+ const bodyLines = body.split('\n')
324
+ bodyLines.forEach((line, index) => {
325
+ if (line.length > this.config.bodyLineLength) {
326
+ warnings.push(
327
+ `Body line ${index + 1} too long (${line.length} chars, recommended max ${this.config.bodyLineLength})`
328
+ )
329
+ }
330
+ })
331
+ }
332
+
333
+ // Required body
334
+ if (this.config.requireBody && (!body || body.trim().length === 0)) {
335
+ errors.push('Commit body is required')
336
+ suggestions.push('Add details about the changes in the commit body')
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Extract footer lines (last paragraph that contains key-value pairs)
342
+ */
343
+ extractFooterLines(lines) {
344
+ if (lines.length < 3) {
345
+ return []
346
+ }
347
+
348
+ const footerLines = []
349
+ for (let i = lines.length - 1; i >= 0; i--) {
350
+ const line = lines[i].trim()
351
+ if (line === '') {
352
+ break // Empty line indicates end of footer
353
+ }
354
+ if (line.includes(':') || line.match(/^(BREAKING CHANGE|Closes?|Fixes?|Refs?)/i)) {
355
+ footerLines.unshift(line)
356
+ } else {
357
+ break // Non-footer line
358
+ }
359
+ }
360
+
361
+ return footerLines
362
+ }
363
+
364
+ /**
365
+ * Validate footer
366
+ */
367
+ validateFooter(footerLines, errors, warnings, suggestions) {
368
+ footerLines.forEach((line) => {
369
+ // Validate footer format
370
+ if (
371
+ this.config.footerFormat === 'conventional' &&
372
+ !(line.match(/^[A-Za-z-]+: .+/) || line.match(/^BREAKING CHANGE: .+/))
373
+ ) {
374
+ warnings.push(`Footer line doesn't follow conventional format: "${line}"`)
375
+ suggestions.push('Use format: "Key: value" or "BREAKING CHANGE: description"')
376
+ }
377
+
378
+ // Validate breaking changes
379
+ if (line.startsWith('BREAKING CHANGE:') && !this.config.allowBreakingChanges) {
380
+ errors.push('Breaking changes are not allowed in this repository')
381
+ }
382
+ })
383
+ }
384
+
385
+ /**
386
+ * Validate against branch context
387
+ */
388
+ validateAgainstBranchContext(parsed, branchAnalysis, warnings, suggestions) {
389
+ if (!branchAnalysis || branchAnalysis.confidence < 50) {
390
+ return
391
+ }
392
+
393
+ // Type mismatch
394
+ if (branchAnalysis.type && parsed.type && branchAnalysis.type !== parsed.type) {
395
+ warnings.push(
396
+ `Commit type "${parsed.type}" doesn't match branch type "${branchAnalysis.type}"`
397
+ )
398
+ suggestions.push(`Consider using type "${branchAnalysis.type}" based on branch name`)
399
+ }
400
+
401
+ // Missing ticket reference
402
+ if (branchAnalysis.ticket && !this.containsTicketReference(parsed, branchAnalysis.ticket)) {
403
+ suggestions.push(`Consider adding ticket reference: ${branchAnalysis.ticket}`)
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Check if commit message contains ticket reference
409
+ */
410
+ containsTicketReference(parsed, ticket) {
411
+ const fullMessage = `${parsed.type}${parsed.scope ? `(${parsed.scope})` : ''}: ${parsed.description}`
412
+ return fullMessage.includes(ticket)
413
+ }
414
+
415
+ /**
416
+ * Validate against configuration
417
+ */
418
+ validateAgainstConfig(_parsed, _errors, _warnings, _suggestions) {
419
+ // This is handled in other validation methods
420
+ // Additional config-specific validations can be added here
421
+ }
422
+
423
+ /**
424
+ * Calculate validation score
425
+ */
426
+ calculateValidationScore(errors, warnings, suggestions) {
427
+ let score = 100
428
+ score -= errors.length * 25 // Major issues
429
+ score -= warnings.length * 10 // Minor issues
430
+ score -= suggestions.length * 5 // Improvements
431
+ return Math.max(0, score)
432
+ }
433
+
434
+ /**
435
+ * Create validation result object
436
+ */
437
+ createValidationResult(
438
+ isValid,
439
+ errors = [],
440
+ warnings = [],
441
+ suggestions = [],
442
+ score = 0,
443
+ parsed = null
444
+ ) {
445
+ return {
446
+ valid: isValid,
447
+ errors,
448
+ warnings,
449
+ suggestions,
450
+ score,
451
+ parsed,
452
+ summary: this.generateValidationSummary(isValid, errors, warnings, suggestions, score),
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Generate human-readable validation summary
458
+ */
459
+ generateValidationSummary(isValid, errors, warnings, suggestions, score) {
460
+ if (isValid && warnings.length === 0 && suggestions.length === 0) {
461
+ return '✅ Perfect commit message!'
462
+ }
463
+
464
+ const parts = []
465
+
466
+ if (errors.length > 0) {
467
+ parts.push(`${errors.length} error${errors.length === 1 ? '' : 's'}`)
468
+ }
469
+
470
+ if (warnings.length > 0) {
471
+ parts.push(`${warnings.length} warning${warnings.length === 1 ? '' : 's'}`)
472
+ }
473
+
474
+ if (suggestions.length > 0) {
475
+ parts.push(`${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`)
476
+ }
477
+
478
+ const status = isValid ? '✅' : '❌'
479
+ return `${status} ${parts.join(', ')} (Score: ${score}/100)`
480
+ }
481
+
482
+ /**
483
+ * Display validation results with colors
484
+ */
485
+ displayValidationResults(validationResult) {
486
+ const { valid, errors, warnings, suggestions, score, summary } = validationResult
487
+
488
+ EnhancedConsole.section('🔍 Commit Message Validation')
489
+ console.log(colors.secondary(`${summary}`))
490
+
491
+ if (errors.length > 0) {
492
+ EnhancedConsole.space()
493
+ console.log(colors.statusSymbol('error', 'Errors (must fix):'))
494
+ errors.forEach((error) => {
495
+ console.log(` ${colors.symbols.bullet} ${colors.error(error)}`)
496
+ })
497
+ }
498
+
499
+ if (warnings.length > 0) {
500
+ EnhancedConsole.space()
501
+ console.log(colors.statusSymbol('warning', 'Warnings (recommended to fix):'))
502
+ warnings.forEach((warning) => {
503
+ console.log(` ${colors.symbols.bullet} ${colors.warning(warning)}`)
504
+ })
505
+ }
506
+
507
+ if (suggestions.length > 0) {
508
+ EnhancedConsole.space()
509
+ console.log(colors.statusSymbol('info', 'Suggestions (optional improvements):'))
510
+ suggestions.forEach((suggestion) => {
511
+ console.log(` ${colors.symbols.bullet} ${colors.dim(suggestion)}`)
512
+ })
513
+ }
514
+
515
+ return valid
516
+ }
517
+
518
+ /**
519
+ * Interactive commit message improvement
520
+ */
521
+ async improveCommitMessage(message, context = {}) {
522
+ const validation = await this.validateCommitMessage(message, context)
523
+
524
+ if (validation.valid && validation.warnings.length === 0) {
525
+ return { improved: false, message, validation }
526
+ }
527
+
528
+ // Generate improved message based on validation results
529
+ let improved = message
530
+
531
+ // Fix common issues automatically
532
+ if (validation.parsed) {
533
+ const { type, scope, description, breaking } = validation.parsed
534
+
535
+ // Fix case issues
536
+ if (description) {
537
+ let fixedDescription = description
538
+
539
+ // Fix capitalization
540
+ if (this.config.subjectCase === 'lower') {
541
+ fixedDescription = fixedDescription.charAt(0).toLowerCase() + fixedDescription.slice(1)
542
+ }
543
+
544
+ // Remove trailing period
545
+ if (!this.config.subjectEndPunctuation && fixedDescription.endsWith('.')) {
546
+ fixedDescription = fixedDescription.slice(0, -1)
547
+ }
548
+
549
+ // Reconstruct subject
550
+ improved = `${type}${scope ? `(${scope})` : ''}${breaking ? '!' : ''}: ${fixedDescription}`
551
+ }
552
+ }
553
+
554
+ return { improved: improved !== message, message: improved, validation }
555
+ }
556
+ }