@m3hti/commit-genie 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/commands/generate.d.ts.map +1 -1
  2. package/dist/commands/generate.js +10 -0
  3. package/dist/commands/generate.js.map +1 -1
  4. package/dist/commands/generate.test.js +38 -0
  5. package/dist/commands/generate.test.js.map +1 -1
  6. package/dist/services/analyzerService.d.ts +0 -157
  7. package/dist/services/analyzerService.d.ts.map +1 -1
  8. package/dist/services/analyzerService.js +75 -2010
  9. package/dist/services/analyzerService.js.map +1 -1
  10. package/dist/services/analyzerService.test.js +66 -4
  11. package/dist/services/analyzerService.test.js.map +1 -1
  12. package/dist/services/breakingChangeDetector.d.ts +9 -0
  13. package/dist/services/breakingChangeDetector.d.ts.map +1 -0
  14. package/dist/services/breakingChangeDetector.js +76 -0
  15. package/dist/services/breakingChangeDetector.js.map +1 -0
  16. package/dist/services/commitTypeDetector.d.ts +39 -0
  17. package/dist/services/commitTypeDetector.d.ts.map +1 -0
  18. package/dist/services/commitTypeDetector.js +510 -0
  19. package/dist/services/commitTypeDetector.js.map +1 -0
  20. package/dist/services/descriptionGenerator.d.ts +58 -0
  21. package/dist/services/descriptionGenerator.d.ts.map +1 -0
  22. package/dist/services/descriptionGenerator.js +566 -0
  23. package/dist/services/descriptionGenerator.js.map +1 -0
  24. package/dist/services/gitService.test.js +242 -24
  25. package/dist/services/gitService.test.js.map +1 -1
  26. package/dist/services/hookService.test.d.ts +2 -0
  27. package/dist/services/hookService.test.d.ts.map +1 -0
  28. package/dist/services/hookService.test.js +182 -0
  29. package/dist/services/hookService.test.js.map +1 -0
  30. package/dist/services/lintService.test.d.ts +2 -0
  31. package/dist/services/lintService.test.d.ts.map +1 -0
  32. package/dist/services/lintService.test.js +288 -0
  33. package/dist/services/lintService.test.js.map +1 -0
  34. package/dist/services/messageBuilder.d.ts +16 -0
  35. package/dist/services/messageBuilder.d.ts.map +1 -0
  36. package/dist/services/messageBuilder.js +135 -0
  37. package/dist/services/messageBuilder.js.map +1 -0
  38. package/dist/services/scopeDetector.d.ts +6 -0
  39. package/dist/services/scopeDetector.d.ts.map +1 -0
  40. package/dist/services/scopeDetector.js +51 -0
  41. package/dist/services/scopeDetector.js.map +1 -0
  42. package/dist/services/semanticAnalyzer.d.ts +25 -0
  43. package/dist/services/semanticAnalyzer.d.ts.map +1 -0
  44. package/dist/services/semanticAnalyzer.js +713 -0
  45. package/dist/services/semanticAnalyzer.js.map +1 -0
  46. package/dist/services/splitService.test.d.ts +2 -0
  47. package/dist/services/splitService.test.d.ts.map +1 -0
  48. package/dist/services/splitService.test.js +190 -0
  49. package/dist/services/splitService.test.js.map +1 -0
  50. package/dist/services/statsService.test.d.ts +2 -0
  51. package/dist/services/statsService.test.d.ts.map +1 -0
  52. package/dist/services/statsService.test.js +211 -0
  53. package/dist/services/statsService.test.js.map +1 -0
  54. package/dist/types/index.d.ts +5 -0
  55. package/dist/types/index.d.ts.map +1 -1
  56. 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