@m3hti/commit-genie 3.1.0 → 3.1.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/dist/services/analyzerService.d.ts +0 -157
- package/dist/services/analyzerService.d.ts.map +1 -1
- package/dist/services/analyzerService.js +23 -2001
- package/dist/services/analyzerService.js.map +1 -1
- package/dist/services/breakingChangeDetector.d.ts +9 -0
- package/dist/services/breakingChangeDetector.d.ts.map +1 -0
- package/dist/services/breakingChangeDetector.js +76 -0
- package/dist/services/breakingChangeDetector.js.map +1 -0
- package/dist/services/commitTypeDetector.d.ts +32 -0
- package/dist/services/commitTypeDetector.d.ts.map +1 -0
- package/dist/services/commitTypeDetector.js +490 -0
- package/dist/services/commitTypeDetector.js.map +1 -0
- package/dist/services/descriptionGenerator.d.ts +58 -0
- package/dist/services/descriptionGenerator.d.ts.map +1 -0
- package/dist/services/descriptionGenerator.js +569 -0
- package/dist/services/descriptionGenerator.js.map +1 -0
- package/dist/services/gitService.test.js +242 -24
- package/dist/services/gitService.test.js.map +1 -1
- package/dist/services/hookService.test.d.ts +2 -0
- package/dist/services/hookService.test.d.ts.map +1 -0
- package/dist/services/hookService.test.js +182 -0
- package/dist/services/hookService.test.js.map +1 -0
- package/dist/services/lintService.test.d.ts +2 -0
- package/dist/services/lintService.test.d.ts.map +1 -0
- package/dist/services/lintService.test.js +288 -0
- package/dist/services/lintService.test.js.map +1 -0
- package/dist/services/messageBuilder.d.ts +16 -0
- package/dist/services/messageBuilder.d.ts.map +1 -0
- package/dist/services/messageBuilder.js +135 -0
- package/dist/services/messageBuilder.js.map +1 -0
- package/dist/services/scopeDetector.d.ts +6 -0
- package/dist/services/scopeDetector.d.ts.map +1 -0
- package/dist/services/scopeDetector.js +51 -0
- package/dist/services/scopeDetector.js.map +1 -0
- package/dist/services/semanticAnalyzer.d.ts +25 -0
- package/dist/services/semanticAnalyzer.d.ts.map +1 -0
- package/dist/services/semanticAnalyzer.js +713 -0
- package/dist/services/semanticAnalyzer.js.map +1 -0
- package/dist/services/splitService.test.d.ts +2 -0
- package/dist/services/splitService.test.d.ts.map +1 -0
- package/dist/services/splitService.test.js +190 -0
- package/dist/services/splitService.test.js.map +1 -0
- package/dist/services/statsService.test.d.ts +2 -0
- package/dist/services/statsService.test.d.ts.map +1 -0
- package/dist/services/statsService.test.js +211 -0
- package/dist/services/statsService.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.INTENT_VERBS = exports.ROLE_DESCRIPTIONS = void 0;
|
|
4
|
+
exports.analyzeSemanticChanges = analyzeSemanticChanges;
|
|
5
|
+
exports.extractAffectedElements = extractAffectedElements;
|
|
6
|
+
exports.extractHunkContext = extractHunkContext;
|
|
7
|
+
// Role detection patterns for semantic analysis
|
|
8
|
+
const ROLE_PATTERNS = {
|
|
9
|
+
ui: [
|
|
10
|
+
// JSX elements and components
|
|
11
|
+
/^\+.*<[A-Z][a-zA-Z]*[\s/>]/m, // JSX component usage
|
|
12
|
+
/^\+.*<\/[A-Z][a-zA-Z]*>/m, // JSX component closing
|
|
13
|
+
/^\+.*<(div|span|p|h[1-6]|button|input|form|ul|li|table|img|a|section|header|footer|nav|main|article|aside)\b/mi,
|
|
14
|
+
/^\+.*className\s*=/m, // className attribute
|
|
15
|
+
/^\+.*style\s*=\s*\{/m, // inline styles
|
|
16
|
+
/^\+.*return\s*\(/m, // render return
|
|
17
|
+
/^\+.*render\s*\(\s*\)/m, // render method
|
|
18
|
+
/^\+.*(?:<>|<\/>)/m, // React fragments
|
|
19
|
+
/^\+.*aria-\w+=/m, // accessibility attributes
|
|
20
|
+
/^\+.*role\s*=/m, // role attribute
|
|
21
|
+
/^\+.*\b(?:onClick|onSubmit|onChange|onFocus|onBlur|onKeyDown|onKeyUp|onMouseEnter|onMouseLeave)\b/m,
|
|
22
|
+
],
|
|
23
|
+
logic: [
|
|
24
|
+
// Business logic patterns
|
|
25
|
+
/^\+.*\bif\s*\(/m, // conditionals
|
|
26
|
+
/^\+.*\bswitch\s*\(/m, // switch statements
|
|
27
|
+
/^\+.*\bfor\s*\(/m, // for loops
|
|
28
|
+
/^\+.*\bwhile\s*\(/m, // while loops
|
|
29
|
+
/^\+.*\.map\s*\(/m, // array operations
|
|
30
|
+
/^\+.*\.filter\s*\(/m,
|
|
31
|
+
/^\+.*\.reduce\s*\(/m,
|
|
32
|
+
/^\+.*\.find\s*\(/m,
|
|
33
|
+
/^\+.*\.some\s*\(/m,
|
|
34
|
+
/^\+.*\.every\s*\(/m,
|
|
35
|
+
/^\+.*\btry\s*\{/m, // error handling
|
|
36
|
+
/^\+.*\bcatch\s*\(/m,
|
|
37
|
+
/^\+.*\bthrow\s+/m,
|
|
38
|
+
/^\+.*\breturn\s+(?![\s(]?<)/m, // return (not JSX)
|
|
39
|
+
/^\+.*\bawait\s+/m, // async operations
|
|
40
|
+
/^\+.*\bnew\s+[A-Z]/m, // object instantiation
|
|
41
|
+
/^\+.*=>\s*\{/m, // arrow function body
|
|
42
|
+
],
|
|
43
|
+
data: [
|
|
44
|
+
// State and data management
|
|
45
|
+
/^\+.*\buseState\s*[<(]/m, // React state
|
|
46
|
+
/^\+.*\buseReducer\s*\(/m, // React reducer
|
|
47
|
+
/^\+.*\buseContext\s*\(/m, // React context
|
|
48
|
+
/^\+.*\buseSelector\s*\(/m, // Redux selector
|
|
49
|
+
/^\+.*\bdispatch\s*\(/m, // Redux dispatch
|
|
50
|
+
/^\+.*\bsetState\s*\(/m, // Class component state
|
|
51
|
+
/^\+.*\bthis\.state\b/m, // Class component state access
|
|
52
|
+
/^\+.*\bprops\./m, // Props access
|
|
53
|
+
/^\+.*interface\s+\w+Props/m, // Props interface
|
|
54
|
+
/^\+.*type\s+\w+Props/m, // Props type
|
|
55
|
+
/^\+.*\bconst\s+\[[a-z]+,\s*set[A-Z]/m, // State destructuring
|
|
56
|
+
/^\+.*:\s*\w+\[\]/m, // Array type
|
|
57
|
+
/^\+.*:\s*(string|number|boolean|object)/m, // Primitive types
|
|
58
|
+
/^\+.*\binterface\s+\w+/m, // Interface definition
|
|
59
|
+
/^\+.*\btype\s+\w+\s*=/m, // Type definition
|
|
60
|
+
],
|
|
61
|
+
style: [
|
|
62
|
+
// CSS and styling
|
|
63
|
+
/^\+.*\bstyles?\./m, // styles object access
|
|
64
|
+
/^\+.*\bstyled\./m, // styled-components
|
|
65
|
+
/^\+.*\bcss`/m, // CSS template literal
|
|
66
|
+
/^\+.*\bsx\s*=\s*\{/m, // MUI sx prop
|
|
67
|
+
/^\+.*:\s*['"]?[0-9]+(px|em|rem|%|vh|vw)/m, // CSS units
|
|
68
|
+
/^\+.*:\s*['"]?#[0-9a-fA-F]{3,8}/m, // Hex colors
|
|
69
|
+
/^\+.*:\s*['"]?rgb(a)?\s*\(/m, // RGB colors
|
|
70
|
+
/^\+.*(margin|padding|width|height|border|background|color|font|display|flex|grid)\s*:/m,
|
|
71
|
+
/^\+.*\btheme\./m, // Theme access
|
|
72
|
+
/^\+.*\.module\.css/m, // CSS modules import
|
|
73
|
+
],
|
|
74
|
+
api: [
|
|
75
|
+
// API and network calls
|
|
76
|
+
/^\+.*\bfetch\s*\(/m, // fetch API
|
|
77
|
+
/^\+.*\baxios\./m, // axios
|
|
78
|
+
/^\+.*\.get\s*\(/m, // HTTP GET
|
|
79
|
+
/^\+.*\.post\s*\(/m, // HTTP POST
|
|
80
|
+
/^\+.*\.put\s*\(/m, // HTTP PUT
|
|
81
|
+
/^\+.*\.delete\s*\(/m, // HTTP DELETE
|
|
82
|
+
/^\+.*\.patch\s*\(/m, // HTTP PATCH
|
|
83
|
+
/^\+.*\bapi\./m, // api object access
|
|
84
|
+
/^\+.*\/api\//m, // API path
|
|
85
|
+
/^\+.*\bendpoint/mi, // endpoint reference
|
|
86
|
+
/^\+.*\bheaders\s*:/m, // HTTP headers
|
|
87
|
+
/^\+.*\bAuthorization:/m, // Auth header
|
|
88
|
+
/^\+.*\buseQuery\s*\(/m, // React Query
|
|
89
|
+
/^\+.*\buseMutation\s*\(/m, // React Query mutation
|
|
90
|
+
/^\+.*\bswr\b/mi, // SWR
|
|
91
|
+
/^\+.*\bgraphql`/m, // GraphQL
|
|
92
|
+
/^\+.*\bquery\s*\{/m, // GraphQL query
|
|
93
|
+
/^\+.*\bmutation\s*\{/m, // GraphQL mutation
|
|
94
|
+
],
|
|
95
|
+
config: [
|
|
96
|
+
// Configuration
|
|
97
|
+
/^\+.*\bprocess\.env\./m, // Environment variables
|
|
98
|
+
/^\+.*\bimport\.meta\.env\./m, // Vite env
|
|
99
|
+
/^\+.*\bCONFIG\./m, // Config constant
|
|
100
|
+
/^\+.*\bsettings\./m, // Settings object
|
|
101
|
+
/^\+.*\boptions\s*:/m, // Options object
|
|
102
|
+
/^\+.*\bdefaultProps/m, // Default props
|
|
103
|
+
/^\+.*\bexport\s+(const|let)\s+[A-Z_]+\s*=/m, // Constant export
|
|
104
|
+
/^\+.*:\s*['"]?(development|production|test)['"]/m, // Environment strings
|
|
105
|
+
],
|
|
106
|
+
test: [
|
|
107
|
+
// Testing patterns
|
|
108
|
+
/^\+.*\bdescribe\s*\(/m, // Test suite
|
|
109
|
+
/^\+.*\bit\s*\(/m, // Test case
|
|
110
|
+
/^\+.*\btest\s*\(/m, // Test case
|
|
111
|
+
/^\+.*\bexpect\s*\(/m, // Assertion
|
|
112
|
+
/^\+.*\bjest\./m, // Jest
|
|
113
|
+
/^\+.*\bmock\(/m, // Mocking
|
|
114
|
+
/^\+.*\bspyOn\s*\(/m, // Spy
|
|
115
|
+
/^\+.*\bbeforeEach\s*\(/m, // Setup
|
|
116
|
+
/^\+.*\bafterEach\s*\(/m, // Teardown
|
|
117
|
+
/^\+.*\brender\s*\(/m, // React testing library
|
|
118
|
+
],
|
|
119
|
+
unknown: [],
|
|
120
|
+
};
|
|
121
|
+
// Intent detection patterns
|
|
122
|
+
const INTENT_PATTERNS = {
|
|
123
|
+
add: [
|
|
124
|
+
/^\+\s*export\s+(function|class|const|interface|type)/m,
|
|
125
|
+
/^\+\s*(async\s+)?function\s+\w+/m,
|
|
126
|
+
/^\+\s*const\s+\w+\s*=\s*(async\s+)?\(/m,
|
|
127
|
+
/^\+\s*class\s+\w+/m,
|
|
128
|
+
],
|
|
129
|
+
modify: [
|
|
130
|
+
// Changes that have both additions and deletions of similar patterns
|
|
131
|
+
],
|
|
132
|
+
fix: [
|
|
133
|
+
/^\+.*\btypeof\s+\w+\s*[!=]==?\s*['"`]/m,
|
|
134
|
+
/^\+.*\binstanceof\s+/m,
|
|
135
|
+
/^\+.*\bArray\.isArray\s*\(/m,
|
|
136
|
+
/^\+.*\bif\s*\(\s*!\w+\s*\)/m,
|
|
137
|
+
/^\+.*\?\?/m,
|
|
138
|
+
/^\+.*\?\./m,
|
|
139
|
+
/^\+.*\|\|\s*['"{\[0]/m, // Default values
|
|
140
|
+
/^\+.*\bcatch\s*\(/m,
|
|
141
|
+
/^\+.*\btry\s*\{/m,
|
|
142
|
+
],
|
|
143
|
+
remove: [
|
|
144
|
+
/^-\s*export\s+(function|class|const|interface|type)/m,
|
|
145
|
+
/^-\s*(async\s+)?function\s+\w+/m,
|
|
146
|
+
/^-\s*class\s+\w+/m,
|
|
147
|
+
],
|
|
148
|
+
refactor: [
|
|
149
|
+
/^\+.*=>/m, // Arrow function conversion
|
|
150
|
+
/^\+.*\.\.\./m, // Spread operator
|
|
151
|
+
/^\+.*`\$\{/m, // Template literal
|
|
152
|
+
/^\+.*Object\.(keys|values|entries)/m, // Object methods
|
|
153
|
+
],
|
|
154
|
+
enhance: [
|
|
155
|
+
/^\+.*\bmemo\s*\(/m, // React memo
|
|
156
|
+
/^\+.*\buseMemo\s*\(/m, // useMemo hook
|
|
157
|
+
/^\+.*\buseCallback\s*\(/m, // useCallback hook
|
|
158
|
+
/^\+.*\blazy\s*\(/m, // React lazy
|
|
159
|
+
/^\+.*\bSuspense\b/m, // React Suspense
|
|
160
|
+
],
|
|
161
|
+
rename: [
|
|
162
|
+
// Rename detection is handled by detectRenames() method
|
|
163
|
+
// which compares removed and added function/class/type names
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
// Role descriptions for commit messages
|
|
167
|
+
exports.ROLE_DESCRIPTIONS = {
|
|
168
|
+
ui: 'UI/rendering',
|
|
169
|
+
logic: 'business logic',
|
|
170
|
+
data: 'data/state management',
|
|
171
|
+
style: 'styling',
|
|
172
|
+
api: 'API integration',
|
|
173
|
+
config: 'configuration',
|
|
174
|
+
test: 'tests',
|
|
175
|
+
unknown: 'code',
|
|
176
|
+
};
|
|
177
|
+
// Intent verb mappings for commit messages
|
|
178
|
+
exports.INTENT_VERBS = {
|
|
179
|
+
add: { past: 'added', present: 'add' },
|
|
180
|
+
modify: { past: 'updated', present: 'update' },
|
|
181
|
+
fix: { past: 'fixed', present: 'fix' },
|
|
182
|
+
remove: { past: 'removed', present: 'remove' },
|
|
183
|
+
refactor: { past: 'refactored', present: 'refactor' },
|
|
184
|
+
enhance: { past: 'enhanced', present: 'improve' },
|
|
185
|
+
rename: { past: 'renamed', present: 'rename' },
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Perform semantic analysis on the diff to understand the nature of changes
|
|
189
|
+
* This provides intent-based understanding rather than line-count metrics
|
|
190
|
+
*/
|
|
191
|
+
function analyzeSemanticChanges(diff, stagedFiles) {
|
|
192
|
+
const roleChanges = detectRoleChanges(diff, stagedFiles);
|
|
193
|
+
const primaryRole = determinePrimaryRole(roleChanges);
|
|
194
|
+
let primaryIntent = determineIntent(diff, stagedFiles, roleChanges);
|
|
195
|
+
const affectedElements = extractAffectedElements(diff);
|
|
196
|
+
const hunkContext = extractHunkContext(diff);
|
|
197
|
+
// Detect renames - this takes priority for determining intent
|
|
198
|
+
const renames = detectRenames(diff);
|
|
199
|
+
if (renames.length > 0) {
|
|
200
|
+
primaryIntent = 'rename';
|
|
201
|
+
}
|
|
202
|
+
// Generate human-readable descriptions
|
|
203
|
+
const intentDescription = generateIntentDescription(primaryIntent, primaryRole, roleChanges);
|
|
204
|
+
const whatChanged = renames.length > 0
|
|
205
|
+
? `${renames[0].oldName} to ${renames[0].newName}`
|
|
206
|
+
: generateWhatChanged(roleChanges, affectedElements, stagedFiles, hunkContext);
|
|
207
|
+
return {
|
|
208
|
+
primaryRole,
|
|
209
|
+
primaryIntent,
|
|
210
|
+
roleChanges,
|
|
211
|
+
intentDescription,
|
|
212
|
+
whatChanged,
|
|
213
|
+
hasMultipleRoles: roleChanges.filter(r => r.significance > 20).length > 1,
|
|
214
|
+
renames: renames.length > 0 ? renames : undefined,
|
|
215
|
+
hunkContext: hunkContext.length > 0 ? hunkContext : undefined,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Detect which roles are affected by the changes and calculate semantic significance
|
|
220
|
+
* Significance is NOT based on line count - it's based on the semantic weight of patterns
|
|
221
|
+
*/
|
|
222
|
+
function detectRoleChanges(diff, _stagedFiles) {
|
|
223
|
+
const roleChanges = [];
|
|
224
|
+
// Check each role for matches
|
|
225
|
+
for (const [role, patterns] of Object.entries(ROLE_PATTERNS)) {
|
|
226
|
+
if (role === 'unknown' || patterns.length === 0)
|
|
227
|
+
continue;
|
|
228
|
+
let matchCount = 0;
|
|
229
|
+
let highValueMatches = 0;
|
|
230
|
+
for (const pattern of patterns) {
|
|
231
|
+
pattern.lastIndex = 0;
|
|
232
|
+
const matches = diff.match(pattern);
|
|
233
|
+
if (matches) {
|
|
234
|
+
matchCount += matches.length;
|
|
235
|
+
// Some patterns indicate more significant changes
|
|
236
|
+
if (isHighValuePattern(pattern, role)) {
|
|
237
|
+
highValueMatches += matches.length;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (matchCount > 0) {
|
|
242
|
+
// Calculate significance based on pattern matches, not line counts
|
|
243
|
+
// High-value patterns contribute more to significance
|
|
244
|
+
const baseSignificance = Math.min(matchCount * 10, 40);
|
|
245
|
+
const highValueBonus = highValueMatches * 15;
|
|
246
|
+
const significance = Math.min(baseSignificance + highValueBonus, 100);
|
|
247
|
+
const intent = detectRoleIntent(diff, role);
|
|
248
|
+
const summary = generateRoleSummary(role, intent, matchCount);
|
|
249
|
+
roleChanges.push({
|
|
250
|
+
role,
|
|
251
|
+
intent,
|
|
252
|
+
significance,
|
|
253
|
+
summary,
|
|
254
|
+
affectedElements: extractElementsForRole(diff, role),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Sort by significance (highest first)
|
|
259
|
+
return roleChanges.sort((a, b) => b.significance - a.significance);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Determine if a pattern represents a high-value semantic change
|
|
263
|
+
*/
|
|
264
|
+
function isHighValuePattern(pattern, role) {
|
|
265
|
+
const patternStr = pattern.source;
|
|
266
|
+
// High-value patterns for each role
|
|
267
|
+
const highValueIndicators = {
|
|
268
|
+
ui: ['<[A-Z]', 'className', 'onClick', 'onSubmit', 'aria-'],
|
|
269
|
+
logic: ['function', 'class', 'if\\s*\\(', 'switch', 'try\\s*\\{', 'throw'],
|
|
270
|
+
data: ['useState', 'useReducer', 'interface', 'type\\s+\\w+'],
|
|
271
|
+
style: ['styled\\.', 'css`', 'theme\\.'],
|
|
272
|
+
api: ['fetch\\s*\\(', 'axios', 'useQuery', 'useMutation', 'graphql'],
|
|
273
|
+
config: ['process\\.env', 'CONFIG\\.', 'export\\s+(const|let)\\s+[A-Z_]+'],
|
|
274
|
+
test: ['describe\\s*\\(', 'it\\s*\\(', 'test\\s*\\(', 'expect\\s*\\('],
|
|
275
|
+
unknown: [],
|
|
276
|
+
};
|
|
277
|
+
return highValueIndicators[role].some(indicator => patternStr.includes(indicator));
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Detect the intent for changes in a specific role
|
|
281
|
+
*/
|
|
282
|
+
function detectRoleIntent(diff, _role) {
|
|
283
|
+
// Check for intent patterns (role context reserved for future use)
|
|
284
|
+
const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
|
|
285
|
+
const removedLines = (diff.match(/^-[^-]/gm) || []).length;
|
|
286
|
+
// Check for add patterns in this role's context
|
|
287
|
+
for (const pattern of INTENT_PATTERNS.add) {
|
|
288
|
+
if (pattern.test(diff)) {
|
|
289
|
+
// If adding new constructs, it's an 'add' intent
|
|
290
|
+
return 'add';
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Check for remove patterns
|
|
294
|
+
for (const pattern of INTENT_PATTERNS.remove) {
|
|
295
|
+
if (pattern.test(diff)) {
|
|
296
|
+
return 'remove';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Check for fix patterns
|
|
300
|
+
for (const pattern of INTENT_PATTERNS.fix) {
|
|
301
|
+
if (pattern.test(diff)) {
|
|
302
|
+
return 'fix';
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Check for enhance patterns
|
|
306
|
+
for (const pattern of INTENT_PATTERNS.enhance) {
|
|
307
|
+
if (pattern.test(diff)) {
|
|
308
|
+
return 'enhance';
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Determine based on add/remove ratio
|
|
312
|
+
if (addedLines > 0 && removedLines === 0) {
|
|
313
|
+
return 'add';
|
|
314
|
+
}
|
|
315
|
+
else if (removedLines > 0 && addedLines === 0) {
|
|
316
|
+
return 'remove';
|
|
317
|
+
}
|
|
318
|
+
else if (addedLines > 0 && removedLines > 0) {
|
|
319
|
+
const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
|
|
320
|
+
if (ratio > 0.5) {
|
|
321
|
+
return 'refactor'; // Balanced changes suggest refactoring
|
|
322
|
+
}
|
|
323
|
+
return addedLines > removedLines ? 'add' : 'modify';
|
|
324
|
+
}
|
|
325
|
+
return 'modify';
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Determine the primary role from all detected role changes
|
|
329
|
+
*/
|
|
330
|
+
function determinePrimaryRole(roleChanges) {
|
|
331
|
+
if (roleChanges.length === 0) {
|
|
332
|
+
return 'unknown';
|
|
333
|
+
}
|
|
334
|
+
// The role with highest significance is primary
|
|
335
|
+
// But if multiple roles have similar significance, prefer more specific ones
|
|
336
|
+
const topRole = roleChanges[0];
|
|
337
|
+
// If there's a close second that's more specific, consider it
|
|
338
|
+
if (roleChanges.length > 1) {
|
|
339
|
+
const secondRole = roleChanges[1];
|
|
340
|
+
const significanceDiff = topRole.significance - secondRole.significance;
|
|
341
|
+
// If within 15 points and second is more specific (api > logic > ui)
|
|
342
|
+
if (significanceDiff <= 15) {
|
|
343
|
+
const specificityOrder = ['api', 'data', 'style', 'logic', 'ui', 'config', 'test', 'unknown'];
|
|
344
|
+
const topIndex = specificityOrder.indexOf(topRole.role);
|
|
345
|
+
const secondIndex = specificityOrder.indexOf(secondRole.role);
|
|
346
|
+
if (secondIndex < topIndex) {
|
|
347
|
+
return secondRole.role;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return topRole.role;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Determine the overall intent of the changes
|
|
355
|
+
*/
|
|
356
|
+
function determineIntent(diff, stagedFiles, roleChanges) {
|
|
357
|
+
// Check file statuses first
|
|
358
|
+
const hasOnlyAdded = stagedFiles.every(f => f.status === 'A');
|
|
359
|
+
const hasOnlyDeleted = stagedFiles.every(f => f.status === 'D');
|
|
360
|
+
const hasOnlyModified = stagedFiles.every(f => f.status === 'M');
|
|
361
|
+
if (hasOnlyAdded) {
|
|
362
|
+
return 'add';
|
|
363
|
+
}
|
|
364
|
+
if (hasOnlyDeleted) {
|
|
365
|
+
return 'remove';
|
|
366
|
+
}
|
|
367
|
+
// If we have role changes, use the most significant role's intent
|
|
368
|
+
if (roleChanges.length > 0) {
|
|
369
|
+
const primaryRoleChange = roleChanges[0];
|
|
370
|
+
// Special case: if the primary intent is 'fix' and we see validation patterns
|
|
371
|
+
// even in non-modified files, treat it as a fix
|
|
372
|
+
if (hasOnlyModified && primaryRoleChange.intent === 'fix') {
|
|
373
|
+
return 'fix';
|
|
374
|
+
}
|
|
375
|
+
// If we're enhancing (useMemo, useCallback, etc.), that takes precedence
|
|
376
|
+
if (roleChanges.some(r => r.intent === 'enhance')) {
|
|
377
|
+
return 'enhance';
|
|
378
|
+
}
|
|
379
|
+
return primaryRoleChange.intent;
|
|
380
|
+
}
|
|
381
|
+
// Fallback to diff analysis
|
|
382
|
+
const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
|
|
383
|
+
const removedLines = (diff.match(/^-[^-]/gm) || []).length;
|
|
384
|
+
if (addedLines > 0 && removedLines === 0) {
|
|
385
|
+
return 'add';
|
|
386
|
+
}
|
|
387
|
+
if (removedLines > 0 && addedLines === 0) {
|
|
388
|
+
return 'remove';
|
|
389
|
+
}
|
|
390
|
+
const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
|
|
391
|
+
if (ratio > 0.6) {
|
|
392
|
+
return 'refactor';
|
|
393
|
+
}
|
|
394
|
+
return 'modify';
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Extract affected element names (components, functions, etc.) from the diff
|
|
398
|
+
*/
|
|
399
|
+
function extractAffectedElements(diff) {
|
|
400
|
+
const elements = [];
|
|
401
|
+
// Helper to add unique element names
|
|
402
|
+
const addUnique = (name) => {
|
|
403
|
+
if (!elements.includes(name)) {
|
|
404
|
+
elements.push(name);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
// Match both added (+) and modified (-) top-level declarations.
|
|
408
|
+
// For fix/refactor commits, the declaration line often appears as a removed line
|
|
409
|
+
// (the old version) or only in the - side of the diff. Using [+-] ensures we
|
|
410
|
+
// capture the affected function/class/component name regardless of whether
|
|
411
|
+
// the declaration itself was added, removed, or modified.
|
|
412
|
+
// (?!\s) after [+-] rejects indented lines (local variables inside function bodies)
|
|
413
|
+
// Extract component names (PascalCase function/const declarations)
|
|
414
|
+
const componentFuncMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+([A-Z][a-zA-Z0-9]*)/gm);
|
|
415
|
+
if (componentFuncMatches) {
|
|
416
|
+
for (const match of componentFuncMatches) {
|
|
417
|
+
const nameMatch = match.match(/function\s+([A-Z][a-zA-Z0-9]*)/);
|
|
418
|
+
if (nameMatch)
|
|
419
|
+
addUnique(nameMatch[1]);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const componentConstMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?const\s+([A-Z][a-zA-Z0-9]*)\s*=\s*(?:async\s+)?(?:function|\()/gm);
|
|
423
|
+
if (componentConstMatches) {
|
|
424
|
+
for (const match of componentConstMatches) {
|
|
425
|
+
const nameMatch = match.match(/const\s+([A-Z][a-zA-Z0-9]*)/);
|
|
426
|
+
if (nameMatch)
|
|
427
|
+
addUnique(nameMatch[1]);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Extract function names (camelCase declarations)
|
|
431
|
+
const funcKeywordMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+([a-z][a-zA-Z0-9]*)\s*\(/gm);
|
|
432
|
+
if (funcKeywordMatches) {
|
|
433
|
+
for (const match of funcKeywordMatches) {
|
|
434
|
+
const nameMatch = match.match(/function\s+([a-z][a-zA-Z0-9]*)/);
|
|
435
|
+
if (nameMatch)
|
|
436
|
+
addUnique(nameMatch[1]);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// const/let arrow or function expression: const foo = (, export const foo = async (
|
|
440
|
+
const funcConstMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?const\s+([a-z][a-zA-Z0-9]*)\s*=\s*(?:async\s+)?(?:function|\()/gm);
|
|
441
|
+
if (funcConstMatches) {
|
|
442
|
+
for (const match of funcConstMatches) {
|
|
443
|
+
const nameMatch = match.match(/const\s+([a-z][a-zA-Z0-9]*)/);
|
|
444
|
+
if (nameMatch)
|
|
445
|
+
addUnique(nameMatch[1]);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Extract interface/type names
|
|
449
|
+
const typeMatches = diff.match(/^[+-](?!\s)(?:export\s+)?(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/gm);
|
|
450
|
+
if (typeMatches) {
|
|
451
|
+
for (const match of typeMatches) {
|
|
452
|
+
const nameMatch = match.match(/(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/);
|
|
453
|
+
if (nameMatch)
|
|
454
|
+
addUnique(nameMatch[1]);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Extract class names
|
|
458
|
+
const classMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Z][a-zA-Z0-9]*)/gm);
|
|
459
|
+
if (classMatches) {
|
|
460
|
+
for (const match of classMatches) {
|
|
461
|
+
const nameMatch = match.match(/class\s+([A-Z][a-zA-Z0-9]*)/);
|
|
462
|
+
if (nameMatch)
|
|
463
|
+
addUnique(nameMatch[1]);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return elements.slice(0, 5); // Limit to top 5 elements
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Extract function/class/method names from git diff by analyzing both hunk headers
|
|
470
|
+
* and the actual context lines around changes.
|
|
471
|
+
*
|
|
472
|
+
* This method prioritizes function declarations found in context lines near the
|
|
473
|
+
* actual changes, which is more accurate than just using hunk headers (which may
|
|
474
|
+
* show preceding functions instead of the one being modified).
|
|
475
|
+
*/
|
|
476
|
+
function extractHunkContext(diff) {
|
|
477
|
+
const names = [];
|
|
478
|
+
// Split diff into hunks
|
|
479
|
+
const hunks = diff.split(/^@@/gm).slice(1); // Skip header before first @@
|
|
480
|
+
for (const hunk of hunks) {
|
|
481
|
+
const lines = hunk.split('\n');
|
|
482
|
+
let lastFunctionName = null;
|
|
483
|
+
let foundChange = false;
|
|
484
|
+
// Skip first line (hunk header like " -425,7 +425,7 @@ function normalize...")
|
|
485
|
+
for (let i = 1; i < lines.length; i++) {
|
|
486
|
+
const line = lines[i];
|
|
487
|
+
// Check if this is a context line (starts with space) or added line (starts with +)
|
|
488
|
+
// Context lines show surrounding code that hasn't changed
|
|
489
|
+
if (line.startsWith(' ') || (line.startsWith('+') && !line.startsWith('+++'))) {
|
|
490
|
+
const extractedName = extractNameFromLine(line);
|
|
491
|
+
if (extractedName) {
|
|
492
|
+
lastFunctionName = extractedName;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// If we find an actual change (+/-) and we have a function name, record it
|
|
496
|
+
if ((line.startsWith('+') || line.startsWith('-')) &&
|
|
497
|
+
!line.startsWith('+++') && !line.startsWith('---') &&
|
|
498
|
+
lastFunctionName &&
|
|
499
|
+
!foundChange) {
|
|
500
|
+
if (!names.includes(lastFunctionName)) {
|
|
501
|
+
names.push(lastFunctionName);
|
|
502
|
+
}
|
|
503
|
+
foundChange = true; // Only record once per hunk
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// Fallback: if no function found in context lines, try hunk header
|
|
507
|
+
if (!foundChange && lines[0]) {
|
|
508
|
+
const headerMatch = lines[0].match(/^@?\s+[^@]+@@\s+(.+)$/);
|
|
509
|
+
if (headerMatch) {
|
|
510
|
+
const name = extractNameFromLine(headerMatch[1]);
|
|
511
|
+
if (name && !names.includes(name)) {
|
|
512
|
+
names.push(name);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return names.slice(0, 5);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Extract a function/class/method name from a single line of code
|
|
521
|
+
*/
|
|
522
|
+
function extractNameFromLine(line) {
|
|
523
|
+
// Remove leading diff markers and whitespace
|
|
524
|
+
const cleanLine = line.replace(/^[+\-\s@]*/, '').trim();
|
|
525
|
+
// Skip lines that are clearly control flow (for, if, while, etc.)
|
|
526
|
+
if (/^\s*(if|else|for|while|do|switch|catch|return|throw)\s*\(/.test(cleanLine)) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
// function declarations: function foo(, async function foo(, export function foo(
|
|
530
|
+
const funcMatch = cleanLine.match(/(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
531
|
+
if (funcMatch)
|
|
532
|
+
return funcMatch[1];
|
|
533
|
+
// const/let/var arrow or function expression: const foo = (, export const foo = async (
|
|
534
|
+
// Only match if it looks like a function (has = followed by function keyword, arrow, or paren)
|
|
535
|
+
const constMatch = cleanLine.match(/(?:export\s+(?:default\s+)?)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\(|[a-zA-Z_$])/);
|
|
536
|
+
if (constMatch)
|
|
537
|
+
return constMatch[1];
|
|
538
|
+
// class declarations: class Foo {, export class Foo extends Bar {
|
|
539
|
+
const classMatch = cleanLine.match(/(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Z][a-zA-Z0-9]*)/);
|
|
540
|
+
if (classMatch)
|
|
541
|
+
return classMatch[1];
|
|
542
|
+
// class method: methodName(, async methodName(, private methodName(, static async methodName(
|
|
543
|
+
// Exclude control flow keywords (if, for, while, etc.) which share the `keyword(` shape
|
|
544
|
+
const methodMatch = cleanLine.match(/(?:public|private|protected)?\s*(?:static\s+)?(?:async\s+)?(?!if|else|for|while|do|switch|catch|return|throw|new|typeof|instanceof|delete|void|yield|await\b)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/);
|
|
545
|
+
if (methodMatch)
|
|
546
|
+
return methodMatch[1];
|
|
547
|
+
// interface/type declarations
|
|
548
|
+
const typeMatch = cleanLine.match(/(?:export\s+)?(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/);
|
|
549
|
+
if (typeMatch)
|
|
550
|
+
return typeMatch[1];
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Extract affected elements for a specific role
|
|
555
|
+
*/
|
|
556
|
+
function extractElementsForRole(diff, role) {
|
|
557
|
+
const elements = [];
|
|
558
|
+
switch (role) {
|
|
559
|
+
case 'ui':
|
|
560
|
+
// Extract JSX component names being used
|
|
561
|
+
const jsxMatches = diff.match(/^\+.*<([A-Z][a-zA-Z0-9]*)/gm);
|
|
562
|
+
if (jsxMatches) {
|
|
563
|
+
for (const match of jsxMatches) {
|
|
564
|
+
const nameMatch = match.match(/<([A-Z][a-zA-Z0-9]*)/);
|
|
565
|
+
if (nameMatch && !elements.includes(nameMatch[1])) {
|
|
566
|
+
elements.push(nameMatch[1]);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
break;
|
|
571
|
+
case 'data':
|
|
572
|
+
// Extract state variable names
|
|
573
|
+
const stateMatches = diff.match(/^\+.*const\s+\[([a-z][a-zA-Z0-9]*),/gm);
|
|
574
|
+
if (stateMatches) {
|
|
575
|
+
for (const match of stateMatches) {
|
|
576
|
+
const nameMatch = match.match(/const\s+\[([a-z][a-zA-Z0-9]*)/);
|
|
577
|
+
if (nameMatch) {
|
|
578
|
+
elements.push(nameMatch[1]);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
break;
|
|
583
|
+
case 'api':
|
|
584
|
+
// Extract API endpoints
|
|
585
|
+
const apiMatches = diff.match(/['"`]\/api\/[^'"`]+['"`]/g);
|
|
586
|
+
if (apiMatches) {
|
|
587
|
+
elements.push(...apiMatches.map(m => m.replace(/['"`]/g, '')));
|
|
588
|
+
}
|
|
589
|
+
break;
|
|
590
|
+
default:
|
|
591
|
+
// Use generic extraction
|
|
592
|
+
return extractAffectedElements(diff).slice(0, 3);
|
|
593
|
+
}
|
|
594
|
+
return elements.slice(0, 3);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Detect function/class/type renames by comparing removed and added names
|
|
598
|
+
* Returns an array of { oldName, newName } objects
|
|
599
|
+
*/
|
|
600
|
+
function detectRenames(diff) {
|
|
601
|
+
const renames = [];
|
|
602
|
+
// Patterns for extracting function/class/type names from removed and added lines
|
|
603
|
+
const patterns = [
|
|
604
|
+
// Function declarations: function name( or const name = or const name: Type =
|
|
605
|
+
{ regex: /^[-+].*(?:function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm, type: 'function' },
|
|
606
|
+
{ regex: /^[-+].*(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[=:]/gm, type: 'function' },
|
|
607
|
+
// Class declarations
|
|
608
|
+
{ regex: /^[-+].*class\s+([A-Z][a-zA-Z0-9]*)/gm, type: 'class' },
|
|
609
|
+
// Interface declarations
|
|
610
|
+
{ regex: /^[-+].*interface\s+([A-Z][a-zA-Z0-9]*)/gm, type: 'interface' },
|
|
611
|
+
// Type declarations
|
|
612
|
+
{ regex: /^[-+].*type\s+([A-Z][a-zA-Z0-9]*)\s*=/gm, type: 'type' },
|
|
613
|
+
];
|
|
614
|
+
for (const { regex, type } of patterns) {
|
|
615
|
+
const removed = [];
|
|
616
|
+
const added = [];
|
|
617
|
+
let match;
|
|
618
|
+
regex.lastIndex = 0;
|
|
619
|
+
while ((match = regex.exec(diff)) !== null) {
|
|
620
|
+
const line = match[0];
|
|
621
|
+
const name = match[1];
|
|
622
|
+
if (line.startsWith('-') && !line.startsWith('---')) {
|
|
623
|
+
removed.push(name);
|
|
624
|
+
}
|
|
625
|
+
else if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
626
|
+
added.push(name);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
// If we have exactly one removed and one added of the same type,
|
|
630
|
+
// and they're different names, it's likely a rename
|
|
631
|
+
if (removed.length === 1 && added.length === 1 && removed[0] !== added[0]) {
|
|
632
|
+
renames.push({ oldName: removed[0], newName: added[0], type });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return renames;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Generate a human-readable summary for a role change
|
|
639
|
+
*/
|
|
640
|
+
function generateRoleSummary(role, intent, matchCount) {
|
|
641
|
+
const roleDesc = exports.ROLE_DESCRIPTIONS[role];
|
|
642
|
+
const intentVerb = exports.INTENT_VERBS[intent].past;
|
|
643
|
+
if (matchCount === 1) {
|
|
644
|
+
return `${intentVerb} ${roleDesc}`;
|
|
645
|
+
}
|
|
646
|
+
return `${intentVerb} ${roleDesc} (${matchCount} changes)`;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Generate the WHY description for the commit
|
|
650
|
+
*/
|
|
651
|
+
function generateIntentDescription(intent, role, roleChanges) {
|
|
652
|
+
const roleDesc = exports.ROLE_DESCRIPTIONS[role];
|
|
653
|
+
switch (intent) {
|
|
654
|
+
case 'add':
|
|
655
|
+
return `to add new ${roleDesc}`;
|
|
656
|
+
case 'fix':
|
|
657
|
+
return `to fix ${roleDesc} issues`;
|
|
658
|
+
case 'refactor':
|
|
659
|
+
return `to improve ${roleDesc} structure`;
|
|
660
|
+
case 'enhance':
|
|
661
|
+
return `to optimize ${roleDesc} performance`;
|
|
662
|
+
case 'remove':
|
|
663
|
+
return `to remove unused ${roleDesc}`;
|
|
664
|
+
case 'modify':
|
|
665
|
+
default:
|
|
666
|
+
if (roleChanges.length > 1) {
|
|
667
|
+
return `to update ${roleDesc} and related code`;
|
|
668
|
+
}
|
|
669
|
+
return `to update ${roleDesc}`;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Generate the WHAT changed description
|
|
674
|
+
*/
|
|
675
|
+
function generateWhatChanged(roleChanges, affectedElements, stagedFiles, hunkContext) {
|
|
676
|
+
// If we have specific elements from declarations, use them
|
|
677
|
+
if (affectedElements.length > 0) {
|
|
678
|
+
if (affectedElements.length === 1) {
|
|
679
|
+
return affectedElements[0];
|
|
680
|
+
}
|
|
681
|
+
if (affectedElements.length <= 3) {
|
|
682
|
+
return affectedElements.join(', ');
|
|
683
|
+
}
|
|
684
|
+
return `${affectedElements.slice(0, 2).join(', ')} and ${affectedElements.length - 2} more`;
|
|
685
|
+
}
|
|
686
|
+
// Fall back to hunk context (function/class names from @@ headers)
|
|
687
|
+
// This is especially useful for fix/refactor where changes are inside
|
|
688
|
+
// function bodies and the declaration line itself isn't in the diff
|
|
689
|
+
if (hunkContext && hunkContext.length > 0) {
|
|
690
|
+
if (hunkContext.length === 1) {
|
|
691
|
+
return hunkContext[0];
|
|
692
|
+
}
|
|
693
|
+
if (hunkContext.length <= 3) {
|
|
694
|
+
return hunkContext.join(', ');
|
|
695
|
+
}
|
|
696
|
+
return `${hunkContext.slice(0, 2).join(', ')} and ${hunkContext.length - 2} more`;
|
|
697
|
+
}
|
|
698
|
+
// Fall back to role-based description
|
|
699
|
+
if (roleChanges.length > 0) {
|
|
700
|
+
const primaryRole = roleChanges[0];
|
|
701
|
+
if (primaryRole.affectedElements.length > 0) {
|
|
702
|
+
return primaryRole.affectedElements[0];
|
|
703
|
+
}
|
|
704
|
+
return exports.ROLE_DESCRIPTIONS[primaryRole.role];
|
|
705
|
+
}
|
|
706
|
+
// Fall back to file names
|
|
707
|
+
if (stagedFiles.length === 1) {
|
|
708
|
+
const parts = stagedFiles[0].path.split('/');
|
|
709
|
+
return parts[parts.length - 1].replace(/\.\w+$/, '');
|
|
710
|
+
}
|
|
711
|
+
return 'code';
|
|
712
|
+
}
|
|
713
|
+
//# sourceMappingURL=semanticAnalyzer.js.map
|