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