@m3hti/commit-genie 1.2.3 → 1.2.5
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/package.json +1 -1
- package/dist/commands/config.d.ts +0 -3
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js +0 -31
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/generate.d.ts +0 -12
- package/dist/commands/generate.d.ts.map +0 -1
- package/dist/commands/generate.js +0 -550
- package/dist/commands/generate.js.map +0 -1
- package/dist/commands/generate.test.d.ts +0 -2
- package/dist/commands/generate.test.d.ts.map +0 -1
- package/dist/commands/generate.test.js +0 -168
- package/dist/commands/generate.test.js.map +0 -1
- package/dist/commands/hook.d.ts +0 -4
- package/dist/commands/hook.d.ts.map +0 -1
- package/dist/commands/hook.js +0 -62
- package/dist/commands/hook.js.map +0 -1
- package/dist/commands/stats.d.ts +0 -6
- package/dist/commands/stats.d.ts.map +0 -1
- package/dist/commands/stats.js +0 -39
- package/dist/commands/stats.js.map +0 -1
- package/dist/data/wordlist.d.ts +0 -11
- package/dist/data/wordlist.d.ts.map +0 -1
- package/dist/data/wordlist.js +0 -170
- package/dist/data/wordlist.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -81
- package/dist/index.js.map +0 -1
- package/dist/services/aiService.d.ts +0 -42
- package/dist/services/aiService.d.ts.map +0 -1
- package/dist/services/aiService.js +0 -224
- package/dist/services/aiService.js.map +0 -1
- package/dist/services/analyzerService.d.ts +0 -124
- package/dist/services/analyzerService.d.ts.map +0 -1
- package/dist/services/analyzerService.js +0 -1595
- package/dist/services/analyzerService.js.map +0 -1
- package/dist/services/analyzerService.test.d.ts +0 -2
- package/dist/services/analyzerService.test.d.ts.map +0 -1
- package/dist/services/analyzerService.test.js +0 -483
- package/dist/services/analyzerService.test.js.map +0 -1
- package/dist/services/configService.d.ts +0 -25
- package/dist/services/configService.d.ts.map +0 -1
- package/dist/services/configService.js +0 -228
- package/dist/services/configService.js.map +0 -1
- package/dist/services/configService.test.d.ts +0 -2
- package/dist/services/configService.test.d.ts.map +0 -1
- package/dist/services/configService.test.js +0 -165
- package/dist/services/configService.test.js.map +0 -1
- package/dist/services/gitService.d.ts +0 -64
- package/dist/services/gitService.d.ts.map +0 -1
- package/dist/services/gitService.js +0 -303
- package/dist/services/gitService.js.map +0 -1
- package/dist/services/gitService.test.d.ts +0 -2
- package/dist/services/gitService.test.d.ts.map +0 -1
- package/dist/services/gitService.test.js +0 -140
- package/dist/services/gitService.test.js.map +0 -1
- package/dist/services/historyService.d.ts +0 -39
- package/dist/services/historyService.d.ts.map +0 -1
- package/dist/services/historyService.js +0 -195
- package/dist/services/historyService.js.map +0 -1
- package/dist/services/historyService.test.d.ts +0 -2
- package/dist/services/historyService.test.d.ts.map +0 -1
- package/dist/services/historyService.test.js +0 -157
- package/dist/services/historyService.test.js.map +0 -1
- package/dist/services/hookService.d.ts +0 -29
- package/dist/services/hookService.d.ts.map +0 -1
- package/dist/services/hookService.js +0 -164
- package/dist/services/hookService.js.map +0 -1
- package/dist/services/lintService.d.ts +0 -32
- package/dist/services/lintService.d.ts.map +0 -1
- package/dist/services/lintService.js +0 -191
- package/dist/services/lintService.js.map +0 -1
- package/dist/services/spellService.d.ts +0 -42
- package/dist/services/spellService.d.ts.map +0 -1
- package/dist/services/spellService.js +0 -175
- package/dist/services/spellService.js.map +0 -1
- package/dist/services/splitService.d.ts +0 -32
- package/dist/services/splitService.d.ts.map +0 -1
- package/dist/services/splitService.js +0 -184
- package/dist/services/splitService.js.map +0 -1
- package/dist/services/statsService.d.ts +0 -28
- package/dist/services/statsService.d.ts.map +0 -1
- package/dist/services/statsService.js +0 -204
- package/dist/services/statsService.js.map +0 -1
- package/dist/types/index.d.ts +0 -217
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +0 -1
- package/dist/utils/filePatterns.d.ts +0 -5
- package/dist/utils/filePatterns.d.ts.map +0 -1
- package/dist/utils/filePatterns.js +0 -77
- package/dist/utils/filePatterns.js.map +0 -1
- package/dist/utils/filePatterns.test.d.ts +0 -2
- package/dist/utils/filePatterns.test.d.ts.map +0 -1
- package/dist/utils/filePatterns.test.js +0 -51
- package/dist/utils/filePatterns.test.js.map +0 -1
- package/dist/utils/prompt.d.ts +0 -4
- package/dist/utils/prompt.d.ts.map +0 -1
- package/dist/utils/prompt.js +0 -60
- package/dist/utils/prompt.js.map +0 -1
- package/npm-output.txt +0 -0
|
@@ -1,1595 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AnalyzerService = void 0;
|
|
4
|
-
const gitService_1 = require("./gitService");
|
|
5
|
-
const configService_1 = require("./configService");
|
|
6
|
-
const historyService_1 = require("./historyService");
|
|
7
|
-
const aiService_1 = require("./aiService");
|
|
8
|
-
const filePatterns_1 = require("../utils/filePatterns");
|
|
9
|
-
const COMMIT_EMOJIS = {
|
|
10
|
-
feat: '✨',
|
|
11
|
-
fix: '🐛',
|
|
12
|
-
docs: '📚',
|
|
13
|
-
style: '💄',
|
|
14
|
-
refactor: '♻️',
|
|
15
|
-
test: '🧪',
|
|
16
|
-
chore: '🔧',
|
|
17
|
-
perf: '⚡',
|
|
18
|
-
};
|
|
19
|
-
// Default keywords that indicate breaking changes
|
|
20
|
-
const DEFAULT_BREAKING_KEYWORDS = [
|
|
21
|
-
'breaking',
|
|
22
|
-
'breaking change',
|
|
23
|
-
'breaking-change',
|
|
24
|
-
'removed',
|
|
25
|
-
'deprecated',
|
|
26
|
-
'incompatible',
|
|
27
|
-
];
|
|
28
|
-
// Patterns that indicate breaking changes in code
|
|
29
|
-
const BREAKING_PATTERNS = [
|
|
30
|
-
// Removed exports
|
|
31
|
-
/^-\s*export\s+(function|class|const|let|var|interface|type|enum)\s+(\w+)/gm,
|
|
32
|
-
// Removed function parameters
|
|
33
|
-
/^-\s*(public|private|protected)?\s*(async\s+)?function\s+\w+\s*\([^)]+\)/gm,
|
|
34
|
-
// Changed function signatures (removed parameters)
|
|
35
|
-
/^-\s*\w+\s*\([^)]+\)\s*[:{]/gm,
|
|
36
|
-
// Removed class methods
|
|
37
|
-
/^-\s*(public|private|protected)\s+(static\s+)?(async\s+)?\w+\s*\(/gm,
|
|
38
|
-
// Removed interface/type properties
|
|
39
|
-
/^-\s+\w+\s*[?:]?\s*:/gm,
|
|
40
|
-
// Major version bump in package.json
|
|
41
|
-
/^-\s*"version":\s*"\d+/gm,
|
|
42
|
-
];
|
|
43
|
-
// Role detection patterns for semantic analysis
|
|
44
|
-
const ROLE_PATTERNS = {
|
|
45
|
-
ui: [
|
|
46
|
-
// JSX elements and components
|
|
47
|
-
/^\+.*<[A-Z][a-zA-Z]*[\s/>]/m, // JSX component usage
|
|
48
|
-
/^\+.*<\/[A-Z][a-zA-Z]*>/m, // JSX component closing
|
|
49
|
-
/^\+.*<(div|span|p|h[1-6]|button|input|form|ul|li|table|img|a|section|header|footer|nav|main|article|aside)\b/mi,
|
|
50
|
-
/^\+.*className\s*=/m, // className attribute
|
|
51
|
-
/^\+.*style\s*=\s*\{/m, // inline styles
|
|
52
|
-
/^\+.*return\s*\(/m, // render return
|
|
53
|
-
/^\+.*render\s*\(\s*\)/m, // render method
|
|
54
|
-
/^\+.*<>|<\/>/m, // React fragments
|
|
55
|
-
/^\+.*aria-\w+=/m, // accessibility attributes
|
|
56
|
-
/^\+.*role\s*=/m, // role attribute
|
|
57
|
-
/^\+.*onClick|onSubmit|onChange|onFocus|onBlur|onKeyDown|onKeyUp|onMouseEnter|onMouseLeave/m,
|
|
58
|
-
],
|
|
59
|
-
logic: [
|
|
60
|
-
// Business logic patterns
|
|
61
|
-
/^\+.*\bif\s*\(/m, // conditionals
|
|
62
|
-
/^\+.*\bswitch\s*\(/m, // switch statements
|
|
63
|
-
/^\+.*\bfor\s*\(/m, // for loops
|
|
64
|
-
/^\+.*\bwhile\s*\(/m, // while loops
|
|
65
|
-
/^\+.*\.map\s*\(/m, // array operations
|
|
66
|
-
/^\+.*\.filter\s*\(/m,
|
|
67
|
-
/^\+.*\.reduce\s*\(/m,
|
|
68
|
-
/^\+.*\.find\s*\(/m,
|
|
69
|
-
/^\+.*\.some\s*\(/m,
|
|
70
|
-
/^\+.*\.every\s*\(/m,
|
|
71
|
-
/^\+.*\btry\s*\{/m, // error handling
|
|
72
|
-
/^\+.*\bcatch\s*\(/m,
|
|
73
|
-
/^\+.*\bthrow\s+/m,
|
|
74
|
-
/^\+.*\breturn\s+(?![\s(]?<)/m, // return (not JSX)
|
|
75
|
-
/^\+.*\bawait\s+/m, // async operations
|
|
76
|
-
/^\+.*\bnew\s+[A-Z]/m, // object instantiation
|
|
77
|
-
/^\+.*=>\s*\{/m, // arrow function body
|
|
78
|
-
],
|
|
79
|
-
data: [
|
|
80
|
-
// State and data management
|
|
81
|
-
/^\+.*\buseState\s*[<(]/m, // React state
|
|
82
|
-
/^\+.*\buseReducer\s*\(/m, // React reducer
|
|
83
|
-
/^\+.*\buseContext\s*\(/m, // React context
|
|
84
|
-
/^\+.*\buseSelector\s*\(/m, // Redux selector
|
|
85
|
-
/^\+.*\bdispatch\s*\(/m, // Redux dispatch
|
|
86
|
-
/^\+.*\bsetState\s*\(/m, // Class component state
|
|
87
|
-
/^\+.*\bthis\.state\b/m, // Class component state access
|
|
88
|
-
/^\+.*\bprops\./m, // Props access
|
|
89
|
-
/^\+.*interface\s+\w+Props/m, // Props interface
|
|
90
|
-
/^\+.*type\s+\w+Props/m, // Props type
|
|
91
|
-
/^\+.*\bconst\s+\[[a-z]+,\s*set[A-Z]/m, // State destructuring
|
|
92
|
-
/^\+.*:\s*\w+\[\]/m, // Array type
|
|
93
|
-
/^\+.*:\s*(string|number|boolean|object)/m, // Primitive types
|
|
94
|
-
/^\+.*\binterface\s+\w+/m, // Interface definition
|
|
95
|
-
/^\+.*\btype\s+\w+\s*=/m, // Type definition
|
|
96
|
-
],
|
|
97
|
-
style: [
|
|
98
|
-
// CSS and styling
|
|
99
|
-
/^\+.*\bstyles?\./m, // styles object access
|
|
100
|
-
/^\+.*\bstyled\./m, // styled-components
|
|
101
|
-
/^\+.*\bcss`/m, // CSS template literal
|
|
102
|
-
/^\+.*\bsx\s*=\s*\{/m, // MUI sx prop
|
|
103
|
-
/^\+.*:\s*['"]?[0-9]+(px|em|rem|%|vh|vw)/m, // CSS units
|
|
104
|
-
/^\+.*:\s*['"]?#[0-9a-fA-F]{3,8}/m, // Hex colors
|
|
105
|
-
/^\+.*:\s*['"]?rgb(a)?\s*\(/m, // RGB colors
|
|
106
|
-
/^\+.*(margin|padding|width|height|border|background|color|font|display|flex|grid)\s*:/m,
|
|
107
|
-
/^\+.*\btheme\./m, // Theme access
|
|
108
|
-
/^\+.*\.module\.css/m, // CSS modules import
|
|
109
|
-
],
|
|
110
|
-
api: [
|
|
111
|
-
// API and network calls
|
|
112
|
-
/^\+.*\bfetch\s*\(/m, // fetch API
|
|
113
|
-
/^\+.*\baxios\./m, // axios
|
|
114
|
-
/^\+.*\.get\s*\(/m, // HTTP GET
|
|
115
|
-
/^\+.*\.post\s*\(/m, // HTTP POST
|
|
116
|
-
/^\+.*\.put\s*\(/m, // HTTP PUT
|
|
117
|
-
/^\+.*\.delete\s*\(/m, // HTTP DELETE
|
|
118
|
-
/^\+.*\.patch\s*\(/m, // HTTP PATCH
|
|
119
|
-
/^\+.*\bapi\./m, // api object access
|
|
120
|
-
/^\+.*\/api\//m, // API path
|
|
121
|
-
/^\+.*\bendpoint/mi, // endpoint reference
|
|
122
|
-
/^\+.*\bheaders\s*:/m, // HTTP headers
|
|
123
|
-
/^\+.*\bAuthorization:/m, // Auth header
|
|
124
|
-
/^\+.*\buseQuery\s*\(/m, // React Query
|
|
125
|
-
/^\+.*\buseMutation\s*\(/m, // React Query mutation
|
|
126
|
-
/^\+.*\bswr\b/mi, // SWR
|
|
127
|
-
/^\+.*\bgraphql`/m, // GraphQL
|
|
128
|
-
/^\+.*\bquery\s*\{/m, // GraphQL query
|
|
129
|
-
/^\+.*\bmutation\s*\{/m, // GraphQL mutation
|
|
130
|
-
],
|
|
131
|
-
config: [
|
|
132
|
-
// Configuration
|
|
133
|
-
/^\+.*\bprocess\.env\./m, // Environment variables
|
|
134
|
-
/^\+.*\bimport\.meta\.env\./m, // Vite env
|
|
135
|
-
/^\+.*\bCONFIG\./m, // Config constant
|
|
136
|
-
/^\+.*\bsettings\./m, // Settings object
|
|
137
|
-
/^\+.*\boptions\s*:/m, // Options object
|
|
138
|
-
/^\+.*\bdefaultProps/m, // Default props
|
|
139
|
-
/^\+.*\bexport\s+(const|let)\s+[A-Z_]+\s*=/m, // Constant export
|
|
140
|
-
/^\+.*:\s*['"]?(development|production|test)['"]/m, // Environment strings
|
|
141
|
-
],
|
|
142
|
-
test: [
|
|
143
|
-
// Testing patterns
|
|
144
|
-
/^\+.*\bdescribe\s*\(/m, // Test suite
|
|
145
|
-
/^\+.*\bit\s*\(/m, // Test case
|
|
146
|
-
/^\+.*\btest\s*\(/m, // Test case
|
|
147
|
-
/^\+.*\bexpect\s*\(/m, // Assertion
|
|
148
|
-
/^\+.*\bjest\./m, // Jest
|
|
149
|
-
/^\+.*\bmock\(/m, // Mocking
|
|
150
|
-
/^\+.*\bspyOn\s*\(/m, // Spy
|
|
151
|
-
/^\+.*\bbeforeEach\s*\(/m, // Setup
|
|
152
|
-
/^\+.*\bafterEach\s*\(/m, // Teardown
|
|
153
|
-
/^\+.*\brender\s*\(/m, // React testing library
|
|
154
|
-
],
|
|
155
|
-
unknown: [],
|
|
156
|
-
};
|
|
157
|
-
// Intent detection patterns
|
|
158
|
-
const INTENT_PATTERNS = {
|
|
159
|
-
add: [
|
|
160
|
-
/^\+\s*export\s+(function|class|const|interface|type)/m,
|
|
161
|
-
/^\+\s*(async\s+)?function\s+\w+/m,
|
|
162
|
-
/^\+\s*const\s+\w+\s*=\s*(async\s+)?\(/m,
|
|
163
|
-
/^\+\s*class\s+\w+/m,
|
|
164
|
-
],
|
|
165
|
-
modify: [
|
|
166
|
-
// Changes that have both additions and deletions of similar patterns
|
|
167
|
-
],
|
|
168
|
-
fix: [
|
|
169
|
-
/^\+.*\btypeof\s+\w+\s*[!=]==?\s*['"`]/m,
|
|
170
|
-
/^\+.*\binstanceof\s+/m,
|
|
171
|
-
/^\+.*\bArray\.isArray\s*\(/m,
|
|
172
|
-
/^\+.*\bif\s*\(\s*!\w+\s*\)/m,
|
|
173
|
-
/^\+.*\?\?/m,
|
|
174
|
-
/^\+.*\?\./m,
|
|
175
|
-
/^\+.*\|\|\s*['"{\[0]/m, // Default values
|
|
176
|
-
/^\+.*\bcatch\s*\(/m,
|
|
177
|
-
/^\+.*\btry\s*\{/m,
|
|
178
|
-
],
|
|
179
|
-
remove: [
|
|
180
|
-
/^-\s*export\s+(function|class|const|interface|type)/m,
|
|
181
|
-
/^-\s*(async\s+)?function\s+\w+/m,
|
|
182
|
-
/^-\s*class\s+\w+/m,
|
|
183
|
-
],
|
|
184
|
-
refactor: [
|
|
185
|
-
/^\+.*=>/m, // Arrow function conversion
|
|
186
|
-
/^\+.*\.\.\./m, // Spread operator
|
|
187
|
-
/^\+.*`\$\{/m, // Template literal
|
|
188
|
-
/^\+.*Object\.(keys|values|entries)/m, // Object methods
|
|
189
|
-
],
|
|
190
|
-
enhance: [
|
|
191
|
-
/^\+.*\bmemo\s*\(/m, // React memo
|
|
192
|
-
/^\+.*\buseMemo\s*\(/m, // useMemo hook
|
|
193
|
-
/^\+.*\buseCallback\s*\(/m, // useCallback hook
|
|
194
|
-
/^\+.*\blazy\s*\(/m, // React lazy
|
|
195
|
-
/^\+.*\bSuspense\b/m, // React Suspense
|
|
196
|
-
],
|
|
197
|
-
};
|
|
198
|
-
// Role descriptions for commit messages
|
|
199
|
-
const ROLE_DESCRIPTIONS = {
|
|
200
|
-
ui: 'UI/rendering',
|
|
201
|
-
logic: 'business logic',
|
|
202
|
-
data: 'data/state management',
|
|
203
|
-
style: 'styling',
|
|
204
|
-
api: 'API integration',
|
|
205
|
-
config: 'configuration',
|
|
206
|
-
test: 'tests',
|
|
207
|
-
unknown: 'code',
|
|
208
|
-
};
|
|
209
|
-
// Intent verb mappings for commit messages
|
|
210
|
-
const INTENT_VERBS = {
|
|
211
|
-
add: { past: 'added', present: 'add' },
|
|
212
|
-
modify: { past: 'updated', present: 'update' },
|
|
213
|
-
fix: { past: 'fixed', present: 'fix' },
|
|
214
|
-
remove: { past: 'removed', present: 'remove' },
|
|
215
|
-
refactor: { past: 'refactored', present: 'refactor' },
|
|
216
|
-
enhance: { past: 'enhanced', present: 'improve' },
|
|
217
|
-
};
|
|
218
|
-
class AnalyzerService {
|
|
219
|
-
/**
|
|
220
|
-
* Analyze staged changes and return structured analysis
|
|
221
|
-
*/
|
|
222
|
-
static analyzeChanges() {
|
|
223
|
-
const stagedFiles = gitService_1.GitService.getStagedFiles();
|
|
224
|
-
const diff = gitService_1.GitService.getDiff();
|
|
225
|
-
const stats = gitService_1.GitService.getDiffStats();
|
|
226
|
-
const filesAffected = {
|
|
227
|
-
test: 0,
|
|
228
|
-
docs: 0,
|
|
229
|
-
config: 0,
|
|
230
|
-
source: 0,
|
|
231
|
-
};
|
|
232
|
-
const fileChanges = {
|
|
233
|
-
added: [],
|
|
234
|
-
modified: [],
|
|
235
|
-
deleted: [],
|
|
236
|
-
renamed: [],
|
|
237
|
-
};
|
|
238
|
-
// Analyze file types and statuses
|
|
239
|
-
for (const file of stagedFiles) {
|
|
240
|
-
const fileType = (0, filePatterns_1.detectFileType)(file.path);
|
|
241
|
-
filesAffected[fileType]++;
|
|
242
|
-
const fileName = this.getFileName(file.path);
|
|
243
|
-
switch (file.status) {
|
|
244
|
-
case 'A':
|
|
245
|
-
fileChanges.added.push(fileName);
|
|
246
|
-
break;
|
|
247
|
-
case 'M':
|
|
248
|
-
fileChanges.modified.push(fileName);
|
|
249
|
-
break;
|
|
250
|
-
case 'D':
|
|
251
|
-
fileChanges.deleted.push(fileName);
|
|
252
|
-
break;
|
|
253
|
-
case 'R':
|
|
254
|
-
fileChanges.renamed.push(fileName);
|
|
255
|
-
break;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
// Determine if this is a large change (3+ files or 100+ lines changed)
|
|
259
|
-
const totalChanges = stats.insertions + stats.deletions;
|
|
260
|
-
const isLargeChange = stagedFiles.length >= 3 || totalChanges >= 100;
|
|
261
|
-
// Determine commit type based on file types and changes
|
|
262
|
-
let commitType = this.determineCommitType(filesAffected, diff, stagedFiles);
|
|
263
|
-
// Determine scope if applicable
|
|
264
|
-
const scope = this.determineScope(stagedFiles);
|
|
265
|
-
// Detect breaking changes
|
|
266
|
-
const { isBreaking, reasons } = this.detectBreakingChanges(diff, stagedFiles);
|
|
267
|
-
// Perform semantic analysis for intent-based messages
|
|
268
|
-
// Only perform for source files to avoid overhead on simple config/doc changes
|
|
269
|
-
let semanticAnalysis;
|
|
270
|
-
if (filesAffected.source > 0 && diff.length > 0) {
|
|
271
|
-
semanticAnalysis = this.analyzeSemanticChanges(diff, stagedFiles);
|
|
272
|
-
}
|
|
273
|
-
// Apply semantic intent → commit type mapping policy
|
|
274
|
-
// Only override heuristic 'feat' type when semantic analysis provides specific intent
|
|
275
|
-
// This preserves specialized types (perf, docs, test, style, chore) detected by heuristics
|
|
276
|
-
if (semanticAnalysis && commitType === 'feat') {
|
|
277
|
-
const intentToType = {
|
|
278
|
-
fix: 'fix',
|
|
279
|
-
refactor: 'refactor',
|
|
280
|
-
};
|
|
281
|
-
const mappedType = intentToType[semanticAnalysis.primaryIntent];
|
|
282
|
-
if (mappedType) {
|
|
283
|
-
commitType = mappedType;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
// Generate description (uses semantic analysis when available)
|
|
287
|
-
const description = this.generateDescription(filesAffected, {
|
|
288
|
-
added: fileChanges.added.length,
|
|
289
|
-
modified: fileChanges.modified.length,
|
|
290
|
-
deleted: fileChanges.deleted.length,
|
|
291
|
-
renamed: fileChanges.renamed.length
|
|
292
|
-
}, stagedFiles, diff, semanticAnalysis);
|
|
293
|
-
return {
|
|
294
|
-
commitType,
|
|
295
|
-
scope,
|
|
296
|
-
description,
|
|
297
|
-
filesAffected,
|
|
298
|
-
fileChanges,
|
|
299
|
-
isLargeChange,
|
|
300
|
-
isBreakingChange: isBreaking,
|
|
301
|
-
breakingChangeReasons: reasons,
|
|
302
|
-
semanticAnalysis,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* Detect breaking changes from diff content and file changes
|
|
307
|
-
*/
|
|
308
|
-
static detectBreakingChanges(diff, stagedFiles) {
|
|
309
|
-
const config = configService_1.ConfigService.getConfig();
|
|
310
|
-
const breakingConfig = config.breakingChangeDetection;
|
|
311
|
-
// Check if breaking change detection is disabled
|
|
312
|
-
if (breakingConfig?.enabled === false) {
|
|
313
|
-
return { isBreaking: false, reasons: [] };
|
|
314
|
-
}
|
|
315
|
-
const reasons = [];
|
|
316
|
-
const diffLower = diff.toLowerCase();
|
|
317
|
-
// Check for keyword-based breaking changes
|
|
318
|
-
const keywords = breakingConfig?.keywords || DEFAULT_BREAKING_KEYWORDS;
|
|
319
|
-
for (const keyword of keywords) {
|
|
320
|
-
if (diffLower.includes(keyword.toLowerCase())) {
|
|
321
|
-
reasons.push(`Contains "${keyword}" keyword`);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
// Check for deleted source files (potentially breaking)
|
|
325
|
-
const deletedSourceFiles = stagedFiles.filter(f => f.status === 'D' && (0, filePatterns_1.detectFileType)(f.path) === 'source');
|
|
326
|
-
if (deletedSourceFiles.length > 0) {
|
|
327
|
-
const fileNames = deletedSourceFiles.map(f => this.getFileName(f.path)).join(', ');
|
|
328
|
-
reasons.push(`Deleted source files: ${fileNames}`);
|
|
329
|
-
}
|
|
330
|
-
// Check for pattern-based breaking changes in diff
|
|
331
|
-
for (const pattern of BREAKING_PATTERNS) {
|
|
332
|
-
pattern.lastIndex = 0; // Reset regex state
|
|
333
|
-
const matches = diff.match(pattern);
|
|
334
|
-
if (matches && matches.length > 0) {
|
|
335
|
-
// Identify what type of breaking change
|
|
336
|
-
if (pattern.source.includes('export')) {
|
|
337
|
-
reasons.push('Removed exported members');
|
|
338
|
-
}
|
|
339
|
-
else if (pattern.source.includes('function')) {
|
|
340
|
-
reasons.push('Changed function signatures');
|
|
341
|
-
}
|
|
342
|
-
else if (pattern.source.includes('public|private|protected')) {
|
|
343
|
-
reasons.push('Removed class methods');
|
|
344
|
-
}
|
|
345
|
-
else if (pattern.source.includes('version')) {
|
|
346
|
-
reasons.push('Major version change detected');
|
|
347
|
-
}
|
|
348
|
-
break; // Only add one pattern-based reason
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
// Check for renamed files that might break imports
|
|
352
|
-
const renamedFiles = stagedFiles.filter(f => f.status === 'R');
|
|
353
|
-
if (renamedFiles.length > 0) {
|
|
354
|
-
const sourceRenames = renamedFiles.filter(f => (0, filePatterns_1.detectFileType)(f.path) === 'source');
|
|
355
|
-
if (sourceRenames.length > 0) {
|
|
356
|
-
reasons.push('Renamed source files (may break imports)');
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
// Deduplicate reasons
|
|
360
|
-
const uniqueReasons = [...new Set(reasons)];
|
|
361
|
-
return {
|
|
362
|
-
isBreaking: uniqueReasons.length > 0,
|
|
363
|
-
reasons: uniqueReasons,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
/**
|
|
367
|
-
* Determine the commit type based on analysis
|
|
368
|
-
*/
|
|
369
|
-
static determineCommitType(filesAffected, diff, stagedFiles) {
|
|
370
|
-
const diffLower = diff.toLowerCase();
|
|
371
|
-
const filePaths = stagedFiles.map((f) => f.path.toLowerCase());
|
|
372
|
-
// === FILE TYPE BASED DETECTION (highest priority) ===
|
|
373
|
-
// If only test files changed
|
|
374
|
-
if (filesAffected.test > 0 &&
|
|
375
|
-
filesAffected.source === 0 &&
|
|
376
|
-
filesAffected.docs === 0) {
|
|
377
|
-
return 'test';
|
|
378
|
-
}
|
|
379
|
-
// If only docs changed
|
|
380
|
-
if (filesAffected.docs > 0 &&
|
|
381
|
-
filesAffected.source === 0 &&
|
|
382
|
-
filesAffected.test === 0) {
|
|
383
|
-
return 'docs';
|
|
384
|
-
}
|
|
385
|
-
// If only config files changed
|
|
386
|
-
if (filesAffected.config > 0 &&
|
|
387
|
-
filesAffected.source === 0 &&
|
|
388
|
-
filesAffected.test === 0 &&
|
|
389
|
-
filesAffected.docs === 0) {
|
|
390
|
-
return 'chore';
|
|
391
|
-
}
|
|
392
|
-
// === DOCS DETECTION (comment-only changes) ===
|
|
393
|
-
// Check if the changes are only adding/modifying comments (documentation)
|
|
394
|
-
if (this.isCommentOnlyChange(diff)) {
|
|
395
|
-
return 'docs';
|
|
396
|
-
}
|
|
397
|
-
// === STYLE DETECTION ===
|
|
398
|
-
// IMPORTANT: "style" type is ONLY for CSS/UI styling changes
|
|
399
|
-
// NOT for: console.log, debug output, comments, test prints, or formatting-only JS changes
|
|
400
|
-
// Check for CSS/styling file extensions
|
|
401
|
-
const isStyleFile = filePaths.some(p => p.endsWith('.css') ||
|
|
402
|
-
p.endsWith('.scss') ||
|
|
403
|
-
p.endsWith('.sass') ||
|
|
404
|
-
p.endsWith('.less') ||
|
|
405
|
-
p.endsWith('.styl') ||
|
|
406
|
-
p.endsWith('.stylus') ||
|
|
407
|
-
p.includes('.styles.') ||
|
|
408
|
-
p.includes('.style.') ||
|
|
409
|
-
p.includes('styles/') ||
|
|
410
|
-
p.includes('/theme') ||
|
|
411
|
-
p.includes('theme.') ||
|
|
412
|
-
p.includes('.theme.'));
|
|
413
|
-
// Check for styled-components, Tailwind, or inline style changes in diff content
|
|
414
|
-
const hasStyledComponentChanges = /^\+.*\bstyled\s*[.(]/m.test(diff) ||
|
|
415
|
-
/^\+.*\bcss\s*`/m.test(diff) ||
|
|
416
|
-
/^\+.*\@emotion\/styled/m.test(diff);
|
|
417
|
-
const hasTailwindChanges = /^\+.*\bclassName\s*=.*\b(flex|grid|bg-|text-|p-|m-|w-|h-|rounded|border|shadow|hover:|focus:|sm:|md:|lg:|xl:)/m.test(diff);
|
|
418
|
-
const hasInlineStyleChanges = /^\+.*\bstyle\s*=\s*\{\{/m.test(diff);
|
|
419
|
-
const hasThemeChanges = /^\+.*(theme\s*[:=]|colors\s*[:=]|palette\s*[:=]|spacing\s*[:=]|typography\s*[:=])/m.test(diff);
|
|
420
|
-
const hasSxPropChanges = /^\+.*\bsx\s*=\s*\{/m.test(diff);
|
|
421
|
-
// Only classify as "style" if it's a CSS file OR contains actual CSS/styling code
|
|
422
|
-
const hasActualStyleChanges = isStyleFile || hasStyledComponentChanges ||
|
|
423
|
-
hasTailwindChanges || hasInlineStyleChanges ||
|
|
424
|
-
hasThemeChanges || hasSxPropChanges;
|
|
425
|
-
// NEVER classify as style if it's just JS without CSS
|
|
426
|
-
// This prevents console.log, debug statements, comments, test prints from being labeled as style
|
|
427
|
-
if (hasActualStyleChanges && !this.hasOnlyNonStyleJsChanges(diff, filePaths)) {
|
|
428
|
-
return 'style';
|
|
429
|
-
}
|
|
430
|
-
// === PERFORMANCE DETECTION ===
|
|
431
|
-
const perfPatterns = [
|
|
432
|
-
/\bperformance\b/i,
|
|
433
|
-
/\boptimiz(e|ation|ing)\b/i,
|
|
434
|
-
/\bfaster\b/i,
|
|
435
|
-
/\bspeed\s*(up|improvement)\b/i,
|
|
436
|
-
/\bcach(e|ing)\b/i,
|
|
437
|
-
/\bmemoiz(e|ation)\b/i,
|
|
438
|
-
/\blazy\s*load/i,
|
|
439
|
-
/\basync\b.*\bawait\b/i,
|
|
440
|
-
/\bparallel\b/i,
|
|
441
|
-
/\bbatch(ing)?\b/i,
|
|
442
|
-
];
|
|
443
|
-
if (perfPatterns.some(p => p.test(diffLower))) {
|
|
444
|
-
return 'perf';
|
|
445
|
-
}
|
|
446
|
-
// === DETECT NEW FUNCTIONALITY FIRST ===
|
|
447
|
-
// This must come BEFORE fix detection to avoid false positives when new functions contain validation
|
|
448
|
-
const hasNewFiles = stagedFiles.some((f) => f.status === 'A');
|
|
449
|
-
// Comprehensive new code detection
|
|
450
|
-
const hasNewExports = /^\+\s*export\s+(function|class|const|let|var|interface|type|default)/m.test(diff);
|
|
451
|
-
const hasNewFunctions = /^\+\s*(async\s+)?function\s+\w+/m.test(diff);
|
|
452
|
-
const hasNewArrowFunctions = /^\+\s*(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>/m.test(diff);
|
|
453
|
-
const hasNewClasses = /^\+\s*(export\s+)?(class|abstract\s+class)\s+\w+/m.test(diff);
|
|
454
|
-
const hasNewMethods = /^\+\s+(async\s+)?\w+\s*\([^)]*\)\s*{/m.test(diff);
|
|
455
|
-
const hasNewInterfaces = /^\+\s*(export\s+)?(interface|type)\s+\w+/m.test(diff);
|
|
456
|
-
const hasNewComponents = /^\+\s*(export\s+)?(const|function)\s+[A-Z]\w+/m.test(diff); // React components start with capital
|
|
457
|
-
const hasNewImports = /^\+\s*import\s+/m.test(diff);
|
|
458
|
-
// Count significant additions vs removals
|
|
459
|
-
const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
|
|
460
|
-
const removedLines = (diff.match(/^-[^-]/gm) || []).length;
|
|
461
|
-
const netNewLines = addedLines - removedLines;
|
|
462
|
-
// Detect if we're adding new functionality
|
|
463
|
-
const hasNewFunctionality = hasNewExports || hasNewFunctions || hasNewArrowFunctions ||
|
|
464
|
-
hasNewClasses || hasNewMethods || hasNewInterfaces || hasNewComponents;
|
|
465
|
-
// === FEAT DETECTION (new functionality) - CHECK BEFORE FIX ===
|
|
466
|
-
if (hasNewFiles) {
|
|
467
|
-
return 'feat';
|
|
468
|
-
}
|
|
469
|
-
// New functions, classes, components = feat (even if they contain validation code)
|
|
470
|
-
if (hasNewFunctionality) {
|
|
471
|
-
return 'feat';
|
|
472
|
-
}
|
|
473
|
-
// Significant net additions with new imports often means new feature
|
|
474
|
-
if (hasNewImports && netNewLines > 5) {
|
|
475
|
-
return 'feat';
|
|
476
|
-
}
|
|
477
|
-
// Check for new feature indicators in comments/strings
|
|
478
|
-
const featPatterns = [
|
|
479
|
-
/\badd(s|ed|ing)?\s+(new\s+)?(feature|support|ability|option|functionality)/i,
|
|
480
|
-
/\bimplement(s|ed|ing)?\s+\w+/i,
|
|
481
|
-
/\bintroduc(e|es|ed|ing)\s+(new\s+)?\w+/i,
|
|
482
|
-
/\benable(s|d|ing)?\s+(new\s+)?\w+/i,
|
|
483
|
-
/\bnew\s+(feature|function|method|api|endpoint)/i,
|
|
484
|
-
];
|
|
485
|
-
if (featPatterns.some(p => p.test(diff))) {
|
|
486
|
-
return 'feat';
|
|
487
|
-
}
|
|
488
|
-
// === FIX DETECTION ===
|
|
489
|
-
// Only check for validation patterns AFTER confirming no new functions/classes were added
|
|
490
|
-
// Check if this is adding validation/guards to existing code (defensive coding = fix)
|
|
491
|
-
const hasOnlyModifications = stagedFiles.every((f) => f.status === 'M');
|
|
492
|
-
const validationPatterns = [
|
|
493
|
-
/^\+.*\btypeof\s+\w+\s*===?\s*['"`]\w+['"`]/m, // typeof checks
|
|
494
|
-
/^\+.*\binstanceof\s+\w+/m, // instanceof checks
|
|
495
|
-
/^\+.*\bArray\.isArray\s*\(/m, // Array.isArray checks
|
|
496
|
-
/^\+.*\b(Number|String|Boolean)\.is\w+\s*\(/m, // Number.isNaN, etc.
|
|
497
|
-
/^\+.*\bif\s*\(\s*typeof\b/m, // if (typeof ...
|
|
498
|
-
/^\+.*\bif\s*\(\s*!\w+\s*\)/m, // if (!var) guards
|
|
499
|
-
/^\+.*\bif\s*\(\s*\w+\s*(===?|!==?)\s*(null|undefined)\s*\)/m, // null/undefined checks
|
|
500
|
-
/^\+.*\bif\s*\(\s*(null|undefined)\s*(===?|!==?)\s*\w+\s*\)/m, // null/undefined checks (reversed)
|
|
501
|
-
/^\+.*\?\?/m, // Nullish coalescing
|
|
502
|
-
/^\+.*\?\./m, // Optional chaining
|
|
503
|
-
/^\+.*\|\|/m, // Default value patterns (when combined with guards)
|
|
504
|
-
];
|
|
505
|
-
// Check if this is a simplification refactor (using modern syntax to replace verbose code)
|
|
506
|
-
// When deletions > additions significantly, it's likely simplifying existing code, not adding new checks
|
|
507
|
-
const isSimplificationRefactor = removedLines > addedLines &&
|
|
508
|
-
(removedLines - addedLines) >= 3 && // At least 3 net lines removed (significant simplification)
|
|
509
|
-
(/^\+.*\?\./m.test(diff) || // Using optional chaining
|
|
510
|
-
/^\+.*\?\?/m.test(diff) // Using nullish coalescing
|
|
511
|
-
) &&
|
|
512
|
-
(/^-.*\bif\s*\(/m.test(diff) // Removing if statements
|
|
513
|
-
);
|
|
514
|
-
// If it's a simplification (replacing verbose ifs with optional chaining), it's refactor
|
|
515
|
-
if (isSimplificationRefactor) {
|
|
516
|
-
return 'refactor';
|
|
517
|
-
}
|
|
518
|
-
// If only modifying files (no new functions detected) and adding validation patterns, it's likely a fix
|
|
519
|
-
if (hasOnlyModifications && validationPatterns.some(p => p.test(diff))) {
|
|
520
|
-
return 'fix';
|
|
521
|
-
}
|
|
522
|
-
const fixPatterns = [
|
|
523
|
-
/\bfix(es|ed|ing)?\s*(the\s*)?(bug|issue|error|problem|crash)/i,
|
|
524
|
-
/\bfix(es|ed|ing)?\b/i, // Simple "fix" or "fixed" alone
|
|
525
|
-
/\bbug\s*fix/i,
|
|
526
|
-
/\bBUG:/i, // Bug comment markers
|
|
527
|
-
/\bhotfix\b/i,
|
|
528
|
-
/\bpatch(es|ed|ing)?\b/i,
|
|
529
|
-
/\bresolv(e|es|ed|ing)\s*(the\s*)?(issue|bug|error)/i,
|
|
530
|
-
/\bcorrect(s|ed|ing)?\s*(the\s*)?(bug|issue|error|problem)/i,
|
|
531
|
-
/\brepair(s|ed|ing)?\b/i,
|
|
532
|
-
/\bhandle\s*(error|exception|null|undefined)/i,
|
|
533
|
-
/\bnull\s*check/i,
|
|
534
|
-
/\bundefined\s*check/i,
|
|
535
|
-
/\btry\s*{\s*.*\s*}\s*catch/i,
|
|
536
|
-
/\bif\s*\(\s*!\s*\w+\s*\)/, // Null/undefined guards
|
|
537
|
-
/\bwas\s*broken\b/i, // "was broken" indicates fixing
|
|
538
|
-
/\bbroken\b.*\bfix/i, // broken...fix pattern
|
|
539
|
-
];
|
|
540
|
-
if (fixPatterns.some(p => p.test(diff))) {
|
|
541
|
-
return 'fix';
|
|
542
|
-
}
|
|
543
|
-
// === REFACTOR DETECTION ===
|
|
544
|
-
const refactorPatterns = [
|
|
545
|
-
/\brefactor(s|ed|ing)?\b/i,
|
|
546
|
-
/\brestructur(e|es|ed|ing)\b/i,
|
|
547
|
-
/\bclean\s*up\b/i,
|
|
548
|
-
/\bsimplif(y|ies|ied|ying)\b/i,
|
|
549
|
-
/\brenam(e|es|ed|ing)\b/i,
|
|
550
|
-
/\bmov(e|es|ed|ing)\s*(to|from|into)\b/i,
|
|
551
|
-
/\bextract(s|ed|ing)?\s*(function|method|class|component)/i,
|
|
552
|
-
/\binline(s|d|ing)?\b/i,
|
|
553
|
-
/\bdedup(licate)?\b/i,
|
|
554
|
-
/\bDRY\b/,
|
|
555
|
-
/\breorganiz(e|es|ed|ing)\b/i,
|
|
556
|
-
];
|
|
557
|
-
if (refactorPatterns.some(p => p.test(diff))) {
|
|
558
|
-
return 'refactor';
|
|
559
|
-
}
|
|
560
|
-
// If modifications with balanced adds/removes and no new functionality, likely refactor
|
|
561
|
-
if (hasOnlyModifications && !hasNewFunctionality) {
|
|
562
|
-
// If roughly equal adds and removes, it's likely refactoring
|
|
563
|
-
if (addedLines > 0 && removedLines > 0) {
|
|
564
|
-
const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
|
|
565
|
-
if (ratio > 0.5) { // More strict ratio - must be very balanced
|
|
566
|
-
return 'refactor';
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
// === CHORE DETECTION ===
|
|
571
|
-
const chorePatterns = [
|
|
572
|
-
/\bdependenc(y|ies)\b/i,
|
|
573
|
-
/\bupgrade\b/i,
|
|
574
|
-
/\bupdate\s*(version|dep)/i,
|
|
575
|
-
/\bbump\b/i,
|
|
576
|
-
/\bpackage\.json\b/i,
|
|
577
|
-
/\bpackage-lock\.json\b/i,
|
|
578
|
-
/\byarn\.lock\b/i,
|
|
579
|
-
/\b\.gitignore\b/i,
|
|
580
|
-
/\bci\b.*\b(config|setup)\b/i,
|
|
581
|
-
/\blint(er|ing)?\b/i,
|
|
582
|
-
];
|
|
583
|
-
if (chorePatterns.some(p => p.test(diff)) || chorePatterns.some(p => filePaths.some(f => p.test(f)))) {
|
|
584
|
-
return 'chore';
|
|
585
|
-
}
|
|
586
|
-
// === FALLBACK ===
|
|
587
|
-
// If we have more additions than removals, lean towards feat
|
|
588
|
-
if (filesAffected.source > 0) {
|
|
589
|
-
if (netNewLines > 10) {
|
|
590
|
-
return 'feat'; // Significant new code added
|
|
591
|
-
}
|
|
592
|
-
if (hasOnlyModifications) {
|
|
593
|
-
return 'refactor'; // Modifications without clear new functionality
|
|
594
|
-
}
|
|
595
|
-
return 'feat'; // Default for source changes with new files
|
|
596
|
-
}
|
|
597
|
-
return 'chore';
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Check if diff contains actual logic changes (not just formatting)
|
|
601
|
-
*/
|
|
602
|
-
static hasLogicChanges(diff) {
|
|
603
|
-
// Remove formatting-only changes and check if there's real code
|
|
604
|
-
const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
|
|
605
|
-
!line.startsWith('+++') &&
|
|
606
|
-
!line.startsWith('---'));
|
|
607
|
-
for (const line of lines) {
|
|
608
|
-
const content = line.substring(1).trim();
|
|
609
|
-
// Skip empty lines, comments, and whitespace-only
|
|
610
|
-
if (content.length === 0 ||
|
|
611
|
-
content.startsWith('//') ||
|
|
612
|
-
content.startsWith('/*') ||
|
|
613
|
-
content.startsWith('*') ||
|
|
614
|
-
content === '{' ||
|
|
615
|
-
content === '}' ||
|
|
616
|
-
content === ';') {
|
|
617
|
-
continue;
|
|
618
|
-
}
|
|
619
|
-
// Has actual code change
|
|
620
|
-
return true;
|
|
621
|
-
}
|
|
622
|
-
return false;
|
|
623
|
-
}
|
|
624
|
-
/**
|
|
625
|
-
* Check if the changes are ONLY non-style JavaScript changes
|
|
626
|
-
* (console.log, debug statements, test prints, comments, etc.)
|
|
627
|
-
* Returns true if JS files have changes but NO actual styling changes
|
|
628
|
-
*/
|
|
629
|
-
static hasOnlyNonStyleJsChanges(diff, filePaths) {
|
|
630
|
-
// Check if all files are JavaScript/TypeScript (not CSS)
|
|
631
|
-
const hasOnlyJsFiles = filePaths.every(p => p.endsWith('.js') ||
|
|
632
|
-
p.endsWith('.jsx') ||
|
|
633
|
-
p.endsWith('.ts') ||
|
|
634
|
-
p.endsWith('.tsx') ||
|
|
635
|
-
p.endsWith('.mjs') ||
|
|
636
|
-
p.endsWith('.cjs'));
|
|
637
|
-
if (!hasOnlyJsFiles) {
|
|
638
|
-
return false;
|
|
639
|
-
}
|
|
640
|
-
// Patterns that indicate NON-style changes (debug, logging, test output)
|
|
641
|
-
const nonStylePatterns = [
|
|
642
|
-
/console\.(log|debug|info|warn|error|trace|dir|table)/,
|
|
643
|
-
/debugger\b/,
|
|
644
|
-
/logger\.\w+/i,
|
|
645
|
-
/debug\s*\(/,
|
|
646
|
-
/print\s*\(/,
|
|
647
|
-
/console\.assert/,
|
|
648
|
-
/console\.time/,
|
|
649
|
-
/console\.count/,
|
|
650
|
-
/console\.group/,
|
|
651
|
-
/process\.stdout/,
|
|
652
|
-
/process\.stderr/,
|
|
653
|
-
/\.toLog\(/,
|
|
654
|
-
/\.log\(/,
|
|
655
|
-
/winston\./,
|
|
656
|
-
/pino\./,
|
|
657
|
-
/bunyan\./,
|
|
658
|
-
/log4js\./,
|
|
659
|
-
];
|
|
660
|
-
// Get all added/changed lines
|
|
661
|
-
const changedLines = diff.split('\n').filter(line => line.startsWith('+') && !line.startsWith('+++'));
|
|
662
|
-
// If the changes match non-style patterns, return true
|
|
663
|
-
const hasNonStyleChanges = changedLines.some(line => nonStylePatterns.some(pattern => pattern.test(line)));
|
|
664
|
-
// Check if there are NO style-related patterns in the JS files
|
|
665
|
-
const stylePatterns = [
|
|
666
|
-
/styled\s*[.(]/,
|
|
667
|
-
/css\s*`/,
|
|
668
|
-
/\bstyle\s*=\s*\{\{/,
|
|
669
|
-
/className\s*=/,
|
|
670
|
-
/\bsx\s*=/,
|
|
671
|
-
/theme\./,
|
|
672
|
-
/colors\./,
|
|
673
|
-
/palette\./,
|
|
674
|
-
];
|
|
675
|
-
const hasStylePatterns = changedLines.some(line => stylePatterns.some(pattern => pattern.test(line)));
|
|
676
|
-
// Return true only if we have non-style changes AND no style patterns
|
|
677
|
-
return hasNonStyleChanges && !hasStylePatterns;
|
|
678
|
-
}
|
|
679
|
-
/**
|
|
680
|
-
* Check if the diff contains only comment changes (documentation)
|
|
681
|
-
* Returns true if ALL changes are comments (no code changes)
|
|
682
|
-
*/
|
|
683
|
-
static isCommentOnlyChange(diff) {
|
|
684
|
-
// Get all changed lines (additions and deletions)
|
|
685
|
-
const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
|
|
686
|
-
!line.startsWith('+++') &&
|
|
687
|
-
!line.startsWith('---'));
|
|
688
|
-
if (lines.length === 0) {
|
|
689
|
-
return false;
|
|
690
|
-
}
|
|
691
|
-
let hasCommentChanges = false;
|
|
692
|
-
for (const line of lines) {
|
|
693
|
-
const content = line.substring(1).trim();
|
|
694
|
-
// Skip empty lines
|
|
695
|
-
if (content.length === 0) {
|
|
696
|
-
continue;
|
|
697
|
-
}
|
|
698
|
-
// Check if line is a comment
|
|
699
|
-
const isComment = content.startsWith('//') || // Single-line comment
|
|
700
|
-
content.startsWith('/*') || // Multi-line comment start
|
|
701
|
-
content.startsWith('*') || // Multi-line comment body
|
|
702
|
-
content.startsWith('*/') || // Multi-line comment end
|
|
703
|
-
content.startsWith('#') || // Shell/Python/Ruby comments
|
|
704
|
-
content.startsWith('<!--') || // HTML comments
|
|
705
|
-
content.startsWith('-->') || // HTML comment end
|
|
706
|
-
content.startsWith('"""') || // Python docstring
|
|
707
|
-
content.startsWith("'''") || // Python docstring
|
|
708
|
-
/^\/\*\*/.test(content) || // JSDoc start
|
|
709
|
-
/^\*\s*@\w+/.test(content); // JSDoc tag
|
|
710
|
-
if (isComment) {
|
|
711
|
-
hasCommentChanges = true;
|
|
712
|
-
}
|
|
713
|
-
else {
|
|
714
|
-
// Found a non-comment change - not a comment-only diff
|
|
715
|
-
return false;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
return hasCommentChanges;
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
|
-
* Perform semantic analysis on the diff to understand the nature of changes
|
|
722
|
-
* This provides intent-based understanding rather than line-count metrics
|
|
723
|
-
*/
|
|
724
|
-
static analyzeSemanticChanges(diff, stagedFiles) {
|
|
725
|
-
const roleChanges = this.detectRoleChanges(diff, stagedFiles);
|
|
726
|
-
const primaryRole = this.determinePrimaryRole(roleChanges);
|
|
727
|
-
const primaryIntent = this.determineIntent(diff, stagedFiles, roleChanges);
|
|
728
|
-
const affectedElements = this.extractAffectedElements(diff);
|
|
729
|
-
// Generate human-readable descriptions
|
|
730
|
-
const intentDescription = this.generateIntentDescription(primaryIntent, primaryRole, roleChanges);
|
|
731
|
-
const whatChanged = this.generateWhatChanged(roleChanges, affectedElements, stagedFiles);
|
|
732
|
-
return {
|
|
733
|
-
primaryRole,
|
|
734
|
-
primaryIntent,
|
|
735
|
-
roleChanges,
|
|
736
|
-
intentDescription,
|
|
737
|
-
whatChanged,
|
|
738
|
-
hasMultipleRoles: roleChanges.filter(r => r.significance > 20).length > 1,
|
|
739
|
-
};
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* Detect which roles are affected by the changes and calculate semantic significance
|
|
743
|
-
* Significance is NOT based on line count - it's based on the semantic weight of patterns
|
|
744
|
-
*/
|
|
745
|
-
static detectRoleChanges(diff, _stagedFiles) {
|
|
746
|
-
const roleChanges = [];
|
|
747
|
-
// Check each role for matches
|
|
748
|
-
for (const [role, patterns] of Object.entries(ROLE_PATTERNS)) {
|
|
749
|
-
if (role === 'unknown' || patterns.length === 0)
|
|
750
|
-
continue;
|
|
751
|
-
let matchCount = 0;
|
|
752
|
-
let highValueMatches = 0;
|
|
753
|
-
for (const pattern of patterns) {
|
|
754
|
-
pattern.lastIndex = 0;
|
|
755
|
-
const matches = diff.match(pattern);
|
|
756
|
-
if (matches) {
|
|
757
|
-
matchCount += matches.length;
|
|
758
|
-
// Some patterns indicate more significant changes
|
|
759
|
-
if (this.isHighValuePattern(pattern, role)) {
|
|
760
|
-
highValueMatches += matches.length;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
if (matchCount > 0) {
|
|
765
|
-
// Calculate significance based on pattern matches, not line counts
|
|
766
|
-
// High-value patterns contribute more to significance
|
|
767
|
-
const baseSignificance = Math.min(matchCount * 10, 40);
|
|
768
|
-
const highValueBonus = highValueMatches * 15;
|
|
769
|
-
const significance = Math.min(baseSignificance + highValueBonus, 100);
|
|
770
|
-
const intent = this.detectRoleIntent(diff, role);
|
|
771
|
-
const summary = this.generateRoleSummary(role, intent, matchCount);
|
|
772
|
-
roleChanges.push({
|
|
773
|
-
role,
|
|
774
|
-
intent,
|
|
775
|
-
significance,
|
|
776
|
-
summary,
|
|
777
|
-
affectedElements: this.extractElementsForRole(diff, role),
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
// Sort by significance (highest first)
|
|
782
|
-
return roleChanges.sort((a, b) => b.significance - a.significance);
|
|
783
|
-
}
|
|
784
|
-
/**
|
|
785
|
-
* Determine if a pattern represents a high-value semantic change
|
|
786
|
-
*/
|
|
787
|
-
static isHighValuePattern(pattern, role) {
|
|
788
|
-
const patternStr = pattern.source;
|
|
789
|
-
// High-value patterns for each role
|
|
790
|
-
const highValueIndicators = {
|
|
791
|
-
ui: ['<[A-Z]', 'className', 'onClick', 'onSubmit', 'aria-'],
|
|
792
|
-
logic: ['function', 'class', 'if\\s*\\(', 'switch', 'try\\s*\\{', 'throw'],
|
|
793
|
-
data: ['useState', 'useReducer', 'interface', 'type\\s+\\w+'],
|
|
794
|
-
style: ['styled\\.', 'css`', 'theme\\.'],
|
|
795
|
-
api: ['fetch\\s*\\(', 'axios', 'useQuery', 'useMutation', 'graphql'],
|
|
796
|
-
config: ['process\\.env', 'CONFIG\\.', 'export\\s+(const|let)\\s+[A-Z_]+'],
|
|
797
|
-
test: ['describe\\s*\\(', 'it\\s*\\(', 'test\\s*\\(', 'expect\\s*\\('],
|
|
798
|
-
unknown: [],
|
|
799
|
-
};
|
|
800
|
-
return highValueIndicators[role].some(indicator => patternStr.includes(indicator));
|
|
801
|
-
}
|
|
802
|
-
/**
|
|
803
|
-
* Detect the intent for changes in a specific role
|
|
804
|
-
*/
|
|
805
|
-
static detectRoleIntent(diff, _role) {
|
|
806
|
-
// Check for intent patterns (role context reserved for future use)
|
|
807
|
-
const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
|
|
808
|
-
const removedLines = (diff.match(/^-[^-]/gm) || []).length;
|
|
809
|
-
// Check for add patterns in this role's context
|
|
810
|
-
for (const pattern of INTENT_PATTERNS.add) {
|
|
811
|
-
if (pattern.test(diff)) {
|
|
812
|
-
// If adding new constructs, it's an 'add' intent
|
|
813
|
-
return 'add';
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
// Check for remove patterns
|
|
817
|
-
for (const pattern of INTENT_PATTERNS.remove) {
|
|
818
|
-
if (pattern.test(diff)) {
|
|
819
|
-
return 'remove';
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
// Check for fix patterns
|
|
823
|
-
for (const pattern of INTENT_PATTERNS.fix) {
|
|
824
|
-
if (pattern.test(diff)) {
|
|
825
|
-
return 'fix';
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
// Check for enhance patterns
|
|
829
|
-
for (const pattern of INTENT_PATTERNS.enhance) {
|
|
830
|
-
if (pattern.test(diff)) {
|
|
831
|
-
return 'enhance';
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
// Determine based on add/remove ratio
|
|
835
|
-
if (addedLines > 0 && removedLines === 0) {
|
|
836
|
-
return 'add';
|
|
837
|
-
}
|
|
838
|
-
else if (removedLines > 0 && addedLines === 0) {
|
|
839
|
-
return 'remove';
|
|
840
|
-
}
|
|
841
|
-
else if (addedLines > 0 && removedLines > 0) {
|
|
842
|
-
const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
|
|
843
|
-
if (ratio > 0.5) {
|
|
844
|
-
return 'refactor'; // Balanced changes suggest refactoring
|
|
845
|
-
}
|
|
846
|
-
return addedLines > removedLines ? 'add' : 'modify';
|
|
847
|
-
}
|
|
848
|
-
return 'modify';
|
|
849
|
-
}
|
|
850
|
-
/**
|
|
851
|
-
* Determine the primary role from all detected role changes
|
|
852
|
-
*/
|
|
853
|
-
static determinePrimaryRole(roleChanges) {
|
|
854
|
-
if (roleChanges.length === 0) {
|
|
855
|
-
return 'unknown';
|
|
856
|
-
}
|
|
857
|
-
// The role with highest significance is primary
|
|
858
|
-
// But if multiple roles have similar significance, prefer more specific ones
|
|
859
|
-
const topRole = roleChanges[0];
|
|
860
|
-
// If there's a close second that's more specific, consider it
|
|
861
|
-
if (roleChanges.length > 1) {
|
|
862
|
-
const secondRole = roleChanges[1];
|
|
863
|
-
const significanceDiff = topRole.significance - secondRole.significance;
|
|
864
|
-
// If within 15 points and second is more specific (api > logic > ui)
|
|
865
|
-
if (significanceDiff <= 15) {
|
|
866
|
-
const specificityOrder = ['api', 'data', 'style', 'logic', 'ui', 'config', 'test', 'unknown'];
|
|
867
|
-
const topIndex = specificityOrder.indexOf(topRole.role);
|
|
868
|
-
const secondIndex = specificityOrder.indexOf(secondRole.role);
|
|
869
|
-
if (secondIndex < topIndex) {
|
|
870
|
-
return secondRole.role;
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
return topRole.role;
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* Determine the overall intent of the changes
|
|
878
|
-
*/
|
|
879
|
-
static determineIntent(diff, stagedFiles, roleChanges) {
|
|
880
|
-
// Check file statuses first
|
|
881
|
-
const hasOnlyAdded = stagedFiles.every(f => f.status === 'A');
|
|
882
|
-
const hasOnlyDeleted = stagedFiles.every(f => f.status === 'D');
|
|
883
|
-
const hasOnlyModified = stagedFiles.every(f => f.status === 'M');
|
|
884
|
-
if (hasOnlyAdded) {
|
|
885
|
-
return 'add';
|
|
886
|
-
}
|
|
887
|
-
if (hasOnlyDeleted) {
|
|
888
|
-
return 'remove';
|
|
889
|
-
}
|
|
890
|
-
// If we have role changes, use the most significant role's intent
|
|
891
|
-
if (roleChanges.length > 0) {
|
|
892
|
-
const primaryRoleChange = roleChanges[0];
|
|
893
|
-
// Special case: if the primary intent is 'fix' and we see validation patterns
|
|
894
|
-
// even in non-modified files, treat it as a fix
|
|
895
|
-
if (hasOnlyModified && primaryRoleChange.intent === 'fix') {
|
|
896
|
-
return 'fix';
|
|
897
|
-
}
|
|
898
|
-
// If we're enhancing (useMemo, useCallback, etc.), that takes precedence
|
|
899
|
-
if (roleChanges.some(r => r.intent === 'enhance')) {
|
|
900
|
-
return 'enhance';
|
|
901
|
-
}
|
|
902
|
-
return primaryRoleChange.intent;
|
|
903
|
-
}
|
|
904
|
-
// Fallback to diff analysis
|
|
905
|
-
const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
|
|
906
|
-
const removedLines = (diff.match(/^-[^-]/gm) || []).length;
|
|
907
|
-
if (addedLines > 0 && removedLines === 0) {
|
|
908
|
-
return 'add';
|
|
909
|
-
}
|
|
910
|
-
if (removedLines > 0 && addedLines === 0) {
|
|
911
|
-
return 'remove';
|
|
912
|
-
}
|
|
913
|
-
const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
|
|
914
|
-
if (ratio > 0.6) {
|
|
915
|
-
return 'refactor';
|
|
916
|
-
}
|
|
917
|
-
return 'modify';
|
|
918
|
-
}
|
|
919
|
-
/**
|
|
920
|
-
* Extract affected element names (components, functions, etc.) from the diff
|
|
921
|
-
*/
|
|
922
|
-
static extractAffectedElements(diff) {
|
|
923
|
-
const elements = [];
|
|
924
|
-
// Extract component names (React/JSX)
|
|
925
|
-
const componentMatches = diff.match(/^\+.*(?:function|const)\s+([A-Z][a-zA-Z0-9]*)/gm);
|
|
926
|
-
if (componentMatches) {
|
|
927
|
-
for (const match of componentMatches) {
|
|
928
|
-
const nameMatch = match.match(/(?:function|const)\s+([A-Z][a-zA-Z0-9]*)/);
|
|
929
|
-
if (nameMatch) {
|
|
930
|
-
elements.push(nameMatch[1]);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
// Extract function names
|
|
935
|
-
const functionMatches = diff.match(/^\+.*(?:function|const)\s+([a-z][a-zA-Z0-9]*)\s*[=(]/gm);
|
|
936
|
-
if (functionMatches) {
|
|
937
|
-
for (const match of functionMatches) {
|
|
938
|
-
const nameMatch = match.match(/(?:function|const)\s+([a-z][a-zA-Z0-9]*)/);
|
|
939
|
-
if (nameMatch && !elements.includes(nameMatch[1])) {
|
|
940
|
-
elements.push(nameMatch[1]);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
// Extract interface/type names
|
|
945
|
-
const typeMatches = diff.match(/^\+.*(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/gm);
|
|
946
|
-
if (typeMatches) {
|
|
947
|
-
for (const match of typeMatches) {
|
|
948
|
-
const nameMatch = match.match(/(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/);
|
|
949
|
-
if (nameMatch && !elements.includes(nameMatch[1])) {
|
|
950
|
-
elements.push(nameMatch[1]);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
// Extract class names
|
|
955
|
-
const classMatches = diff.match(/^\+.*class\s+([A-Z][a-zA-Z0-9]*)/gm);
|
|
956
|
-
if (classMatches) {
|
|
957
|
-
for (const match of classMatches) {
|
|
958
|
-
const nameMatch = match.match(/class\s+([A-Z][a-zA-Z0-9]*)/);
|
|
959
|
-
if (nameMatch && !elements.includes(nameMatch[1])) {
|
|
960
|
-
elements.push(nameMatch[1]);
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
return elements.slice(0, 5); // Limit to top 5 elements
|
|
965
|
-
}
|
|
966
|
-
/**
|
|
967
|
-
* Extract affected elements for a specific role
|
|
968
|
-
*/
|
|
969
|
-
static extractElementsForRole(diff, role) {
|
|
970
|
-
const elements = [];
|
|
971
|
-
switch (role) {
|
|
972
|
-
case 'ui':
|
|
973
|
-
// Extract JSX component names being used
|
|
974
|
-
const jsxMatches = diff.match(/^\+.*<([A-Z][a-zA-Z0-9]*)/gm);
|
|
975
|
-
if (jsxMatches) {
|
|
976
|
-
for (const match of jsxMatches) {
|
|
977
|
-
const nameMatch = match.match(/<([A-Z][a-zA-Z0-9]*)/);
|
|
978
|
-
if (nameMatch && !elements.includes(nameMatch[1])) {
|
|
979
|
-
elements.push(nameMatch[1]);
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
break;
|
|
984
|
-
case 'data':
|
|
985
|
-
// Extract state variable names
|
|
986
|
-
const stateMatches = diff.match(/^\+.*const\s+\[([a-z][a-zA-Z0-9]*),/gm);
|
|
987
|
-
if (stateMatches) {
|
|
988
|
-
for (const match of stateMatches) {
|
|
989
|
-
const nameMatch = match.match(/const\s+\[([a-z][a-zA-Z0-9]*)/);
|
|
990
|
-
if (nameMatch) {
|
|
991
|
-
elements.push(nameMatch[1]);
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
break;
|
|
996
|
-
case 'api':
|
|
997
|
-
// Extract API endpoints
|
|
998
|
-
const apiMatches = diff.match(/['"`]\/api\/[^'"`]+['"`]/g);
|
|
999
|
-
if (apiMatches) {
|
|
1000
|
-
elements.push(...apiMatches.map(m => m.replace(/['"`]/g, '')));
|
|
1001
|
-
}
|
|
1002
|
-
break;
|
|
1003
|
-
default:
|
|
1004
|
-
// Use generic extraction
|
|
1005
|
-
return this.extractAffectedElements(diff).slice(0, 3);
|
|
1006
|
-
}
|
|
1007
|
-
return elements.slice(0, 3);
|
|
1008
|
-
}
|
|
1009
|
-
/**
|
|
1010
|
-
* Generate a human-readable summary for a role change
|
|
1011
|
-
*/
|
|
1012
|
-
static generateRoleSummary(role, intent, matchCount) {
|
|
1013
|
-
const roleDesc = ROLE_DESCRIPTIONS[role];
|
|
1014
|
-
const intentVerb = INTENT_VERBS[intent].past;
|
|
1015
|
-
if (matchCount === 1) {
|
|
1016
|
-
return `${intentVerb} ${roleDesc}`;
|
|
1017
|
-
}
|
|
1018
|
-
return `${intentVerb} ${roleDesc} (${matchCount} changes)`;
|
|
1019
|
-
}
|
|
1020
|
-
/**
|
|
1021
|
-
* Generate the WHY description for the commit
|
|
1022
|
-
*/
|
|
1023
|
-
static generateIntentDescription(intent, role, roleChanges) {
|
|
1024
|
-
const roleDesc = ROLE_DESCRIPTIONS[role];
|
|
1025
|
-
switch (intent) {
|
|
1026
|
-
case 'add':
|
|
1027
|
-
return `to add new ${roleDesc}`;
|
|
1028
|
-
case 'fix':
|
|
1029
|
-
return `to fix ${roleDesc} issues`;
|
|
1030
|
-
case 'refactor':
|
|
1031
|
-
return `to improve ${roleDesc} structure`;
|
|
1032
|
-
case 'enhance':
|
|
1033
|
-
return `to optimize ${roleDesc} performance`;
|
|
1034
|
-
case 'remove':
|
|
1035
|
-
return `to remove unused ${roleDesc}`;
|
|
1036
|
-
case 'modify':
|
|
1037
|
-
default:
|
|
1038
|
-
if (roleChanges.length > 1) {
|
|
1039
|
-
return `to update ${roleDesc} and related code`;
|
|
1040
|
-
}
|
|
1041
|
-
return `to update ${roleDesc}`;
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
/**
|
|
1045
|
-
* Generate the WHAT changed description
|
|
1046
|
-
*/
|
|
1047
|
-
static generateWhatChanged(roleChanges, affectedElements, stagedFiles) {
|
|
1048
|
-
// If we have specific elements, use them
|
|
1049
|
-
if (affectedElements.length > 0) {
|
|
1050
|
-
if (affectedElements.length === 1) {
|
|
1051
|
-
return affectedElements[0];
|
|
1052
|
-
}
|
|
1053
|
-
if (affectedElements.length <= 3) {
|
|
1054
|
-
return affectedElements.join(', ');
|
|
1055
|
-
}
|
|
1056
|
-
return `${affectedElements.slice(0, 2).join(', ')} and ${affectedElements.length - 2} more`;
|
|
1057
|
-
}
|
|
1058
|
-
// Fall back to role-based description
|
|
1059
|
-
if (roleChanges.length > 0) {
|
|
1060
|
-
const primaryRole = roleChanges[0];
|
|
1061
|
-
if (primaryRole.affectedElements.length > 0) {
|
|
1062
|
-
return primaryRole.affectedElements[0];
|
|
1063
|
-
}
|
|
1064
|
-
return ROLE_DESCRIPTIONS[primaryRole.role];
|
|
1065
|
-
}
|
|
1066
|
-
// Fall back to file names
|
|
1067
|
-
if (stagedFiles.length === 1) {
|
|
1068
|
-
const parts = stagedFiles[0].path.split('/');
|
|
1069
|
-
return parts[parts.length - 1].replace(/\.\w+$/, '');
|
|
1070
|
-
}
|
|
1071
|
-
return 'code';
|
|
1072
|
-
}
|
|
1073
|
-
/**
|
|
1074
|
-
* Generate a descriptive commit message
|
|
1075
|
-
* Uses semantic analysis when available for intent-based descriptions
|
|
1076
|
-
*/
|
|
1077
|
-
static generateDescription(filesAffected, fileStatuses, stagedFiles, diff, semanticAnalysis) {
|
|
1078
|
-
// Check for comment-only changes (documentation in source files)
|
|
1079
|
-
if (this.isCommentOnlyChange(diff)) {
|
|
1080
|
-
const fileName = stagedFiles.length === 1 ? this.getFileName(stagedFiles[0].path) : null;
|
|
1081
|
-
// Detect type of comments being added
|
|
1082
|
-
const hasJSDoc = /^\+\s*\/\*\*/.test(diff) || /^\+\s*\*\s*@\w+/.test(diff);
|
|
1083
|
-
const hasFunctionComments = /^\+\s*\/\/.*\b(function|method|param|return|arg)/i.test(diff);
|
|
1084
|
-
if (hasJSDoc) {
|
|
1085
|
-
return fileName ? `add JSDoc comments to ${fileName}` : 'add JSDoc documentation';
|
|
1086
|
-
}
|
|
1087
|
-
else if (hasFunctionComments) {
|
|
1088
|
-
return fileName ? `add function documentation to ${fileName}` : 'add function documentation';
|
|
1089
|
-
}
|
|
1090
|
-
else {
|
|
1091
|
-
return fileName ? `add comments to ${fileName}` : 'add code comments';
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
// Use semantic analysis for intent-based descriptions when available
|
|
1095
|
-
if (semanticAnalysis && semanticAnalysis.roleChanges.length > 0) {
|
|
1096
|
-
return this.generateSemanticDescription(semanticAnalysis, stagedFiles);
|
|
1097
|
-
}
|
|
1098
|
-
// Single file changes (fallback)
|
|
1099
|
-
if (stagedFiles.length === 1) {
|
|
1100
|
-
const file = stagedFiles[0];
|
|
1101
|
-
const fileName = this.getFileName(file.path);
|
|
1102
|
-
if (file.status === 'A') {
|
|
1103
|
-
return `add ${fileName}`;
|
|
1104
|
-
}
|
|
1105
|
-
else if (file.status === 'D') {
|
|
1106
|
-
return `remove ${fileName}`;
|
|
1107
|
-
}
|
|
1108
|
-
else if (file.status === 'M') {
|
|
1109
|
-
return `update ${fileName}`;
|
|
1110
|
-
}
|
|
1111
|
-
else if (file.status === 'R') {
|
|
1112
|
-
return `rename ${fileName}`;
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
// Multiple files of the same type
|
|
1116
|
-
if (filesAffected.test > 0 && filesAffected.source === 0) {
|
|
1117
|
-
return `update test files`;
|
|
1118
|
-
}
|
|
1119
|
-
if (filesAffected.docs > 0 && filesAffected.source === 0) {
|
|
1120
|
-
return `update documentation`;
|
|
1121
|
-
}
|
|
1122
|
-
if (filesAffected.config > 0 && filesAffected.source === 0) {
|
|
1123
|
-
return `update configuration`;
|
|
1124
|
-
}
|
|
1125
|
-
// Mixed changes - try to be descriptive
|
|
1126
|
-
const parts = [];
|
|
1127
|
-
if (fileStatuses.added > 0) {
|
|
1128
|
-
parts.push(`add ${fileStatuses.added} file${fileStatuses.added > 1 ? 's' : ''}`);
|
|
1129
|
-
}
|
|
1130
|
-
if (fileStatuses.modified > 0) {
|
|
1131
|
-
if (parts.length === 0) {
|
|
1132
|
-
parts.push(`update ${fileStatuses.modified} file${fileStatuses.modified > 1 ? 's' : ''}`);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
if (fileStatuses.deleted > 0) {
|
|
1136
|
-
parts.push(`remove ${fileStatuses.deleted} file${fileStatuses.deleted > 1 ? 's' : ''}`);
|
|
1137
|
-
}
|
|
1138
|
-
if (parts.length > 0) {
|
|
1139
|
-
return parts.join(' and ');
|
|
1140
|
-
}
|
|
1141
|
-
// Fallback
|
|
1142
|
-
return `update ${stagedFiles.length} file${stagedFiles.length > 1 ? 's' : ''}`;
|
|
1143
|
-
}
|
|
1144
|
-
/**
|
|
1145
|
-
* Generate description based on semantic analysis
|
|
1146
|
-
* Creates intent-based messages like "update UserProfile validation logic"
|
|
1147
|
-
*/
|
|
1148
|
-
static generateSemanticDescription(semantic, stagedFiles) {
|
|
1149
|
-
const intent = semantic.primaryIntent;
|
|
1150
|
-
const role = semantic.primaryRole;
|
|
1151
|
-
const verb = INTENT_VERBS[intent].present;
|
|
1152
|
-
const roleDesc = ROLE_DESCRIPTIONS[role];
|
|
1153
|
-
// If we have specific elements affected, include them
|
|
1154
|
-
const whatChanged = semantic.whatChanged;
|
|
1155
|
-
// For single file with identified elements
|
|
1156
|
-
if (stagedFiles.length === 1 && whatChanged && whatChanged !== 'code') {
|
|
1157
|
-
// Check if whatChanged is a component/function name
|
|
1158
|
-
if (/^[A-Z][a-zA-Z0-9]*$/.test(whatChanged)) {
|
|
1159
|
-
// It's a component name
|
|
1160
|
-
return `${verb} ${whatChanged} ${roleDesc}`;
|
|
1161
|
-
}
|
|
1162
|
-
if (/^[a-z][a-zA-Z0-9]*$/.test(whatChanged)) {
|
|
1163
|
-
// It's a function name
|
|
1164
|
-
return `${verb} ${whatChanged} ${roleDesc}`;
|
|
1165
|
-
}
|
|
1166
|
-
// Multiple elements or descriptive text
|
|
1167
|
-
return `${verb} ${whatChanged}`;
|
|
1168
|
-
}
|
|
1169
|
-
// For multiple files or when we only have role info
|
|
1170
|
-
if (semantic.hasMultipleRoles) {
|
|
1171
|
-
// Changes span multiple concerns
|
|
1172
|
-
const roles = semantic.roleChanges
|
|
1173
|
-
.filter(r => r.significance > 20)
|
|
1174
|
-
.slice(0, 2)
|
|
1175
|
-
.map(r => ROLE_DESCRIPTIONS[r.role]);
|
|
1176
|
-
if (roles.length > 1) {
|
|
1177
|
-
return `${verb} ${roles.join(' and ')}`;
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
// Single role with file context
|
|
1181
|
-
if (stagedFiles.length === 1) {
|
|
1182
|
-
const fileName = this.getFileName(stagedFiles[0].path).replace(/\.\w+$/, '');
|
|
1183
|
-
return `${verb} ${fileName} ${roleDesc}`;
|
|
1184
|
-
}
|
|
1185
|
-
// Multiple files, same role
|
|
1186
|
-
return `${verb} ${roleDesc}`;
|
|
1187
|
-
}
|
|
1188
|
-
/**
|
|
1189
|
-
* Determine scope from file paths
|
|
1190
|
-
*/
|
|
1191
|
-
static determineScope(stagedFiles) {
|
|
1192
|
-
if (stagedFiles.length === 0)
|
|
1193
|
-
return undefined;
|
|
1194
|
-
const config = configService_1.ConfigService.getConfig();
|
|
1195
|
-
const paths = stagedFiles.map((f) => f.path);
|
|
1196
|
-
// Check config-based scope mappings first
|
|
1197
|
-
if (config.scopes && config.scopes.length > 0) {
|
|
1198
|
-
for (const mapping of config.scopes) {
|
|
1199
|
-
const matchingFiles = paths.filter((p) => p.includes(mapping.pattern));
|
|
1200
|
-
if (matchingFiles.length === paths.length) {
|
|
1201
|
-
return mapping.scope;
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
// If most files match a pattern, use that scope
|
|
1205
|
-
for (const mapping of config.scopes) {
|
|
1206
|
-
const matchingFiles = paths.filter((p) => p.includes(mapping.pattern));
|
|
1207
|
-
if (matchingFiles.length > paths.length / 2) {
|
|
1208
|
-
return mapping.scope;
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
// Fallback to default heuristic
|
|
1213
|
-
const firstPath = paths[0];
|
|
1214
|
-
const parts = firstPath.split('/');
|
|
1215
|
-
if (parts.length > 1) {
|
|
1216
|
-
const potentialScope = parts[0];
|
|
1217
|
-
// Common scope names to look for
|
|
1218
|
-
const validScopes = [
|
|
1219
|
-
'api',
|
|
1220
|
-
'ui',
|
|
1221
|
-
'auth',
|
|
1222
|
-
'db',
|
|
1223
|
-
'core',
|
|
1224
|
-
'utils',
|
|
1225
|
-
'components',
|
|
1226
|
-
'services',
|
|
1227
|
-
];
|
|
1228
|
-
if (validScopes.includes(potentialScope.toLowerCase())) {
|
|
1229
|
-
return potentialScope;
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
return undefined;
|
|
1233
|
-
}
|
|
1234
|
-
/**
|
|
1235
|
-
* Extract file name from path
|
|
1236
|
-
*/
|
|
1237
|
-
static getFileName(path) {
|
|
1238
|
-
const parts = path.split('/');
|
|
1239
|
-
return parts[parts.length - 1];
|
|
1240
|
-
}
|
|
1241
|
-
/**
|
|
1242
|
-
* Generate commit body for larger changes
|
|
1243
|
-
* Includes semantic role context when available
|
|
1244
|
-
*/
|
|
1245
|
-
static generateBody(analysis) {
|
|
1246
|
-
const lines = [];
|
|
1247
|
-
const semantic = analysis.semanticAnalysis;
|
|
1248
|
-
// Include semantic context (WHY the change was made) for multi-role changes
|
|
1249
|
-
if (semantic && semantic.hasMultipleRoles) {
|
|
1250
|
-
lines.push('Changes:');
|
|
1251
|
-
for (const roleChange of semantic.roleChanges.filter(r => r.significance > 15).slice(0, 4)) {
|
|
1252
|
-
const elements = roleChange.affectedElements.length > 0
|
|
1253
|
-
? ` (${roleChange.affectedElements.slice(0, 2).join(', ')})`
|
|
1254
|
-
: '';
|
|
1255
|
-
lines.push(`- ${roleChange.summary}${elements}`);
|
|
1256
|
-
}
|
|
1257
|
-
lines.push('');
|
|
1258
|
-
}
|
|
1259
|
-
// Only add file details for truly large changes
|
|
1260
|
-
if (!analysis.isLargeChange && lines.length === 0) {
|
|
1261
|
-
return undefined;
|
|
1262
|
-
}
|
|
1263
|
-
// Add file change details for large changes
|
|
1264
|
-
if (analysis.isLargeChange) {
|
|
1265
|
-
if (lines.length > 0) {
|
|
1266
|
-
lines.push('Files:');
|
|
1267
|
-
}
|
|
1268
|
-
if (analysis.fileChanges.added.length > 0) {
|
|
1269
|
-
lines.push(`- Add ${analysis.fileChanges.added.join(', ')}`);
|
|
1270
|
-
}
|
|
1271
|
-
if (analysis.fileChanges.modified.length > 0) {
|
|
1272
|
-
const files = analysis.fileChanges.modified.slice(0, 5);
|
|
1273
|
-
const suffix = analysis.fileChanges.modified.length > 5
|
|
1274
|
-
? ` and ${analysis.fileChanges.modified.length - 5} more`
|
|
1275
|
-
: '';
|
|
1276
|
-
lines.push(`- Update ${files.join(', ')}${suffix}`);
|
|
1277
|
-
}
|
|
1278
|
-
if (analysis.fileChanges.deleted.length > 0) {
|
|
1279
|
-
lines.push(`- Remove ${analysis.fileChanges.deleted.join(', ')}`);
|
|
1280
|
-
}
|
|
1281
|
-
if (analysis.fileChanges.renamed.length > 0) {
|
|
1282
|
-
lines.push(`- Rename ${analysis.fileChanges.renamed.join(', ')}`);
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
return lines.length > 0 ? lines.join('\n') : undefined;
|
|
1286
|
-
}
|
|
1287
|
-
/**
|
|
1288
|
-
* Apply a template to build the commit message subject line
|
|
1289
|
-
*/
|
|
1290
|
-
static applyTemplate(template, type, scope, description, includeEmoji, isBreaking) {
|
|
1291
|
-
const emoji = includeEmoji ? COMMIT_EMOJIS[type] : '';
|
|
1292
|
-
const breakingIndicator = isBreaking ? '!' : '';
|
|
1293
|
-
let result = template
|
|
1294
|
-
.replace('{emoji}', emoji)
|
|
1295
|
-
.replace('{type}', type + breakingIndicator)
|
|
1296
|
-
.replace('{description}', description);
|
|
1297
|
-
// Handle scope - if no scope, use noScope template or remove scope placeholder
|
|
1298
|
-
if (scope) {
|
|
1299
|
-
result = result.replace('{scope}', scope);
|
|
1300
|
-
}
|
|
1301
|
-
else {
|
|
1302
|
-
// Remove scope and parentheses if no scope
|
|
1303
|
-
result = result.replace('({scope})', '').replace('{scope}', '');
|
|
1304
|
-
}
|
|
1305
|
-
// Clean up extra spaces
|
|
1306
|
-
result = result.replace(/\s+/g, ' ').trim();
|
|
1307
|
-
return result;
|
|
1308
|
-
}
|
|
1309
|
-
/**
|
|
1310
|
-
* Build full commit message string
|
|
1311
|
-
*/
|
|
1312
|
-
static buildFullMessage(type, scope, description, body, includeEmoji, ticketInfo, isBreaking, breakingReasons) {
|
|
1313
|
-
const config = configService_1.ConfigService.getConfig();
|
|
1314
|
-
const includeBreakingFooter = config.breakingChangeDetection?.includeFooter !== false;
|
|
1315
|
-
const templates = config.templates;
|
|
1316
|
-
let full = '';
|
|
1317
|
-
// Use template if available
|
|
1318
|
-
if (templates) {
|
|
1319
|
-
const template = scope
|
|
1320
|
-
? (templates.default || '{emoji} {type}({scope}): {description}')
|
|
1321
|
-
: (templates.noScope || '{emoji} {type}: {description}');
|
|
1322
|
-
full = this.applyTemplate(template, type, scope, description, includeEmoji, isBreaking);
|
|
1323
|
-
}
|
|
1324
|
-
else {
|
|
1325
|
-
// Fallback to original logic
|
|
1326
|
-
if (includeEmoji) {
|
|
1327
|
-
full += `${COMMIT_EMOJIS[type]} `;
|
|
1328
|
-
}
|
|
1329
|
-
full += type;
|
|
1330
|
-
if (scope) {
|
|
1331
|
-
full += `(${scope})`;
|
|
1332
|
-
}
|
|
1333
|
-
// Add breaking change indicator
|
|
1334
|
-
if (isBreaking) {
|
|
1335
|
-
full += '!';
|
|
1336
|
-
}
|
|
1337
|
-
full += `: ${description}`;
|
|
1338
|
-
}
|
|
1339
|
-
if (body) {
|
|
1340
|
-
full += `\n\n${body}`;
|
|
1341
|
-
}
|
|
1342
|
-
// Add BREAKING CHANGE footer if enabled and breaking
|
|
1343
|
-
if (isBreaking && includeBreakingFooter && breakingReasons && breakingReasons.length > 0) {
|
|
1344
|
-
full += '\n\nBREAKING CHANGE: ' + breakingReasons[0];
|
|
1345
|
-
if (breakingReasons.length > 1) {
|
|
1346
|
-
for (let i = 1; i < breakingReasons.length; i++) {
|
|
1347
|
-
full += `\n- ${breakingReasons[i]}`;
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
// Add ticket reference as footer
|
|
1352
|
-
if (ticketInfo) {
|
|
1353
|
-
const prefix = ticketInfo.prefix || 'Refs:';
|
|
1354
|
-
full += `\n\n${prefix} ${ticketInfo.id}`;
|
|
1355
|
-
}
|
|
1356
|
-
return full;
|
|
1357
|
-
}
|
|
1358
|
-
/**
|
|
1359
|
-
* Generate the final commit message
|
|
1360
|
-
*/
|
|
1361
|
-
static generateCommitMessage() {
|
|
1362
|
-
const analysis = this.analyzeChanges();
|
|
1363
|
-
const config = configService_1.ConfigService.getConfig();
|
|
1364
|
-
// Determine emoji usage: config overrides, then history learning
|
|
1365
|
-
let includeEmoji = config.includeEmoji;
|
|
1366
|
-
if (includeEmoji === undefined) {
|
|
1367
|
-
includeEmoji = historyService_1.HistoryService.projectUsesEmojis();
|
|
1368
|
-
}
|
|
1369
|
-
const body = this.generateBody(analysis);
|
|
1370
|
-
const ticketInfo = historyService_1.HistoryService.detectTicketFromBranch();
|
|
1371
|
-
const full = this.buildFullMessage(analysis.commitType, analysis.scope, analysis.description, body, includeEmoji, ticketInfo, analysis.isBreakingChange, analysis.breakingChangeReasons);
|
|
1372
|
-
return {
|
|
1373
|
-
type: analysis.commitType,
|
|
1374
|
-
scope: analysis.scope,
|
|
1375
|
-
description: analysis.description,
|
|
1376
|
-
body,
|
|
1377
|
-
full,
|
|
1378
|
-
isBreaking: analysis.isBreakingChange,
|
|
1379
|
-
};
|
|
1380
|
-
}
|
|
1381
|
-
/**
|
|
1382
|
-
* Generate multiple message suggestions
|
|
1383
|
-
*/
|
|
1384
|
-
static generateMultipleSuggestions() {
|
|
1385
|
-
const analysis = this.analyzeChanges();
|
|
1386
|
-
const config = configService_1.ConfigService.getConfig();
|
|
1387
|
-
// Determine emoji usage: config overrides, then history learning
|
|
1388
|
-
let includeEmoji = config.includeEmoji;
|
|
1389
|
-
if (includeEmoji === undefined) {
|
|
1390
|
-
includeEmoji = historyService_1.HistoryService.projectUsesEmojis();
|
|
1391
|
-
}
|
|
1392
|
-
const suggestions = [];
|
|
1393
|
-
const body = this.generateBody(analysis);
|
|
1394
|
-
const ticketInfo = historyService_1.HistoryService.detectTicketFromBranch();
|
|
1395
|
-
const { isBreakingChange, breakingChangeReasons } = analysis;
|
|
1396
|
-
// Try to get a better scope from history if none detected
|
|
1397
|
-
let scope = analysis.scope;
|
|
1398
|
-
if (!scope) {
|
|
1399
|
-
const stagedFiles = gitService_1.GitService.getStagedFiles();
|
|
1400
|
-
const filePaths = stagedFiles.map(f => f.path);
|
|
1401
|
-
scope = historyService_1.HistoryService.getSuggestedScope(filePaths);
|
|
1402
|
-
}
|
|
1403
|
-
// Suggestion 1: Default (with scope if detected, with ticket, with breaking change)
|
|
1404
|
-
const defaultFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
|
|
1405
|
-
suggestions.push({
|
|
1406
|
-
id: 1,
|
|
1407
|
-
label: isBreakingChange ? 'Breaking Change' : 'Recommended',
|
|
1408
|
-
message: {
|
|
1409
|
-
type: analysis.commitType,
|
|
1410
|
-
scope: scope,
|
|
1411
|
-
description: analysis.description,
|
|
1412
|
-
body,
|
|
1413
|
-
full: defaultFull,
|
|
1414
|
-
isBreaking: isBreakingChange,
|
|
1415
|
-
},
|
|
1416
|
-
});
|
|
1417
|
-
// Suggestion 2: Without scope (more concise)
|
|
1418
|
-
if (scope) {
|
|
1419
|
-
const noScopeFull = this.buildFullMessage(analysis.commitType, undefined, analysis.description, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
|
|
1420
|
-
suggestions.push({
|
|
1421
|
-
id: 2,
|
|
1422
|
-
label: 'Concise',
|
|
1423
|
-
message: {
|
|
1424
|
-
type: analysis.commitType,
|
|
1425
|
-
scope: undefined,
|
|
1426
|
-
description: analysis.description,
|
|
1427
|
-
body,
|
|
1428
|
-
full: noScopeFull,
|
|
1429
|
-
isBreaking: isBreakingChange,
|
|
1430
|
-
},
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
// Suggestion 3: Alternative description style
|
|
1434
|
-
const altDescription = this.generateAlternativeDescription(analysis);
|
|
1435
|
-
if (altDescription && altDescription !== analysis.description) {
|
|
1436
|
-
const altFull = this.buildFullMessage(analysis.commitType, scope, altDescription, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
|
|
1437
|
-
suggestions.push({
|
|
1438
|
-
id: suggestions.length + 1,
|
|
1439
|
-
label: 'Detailed',
|
|
1440
|
-
message: {
|
|
1441
|
-
type: analysis.commitType,
|
|
1442
|
-
scope: scope,
|
|
1443
|
-
description: altDescription,
|
|
1444
|
-
body,
|
|
1445
|
-
full: altFull,
|
|
1446
|
-
isBreaking: isBreakingChange,
|
|
1447
|
-
},
|
|
1448
|
-
});
|
|
1449
|
-
}
|
|
1450
|
-
// Suggestion 4: Without body (compact) - only if body exists
|
|
1451
|
-
if (body) {
|
|
1452
|
-
const compactFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, undefined, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
|
|
1453
|
-
suggestions.push({
|
|
1454
|
-
id: suggestions.length + 1,
|
|
1455
|
-
label: 'Compact',
|
|
1456
|
-
message: {
|
|
1457
|
-
type: analysis.commitType,
|
|
1458
|
-
scope: scope,
|
|
1459
|
-
description: analysis.description,
|
|
1460
|
-
body: undefined,
|
|
1461
|
-
full: compactFull,
|
|
1462
|
-
isBreaking: isBreakingChange,
|
|
1463
|
-
},
|
|
1464
|
-
});
|
|
1465
|
-
}
|
|
1466
|
-
// Suggestion 5: Without ticket reference (if ticket was detected)
|
|
1467
|
-
if (ticketInfo) {
|
|
1468
|
-
const noTicketFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, body, includeEmoji, null, isBreakingChange, breakingChangeReasons);
|
|
1469
|
-
suggestions.push({
|
|
1470
|
-
id: suggestions.length + 1,
|
|
1471
|
-
label: 'No Ticket',
|
|
1472
|
-
message: {
|
|
1473
|
-
type: analysis.commitType,
|
|
1474
|
-
scope: scope,
|
|
1475
|
-
description: analysis.description,
|
|
1476
|
-
body,
|
|
1477
|
-
full: noTicketFull,
|
|
1478
|
-
isBreaking: isBreakingChange,
|
|
1479
|
-
},
|
|
1480
|
-
});
|
|
1481
|
-
}
|
|
1482
|
-
// Suggestion 6: Without breaking change indicator (if breaking change detected)
|
|
1483
|
-
if (isBreakingChange) {
|
|
1484
|
-
const noBreakingFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, body, includeEmoji, ticketInfo, false, []);
|
|
1485
|
-
suggestions.push({
|
|
1486
|
-
id: suggestions.length + 1,
|
|
1487
|
-
label: 'No Breaking Flag',
|
|
1488
|
-
message: {
|
|
1489
|
-
type: analysis.commitType,
|
|
1490
|
-
scope: scope,
|
|
1491
|
-
description: analysis.description,
|
|
1492
|
-
body,
|
|
1493
|
-
full: noBreakingFull,
|
|
1494
|
-
isBreaking: false,
|
|
1495
|
-
},
|
|
1496
|
-
});
|
|
1497
|
-
}
|
|
1498
|
-
return suggestions;
|
|
1499
|
-
}
|
|
1500
|
-
/**
|
|
1501
|
-
* Generate suggestions with optional AI enhancement
|
|
1502
|
-
*/
|
|
1503
|
-
static async generateSuggestionsWithAI(useAI = false) {
|
|
1504
|
-
const suggestions = this.generateMultipleSuggestions();
|
|
1505
|
-
// If AI is not requested or not enabled, return regular suggestions
|
|
1506
|
-
if (!useAI || !aiService_1.AIService.isEnabled()) {
|
|
1507
|
-
return suggestions;
|
|
1508
|
-
}
|
|
1509
|
-
try {
|
|
1510
|
-
const analysis = this.analyzeChanges();
|
|
1511
|
-
const diff = gitService_1.GitService.getDiff();
|
|
1512
|
-
const config = configService_1.ConfigService.getConfig();
|
|
1513
|
-
// Determine emoji usage
|
|
1514
|
-
let includeEmoji = config.includeEmoji;
|
|
1515
|
-
if (includeEmoji === undefined) {
|
|
1516
|
-
includeEmoji = historyService_1.HistoryService.projectUsesEmojis();
|
|
1517
|
-
}
|
|
1518
|
-
const body = this.generateBody(analysis);
|
|
1519
|
-
const ticketInfo = historyService_1.HistoryService.detectTicketFromBranch();
|
|
1520
|
-
const { isBreakingChange, breakingChangeReasons } = analysis;
|
|
1521
|
-
// Get scope
|
|
1522
|
-
let scope = analysis.scope;
|
|
1523
|
-
if (!scope) {
|
|
1524
|
-
const stagedFiles = gitService_1.GitService.getStagedFiles();
|
|
1525
|
-
const filePaths = stagedFiles.map(f => f.path);
|
|
1526
|
-
scope = historyService_1.HistoryService.getSuggestedScope(filePaths);
|
|
1527
|
-
}
|
|
1528
|
-
// Get AI-enhanced description
|
|
1529
|
-
const aiResponse = await aiService_1.AIService.generateDescription(analysis, diff);
|
|
1530
|
-
if (aiResponse && aiResponse.description) {
|
|
1531
|
-
const aiDescription = aiResponse.description;
|
|
1532
|
-
const aiFull = this.buildFullMessage(analysis.commitType, scope, aiDescription, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
|
|
1533
|
-
// Insert AI suggestion at the beginning
|
|
1534
|
-
suggestions.unshift({
|
|
1535
|
-
id: 0,
|
|
1536
|
-
label: 'AI Enhanced',
|
|
1537
|
-
message: {
|
|
1538
|
-
type: analysis.commitType,
|
|
1539
|
-
scope: scope,
|
|
1540
|
-
description: aiDescription,
|
|
1541
|
-
body,
|
|
1542
|
-
full: aiFull,
|
|
1543
|
-
isBreaking: isBreakingChange,
|
|
1544
|
-
},
|
|
1545
|
-
});
|
|
1546
|
-
// Re-number suggestions
|
|
1547
|
-
suggestions.forEach((s, i) => {
|
|
1548
|
-
s.id = i + 1;
|
|
1549
|
-
});
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
catch (error) {
|
|
1553
|
-
// AI failed - error already logged by AIService
|
|
1554
|
-
console.log('Falling back to rule-based suggestions.\n');
|
|
1555
|
-
}
|
|
1556
|
-
return suggestions;
|
|
1557
|
-
}
|
|
1558
|
-
/**
|
|
1559
|
-
* Generate an alternative description style
|
|
1560
|
-
*/
|
|
1561
|
-
static generateAlternativeDescription(analysis) {
|
|
1562
|
-
const { fileChanges, filesAffected } = analysis;
|
|
1563
|
-
const totalFiles = fileChanges.added.length + fileChanges.modified.length +
|
|
1564
|
-
fileChanges.deleted.length + fileChanges.renamed.length;
|
|
1565
|
-
// For single file, provide more detail
|
|
1566
|
-
if (totalFiles === 1) {
|
|
1567
|
-
if (fileChanges.added.length === 1) {
|
|
1568
|
-
return `implement ${fileChanges.added[0]}`;
|
|
1569
|
-
}
|
|
1570
|
-
if (fileChanges.modified.length === 1) {
|
|
1571
|
-
return `improve ${fileChanges.modified[0]}`;
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
// For multiple files, be more descriptive about categories
|
|
1575
|
-
const parts = [];
|
|
1576
|
-
if (filesAffected.source > 0) {
|
|
1577
|
-
parts.push(`${filesAffected.source} source file${filesAffected.source > 1 ? 's' : ''}`);
|
|
1578
|
-
}
|
|
1579
|
-
if (filesAffected.test > 0) {
|
|
1580
|
-
parts.push(`${filesAffected.test} test${filesAffected.test > 1 ? 's' : ''}`);
|
|
1581
|
-
}
|
|
1582
|
-
if (filesAffected.docs > 0) {
|
|
1583
|
-
parts.push(`${filesAffected.docs} doc${filesAffected.docs > 1 ? 's' : ''}`);
|
|
1584
|
-
}
|
|
1585
|
-
if (filesAffected.config > 0) {
|
|
1586
|
-
parts.push(`${filesAffected.config} config${filesAffected.config > 1 ? 's' : ''}`);
|
|
1587
|
-
}
|
|
1588
|
-
if (parts.length > 0) {
|
|
1589
|
-
return `update ${parts.join(', ')}`;
|
|
1590
|
-
}
|
|
1591
|
-
return analysis.description;
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
exports.AnalyzerService = AnalyzerService;
|
|
1595
|
-
//# sourceMappingURL=analyzerService.js.map
|