@m3hti/commit-genie 3.0.1 โ†’ 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.
Files changed (57) hide show
  1. package/dist/services/analyzerService.d.ts +0 -141
  2. package/dist/services/analyzerService.d.ts.map +1 -1
  3. package/dist/services/analyzerService.js +23 -1880
  4. package/dist/services/analyzerService.js.map +1 -1
  5. package/dist/services/analyzerService.test.js +97 -0
  6. package/dist/services/analyzerService.test.js.map +1 -1
  7. package/dist/services/breakingChangeDetector.d.ts +9 -0
  8. package/dist/services/breakingChangeDetector.d.ts.map +1 -0
  9. package/dist/services/breakingChangeDetector.js +76 -0
  10. package/dist/services/breakingChangeDetector.js.map +1 -0
  11. package/dist/services/commitTypeDetector.d.ts +32 -0
  12. package/dist/services/commitTypeDetector.d.ts.map +1 -0
  13. package/dist/services/commitTypeDetector.js +490 -0
  14. package/dist/services/commitTypeDetector.js.map +1 -0
  15. package/dist/services/descriptionGenerator.d.ts +58 -0
  16. package/dist/services/descriptionGenerator.d.ts.map +1 -0
  17. package/dist/services/descriptionGenerator.js +569 -0
  18. package/dist/services/descriptionGenerator.js.map +1 -0
  19. package/dist/services/fileContentAnalyzer.d.ts +10 -0
  20. package/dist/services/fileContentAnalyzer.d.ts.map +1 -0
  21. package/dist/services/fileContentAnalyzer.js +170 -0
  22. package/dist/services/fileContentAnalyzer.js.map +1 -0
  23. package/dist/services/fileContentAnalyzer.test.d.ts +2 -0
  24. package/dist/services/fileContentAnalyzer.test.d.ts.map +1 -0
  25. package/dist/services/fileContentAnalyzer.test.js +118 -0
  26. package/dist/services/fileContentAnalyzer.test.js.map +1 -0
  27. package/dist/services/gitService.test.js +242 -24
  28. package/dist/services/gitService.test.js.map +1 -1
  29. package/dist/services/hookService.test.d.ts +2 -0
  30. package/dist/services/hookService.test.d.ts.map +1 -0
  31. package/dist/services/hookService.test.js +182 -0
  32. package/dist/services/hookService.test.js.map +1 -0
  33. package/dist/services/lintService.test.d.ts +2 -0
  34. package/dist/services/lintService.test.d.ts.map +1 -0
  35. package/dist/services/lintService.test.js +288 -0
  36. package/dist/services/lintService.test.js.map +1 -0
  37. package/dist/services/messageBuilder.d.ts +16 -0
  38. package/dist/services/messageBuilder.d.ts.map +1 -0
  39. package/dist/services/messageBuilder.js +135 -0
  40. package/dist/services/messageBuilder.js.map +1 -0
  41. package/dist/services/scopeDetector.d.ts +6 -0
  42. package/dist/services/scopeDetector.d.ts.map +1 -0
  43. package/dist/services/scopeDetector.js +51 -0
  44. package/dist/services/scopeDetector.js.map +1 -0
  45. package/dist/services/semanticAnalyzer.d.ts +25 -0
  46. package/dist/services/semanticAnalyzer.d.ts.map +1 -0
  47. package/dist/services/semanticAnalyzer.js +713 -0
  48. package/dist/services/semanticAnalyzer.js.map +1 -0
  49. package/dist/services/splitService.test.d.ts +2 -0
  50. package/dist/services/splitService.test.d.ts.map +1 -0
  51. package/dist/services/splitService.test.js +190 -0
  52. package/dist/services/splitService.test.js.map +1 -0
  53. package/dist/services/statsService.test.d.ts +2 -0
  54. package/dist/services/statsService.test.d.ts.map +1 -0
  55. package/dist/services/statsService.test.js +211 -0
  56. package/dist/services/statsService.test.js.map +1 -0
  57. package/package.json +1 -1
@@ -6,27 +6,14 @@ const configService_1 = require("./configService");
6
6
  const historyService_1 = require("./historyService");
7
7
  const aiService_1 = require("./aiService");
8
8
  const filePatterns_1 = require("../utils/filePatterns");
9
+ const commitTypeDetector_1 = require("./commitTypeDetector");
9
10
  const astExportAnalyzer_1 = require("./astExportAnalyzer");
10
11
  const astClassifier_1 = require("./astClassifier");
11
- const COMMIT_EMOJIS = {
12
- feat: 'โœจ',
13
- fix: '๐Ÿ›',
14
- docs: '๐Ÿ“š',
15
- style: '๐Ÿ’„',
16
- refactor: 'โ™ป๏ธ',
17
- test: '๐Ÿงช',
18
- chore: '๐Ÿ”ง',
19
- perf: 'โšก',
20
- };
21
- // Default keywords that indicate breaking changes
22
- // Note: generic words like "removed" are excluded to avoid false positives
23
- // from descriptive comments (e.g. "// Return array with zeros removed")
24
- const DEFAULT_BREAKING_KEYWORDS = [
25
- 'breaking',
26
- 'breaking change',
27
- 'breaking-change',
28
- 'incompatible',
29
- ];
12
+ const breakingChangeDetector_1 = require("./breakingChangeDetector");
13
+ const descriptionGenerator_1 = require("./descriptionGenerator");
14
+ const semanticAnalyzer_1 = require("./semanticAnalyzer");
15
+ const scopeDetector_1 = require("./scopeDetector");
16
+ const messageBuilder_1 = require("./messageBuilder");
30
17
  /**
31
18
  * Compute the aggregate export diff across all staged source files
32
19
  * using AST analysis on full file contents (before vs after).
@@ -47,186 +34,6 @@ function computeExportDiffForStagedFiles(stagedFiles) {
47
34
  }
48
35
  return { removed: allRemoved, added: allAdded };
49
36
  }
50
- // Role detection patterns for semantic analysis
51
- const ROLE_PATTERNS = {
52
- ui: [
53
- // JSX elements and components
54
- /^\+.*<[A-Z][a-zA-Z]*[\s/>]/m, // JSX component usage
55
- /^\+.*<\/[A-Z][a-zA-Z]*>/m, // JSX component closing
56
- /^\+.*<(div|span|p|h[1-6]|button|input|form|ul|li|table|img|a|section|header|footer|nav|main|article|aside)\b/mi,
57
- /^\+.*className\s*=/m, // className attribute
58
- /^\+.*style\s*=\s*\{/m, // inline styles
59
- /^\+.*return\s*\(/m, // render return
60
- /^\+.*render\s*\(\s*\)/m, // render method
61
- /^\+.*(?:<>|<\/>)/m, // React fragments
62
- /^\+.*aria-\w+=/m, // accessibility attributes
63
- /^\+.*role\s*=/m, // role attribute
64
- /^\+.*\b(?:onClick|onSubmit|onChange|onFocus|onBlur|onKeyDown|onKeyUp|onMouseEnter|onMouseLeave)\b/m,
65
- ],
66
- logic: [
67
- // Business logic patterns
68
- /^\+.*\bif\s*\(/m, // conditionals
69
- /^\+.*\bswitch\s*\(/m, // switch statements
70
- /^\+.*\bfor\s*\(/m, // for loops
71
- /^\+.*\bwhile\s*\(/m, // while loops
72
- /^\+.*\.map\s*\(/m, // array operations
73
- /^\+.*\.filter\s*\(/m,
74
- /^\+.*\.reduce\s*\(/m,
75
- /^\+.*\.find\s*\(/m,
76
- /^\+.*\.some\s*\(/m,
77
- /^\+.*\.every\s*\(/m,
78
- /^\+.*\btry\s*\{/m, // error handling
79
- /^\+.*\bcatch\s*\(/m,
80
- /^\+.*\bthrow\s+/m,
81
- /^\+.*\breturn\s+(?![\s(]?<)/m, // return (not JSX)
82
- /^\+.*\bawait\s+/m, // async operations
83
- /^\+.*\bnew\s+[A-Z]/m, // object instantiation
84
- /^\+.*=>\s*\{/m, // arrow function body
85
- ],
86
- data: [
87
- // State and data management
88
- /^\+.*\buseState\s*[<(]/m, // React state
89
- /^\+.*\buseReducer\s*\(/m, // React reducer
90
- /^\+.*\buseContext\s*\(/m, // React context
91
- /^\+.*\buseSelector\s*\(/m, // Redux selector
92
- /^\+.*\bdispatch\s*\(/m, // Redux dispatch
93
- /^\+.*\bsetState\s*\(/m, // Class component state
94
- /^\+.*\bthis\.state\b/m, // Class component state access
95
- /^\+.*\bprops\./m, // Props access
96
- /^\+.*interface\s+\w+Props/m, // Props interface
97
- /^\+.*type\s+\w+Props/m, // Props type
98
- /^\+.*\bconst\s+\[[a-z]+,\s*set[A-Z]/m, // State destructuring
99
- /^\+.*:\s*\w+\[\]/m, // Array type
100
- /^\+.*:\s*(string|number|boolean|object)/m, // Primitive types
101
- /^\+.*\binterface\s+\w+/m, // Interface definition
102
- /^\+.*\btype\s+\w+\s*=/m, // Type definition
103
- ],
104
- style: [
105
- // CSS and styling
106
- /^\+.*\bstyles?\./m, // styles object access
107
- /^\+.*\bstyled\./m, // styled-components
108
- /^\+.*\bcss`/m, // CSS template literal
109
- /^\+.*\bsx\s*=\s*\{/m, // MUI sx prop
110
- /^\+.*:\s*['"]?[0-9]+(px|em|rem|%|vh|vw)/m, // CSS units
111
- /^\+.*:\s*['"]?#[0-9a-fA-F]{3,8}/m, // Hex colors
112
- /^\+.*:\s*['"]?rgb(a)?\s*\(/m, // RGB colors
113
- /^\+.*(margin|padding|width|height|border|background|color|font|display|flex|grid)\s*:/m,
114
- /^\+.*\btheme\./m, // Theme access
115
- /^\+.*\.module\.css/m, // CSS modules import
116
- ],
117
- api: [
118
- // API and network calls
119
- /^\+.*\bfetch\s*\(/m, // fetch API
120
- /^\+.*\baxios\./m, // axios
121
- /^\+.*\.get\s*\(/m, // HTTP GET
122
- /^\+.*\.post\s*\(/m, // HTTP POST
123
- /^\+.*\.put\s*\(/m, // HTTP PUT
124
- /^\+.*\.delete\s*\(/m, // HTTP DELETE
125
- /^\+.*\.patch\s*\(/m, // HTTP PATCH
126
- /^\+.*\bapi\./m, // api object access
127
- /^\+.*\/api\//m, // API path
128
- /^\+.*\bendpoint/mi, // endpoint reference
129
- /^\+.*\bheaders\s*:/m, // HTTP headers
130
- /^\+.*\bAuthorization:/m, // Auth header
131
- /^\+.*\buseQuery\s*\(/m, // React Query
132
- /^\+.*\buseMutation\s*\(/m, // React Query mutation
133
- /^\+.*\bswr\b/mi, // SWR
134
- /^\+.*\bgraphql`/m, // GraphQL
135
- /^\+.*\bquery\s*\{/m, // GraphQL query
136
- /^\+.*\bmutation\s*\{/m, // GraphQL mutation
137
- ],
138
- config: [
139
- // Configuration
140
- /^\+.*\bprocess\.env\./m, // Environment variables
141
- /^\+.*\bimport\.meta\.env\./m, // Vite env
142
- /^\+.*\bCONFIG\./m, // Config constant
143
- /^\+.*\bsettings\./m, // Settings object
144
- /^\+.*\boptions\s*:/m, // Options object
145
- /^\+.*\bdefaultProps/m, // Default props
146
- /^\+.*\bexport\s+(const|let)\s+[A-Z_]+\s*=/m, // Constant export
147
- /^\+.*:\s*['"]?(development|production|test)['"]/m, // Environment strings
148
- ],
149
- test: [
150
- // Testing patterns
151
- /^\+.*\bdescribe\s*\(/m, // Test suite
152
- /^\+.*\bit\s*\(/m, // Test case
153
- /^\+.*\btest\s*\(/m, // Test case
154
- /^\+.*\bexpect\s*\(/m, // Assertion
155
- /^\+.*\bjest\./m, // Jest
156
- /^\+.*\bmock\(/m, // Mocking
157
- /^\+.*\bspyOn\s*\(/m, // Spy
158
- /^\+.*\bbeforeEach\s*\(/m, // Setup
159
- /^\+.*\bafterEach\s*\(/m, // Teardown
160
- /^\+.*\brender\s*\(/m, // React testing library
161
- ],
162
- unknown: [],
163
- };
164
- // Intent detection patterns
165
- const INTENT_PATTERNS = {
166
- add: [
167
- /^\+\s*export\s+(function|class|const|interface|type)/m,
168
- /^\+\s*(async\s+)?function\s+\w+/m,
169
- /^\+\s*const\s+\w+\s*=\s*(async\s+)?\(/m,
170
- /^\+\s*class\s+\w+/m,
171
- ],
172
- modify: [
173
- // Changes that have both additions and deletions of similar patterns
174
- ],
175
- fix: [
176
- /^\+.*\btypeof\s+\w+\s*[!=]==?\s*['"`]/m,
177
- /^\+.*\binstanceof\s+/m,
178
- /^\+.*\bArray\.isArray\s*\(/m,
179
- /^\+.*\bif\s*\(\s*!\w+\s*\)/m,
180
- /^\+.*\?\?/m,
181
- /^\+.*\?\./m,
182
- /^\+.*\|\|\s*['"{\[0]/m, // Default values
183
- /^\+.*\bcatch\s*\(/m,
184
- /^\+.*\btry\s*\{/m,
185
- ],
186
- remove: [
187
- /^-\s*export\s+(function|class|const|interface|type)/m,
188
- /^-\s*(async\s+)?function\s+\w+/m,
189
- /^-\s*class\s+\w+/m,
190
- ],
191
- refactor: [
192
- /^\+.*=>/m, // Arrow function conversion
193
- /^\+.*\.\.\./m, // Spread operator
194
- /^\+.*`\$\{/m, // Template literal
195
- /^\+.*Object\.(keys|values|entries)/m, // Object methods
196
- ],
197
- enhance: [
198
- /^\+.*\bmemo\s*\(/m, // React memo
199
- /^\+.*\buseMemo\s*\(/m, // useMemo hook
200
- /^\+.*\buseCallback\s*\(/m, // useCallback hook
201
- /^\+.*\blazy\s*\(/m, // React lazy
202
- /^\+.*\bSuspense\b/m, // React Suspense
203
- ],
204
- rename: [
205
- // Rename detection is handled by detectRenames() method
206
- // which compares removed and added function/class/type names
207
- ],
208
- };
209
- // Role descriptions for commit messages
210
- const ROLE_DESCRIPTIONS = {
211
- ui: 'UI/rendering',
212
- logic: 'business logic',
213
- data: 'data/state management',
214
- style: 'styling',
215
- api: 'API integration',
216
- config: 'configuration',
217
- test: 'tests',
218
- unknown: 'code',
219
- };
220
- // Intent verb mappings for commit messages
221
- const INTENT_VERBS = {
222
- add: { past: 'added', present: 'add' },
223
- modify: { past: 'updated', present: 'update' },
224
- fix: { past: 'fixed', present: 'fix' },
225
- remove: { past: 'removed', present: 'remove' },
226
- refactor: { past: 'refactored', present: 'refactor' },
227
- enhance: { past: 'enhanced', present: 'improve' },
228
- rename: { past: 'renamed', present: 'rename' },
229
- };
230
37
  class AnalyzerService {
231
38
  /**
232
39
  * Analyze staged changes and return structured analysis
@@ -293,12 +100,12 @@ class AnalyzerService {
293
100
  commitType = astResult.primaryType;
294
101
  }
295
102
  else {
296
- commitType = this.determineCommitType(filesAffected, diff, stagedFiles, exportDiff);
103
+ commitType = (0, commitTypeDetector_1.determineCommitType)(filesAffected, diff, stagedFiles, exportDiff);
297
104
  }
298
105
  // Determine scope if applicable
299
- const scope = this.determineScope(stagedFiles);
106
+ const scope = (0, scopeDetector_1.determineScope)(stagedFiles);
300
107
  // Detect breaking changes (merge AST + regex results)
301
- let { isBreaking, reasons } = this.detectBreakingChanges(diff, stagedFiles, exportDiff);
108
+ let { isBreaking, reasons } = (0, breakingChangeDetector_1.detectBreakingChanges)(diff, stagedFiles, exportDiff);
302
109
  if (astResult?.isBreaking) {
303
110
  isBreaking = true;
304
111
  reasons = [...new Set([...reasons, ...astResult.breakingReasons])];
@@ -307,7 +114,7 @@ class AnalyzerService {
307
114
  // Only perform for source files to avoid overhead on simple config/doc changes
308
115
  let semanticAnalysis;
309
116
  if (filesAffected.source > 0 && diff.length > 0) {
310
- semanticAnalysis = this.analyzeSemanticChanges(diff, stagedFiles);
117
+ semanticAnalysis = (0, semanticAnalyzer_1.analyzeSemanticChanges)(diff, stagedFiles);
311
118
  }
312
119
  // Non-exported renames don't break consumers โ€” the function still exists with a new name
313
120
  // But a renamed EXPORT is breaking because consumers' import statements break
@@ -336,7 +143,7 @@ class AnalyzerService {
336
143
  }
337
144
  }
338
145
  // Generate description (uses AST analysis, then semantic analysis as fallback)
339
- const description = this.generateDescription(filesAffected, {
146
+ const description = (0, descriptionGenerator_1.generateDescription)(filesAffected, {
340
147
  added: fileChanges.added.length,
341
148
  modified: fileChanges.modified.length,
342
149
  deleted: fileChanges.deleted.length,
@@ -355,1463 +162,6 @@ class AnalyzerService {
355
162
  astResult,
356
163
  };
357
164
  }
358
- /**
359
- * Detect breaking changes from diff content and file changes
360
- */
361
- static detectBreakingChanges(diff, stagedFiles, exportDiff) {
362
- const config = configService_1.ConfigService.getConfig();
363
- const breakingConfig = config.breakingChangeDetection;
364
- // Check if breaking change detection is disabled
365
- if (breakingConfig?.enabled === false) {
366
- return { isBreaking: false, reasons: [] };
367
- }
368
- const reasons = [];
369
- // Check for keyword-based breaking changes
370
- // Only check against non-comment code changes to avoid false positives
371
- // from descriptive comments (e.g. "// Return array with zeros removed")
372
- const keywords = breakingConfig?.keywords || DEFAULT_BREAKING_KEYWORDS;
373
- const codeChangesLower = this.extractNonCommentChanges(diff).toLowerCase();
374
- for (const keyword of keywords) {
375
- if (codeChangesLower.includes(keyword.toLowerCase())) {
376
- reasons.push(`Contains "${keyword}" keyword`);
377
- }
378
- }
379
- // Check for deleted source files (potentially breaking)
380
- const deletedSourceFiles = stagedFiles.filter(f => f.status === 'D' && (0, filePatterns_1.detectFileType)(f.path) === 'source');
381
- if (deletedSourceFiles.length > 0) {
382
- const fileNames = deletedSourceFiles.map(f => this.getFileName(f.path)).join(', ');
383
- reasons.push(`Deleted source files: ${fileNames}`);
384
- }
385
- // Export-aware breaking change detection (AST-based)
386
- const addedExportNames = new Set(exportDiff.added.map(e => e.name));
387
- // Exported symbols that were removed entirely (not renamed/moved)
388
- const trulyRemovedExports = exportDiff.removed.filter(e => !addedExportNames.has(e.name));
389
- if (trulyRemovedExports.length > 0) {
390
- const names = trulyRemovedExports.map(e => e.name).join(', ');
391
- reasons.push(`Removed exported members: ${names}`);
392
- }
393
- // Check for breaking signature changes on exported functions
394
- for (const removedExport of exportDiff.removed) {
395
- const matchingAdded = exportDiff.added.find(e => e.name === removedExport.name);
396
- if (matchingAdded && removedExport.params && matchingAdded.params) {
397
- if ((0, astExportAnalyzer_1.isSignatureBreakingAST)(removedExport.params, matchingAdded.params)) {
398
- reasons.push(`Breaking signature change on exported ${removedExport.kind}: ${removedExport.name}`);
399
- }
400
- }
401
- }
402
- // Check for renamed files that might break imports
403
- const renamedFiles = stagedFiles.filter(f => f.status === 'R');
404
- if (renamedFiles.length > 0) {
405
- const sourceRenames = renamedFiles.filter(f => (0, filePatterns_1.detectFileType)(f.path) === 'source');
406
- if (sourceRenames.length > 0) {
407
- reasons.push('Renamed source files (may break imports)');
408
- }
409
- }
410
- // Deduplicate reasons
411
- const uniqueReasons = [...new Set(reasons)];
412
- return {
413
- isBreaking: uniqueReasons.length > 0,
414
- reasons: uniqueReasons,
415
- };
416
- }
417
- /**
418
- * Determine the commit type based on analysis
419
- */
420
- static determineCommitType(filesAffected, diff, stagedFiles, exportDiff) {
421
- const diffLower = diff.toLowerCase();
422
- const filePaths = stagedFiles.map((f) => f.path.toLowerCase());
423
- // === FILE TYPE BASED DETECTION (highest priority) ===
424
- // If only test files changed
425
- if (filesAffected.test > 0 &&
426
- filesAffected.source === 0 &&
427
- filesAffected.docs === 0) {
428
- return 'test';
429
- }
430
- // If only docs changed
431
- if (filesAffected.docs > 0 &&
432
- filesAffected.source === 0 &&
433
- filesAffected.test === 0) {
434
- return 'docs';
435
- }
436
- // If only config files changed
437
- if (filesAffected.config > 0 &&
438
- filesAffected.source === 0 &&
439
- filesAffected.test === 0 &&
440
- filesAffected.docs === 0) {
441
- return 'chore';
442
- }
443
- // === DOCS DETECTION (comment-only changes) ===
444
- // Check if the changes are only adding/modifying comments (documentation)
445
- if (this.isCommentOnlyChange(diff)) {
446
- return 'docs';
447
- }
448
- // === FORMATTING-ONLY DETECTION (code style, not CSS) ===
449
- // Per Conventional Commits: "style" = changes that do not affect the meaning of the code
450
- // (white-space, formatting, missing semi-colons, etc.)
451
- if (this.isFormattingOnlyChange(diff)) {
452
- return 'style';
453
- }
454
- // === STYLE DETECTION (CSS/UI) ===
455
- // CSS/UI styling changes also classify as "style"
456
- // Check for CSS/styling file extensions
457
- const isStyleFile = filePaths.some(p => p.endsWith('.css') ||
458
- p.endsWith('.scss') ||
459
- p.endsWith('.sass') ||
460
- p.endsWith('.less') ||
461
- p.endsWith('.styl') ||
462
- p.endsWith('.stylus') ||
463
- p.includes('.styles.') ||
464
- p.includes('.style.') ||
465
- p.includes('styles/') ||
466
- p.includes('/theme') ||
467
- p.includes('theme.') ||
468
- p.includes('.theme.'));
469
- // Check for styled-components, Tailwind, or inline style changes in diff content
470
- const hasStyledComponentChanges = /^\+.*\bstyled\s*[.(]/m.test(diff) ||
471
- /^\+.*\bcss\s*`/m.test(diff) ||
472
- /^\+.*\@emotion\/styled/m.test(diff);
473
- 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);
474
- const hasInlineStyleChanges = /^\+.*\bstyle\s*=\s*\{\{/m.test(diff);
475
- const hasThemeChanges = /^\+.*(theme\s*[:=]|colors\s*[:=]|palette\s*[:=]|spacing\s*[:=]|typography\s*[:=])/m.test(diff);
476
- const hasSxPropChanges = /^\+.*\bsx\s*=\s*\{/m.test(diff);
477
- // Only classify as "style" if it's a CSS file OR contains actual CSS/styling code
478
- const hasActualStyleChanges = isStyleFile || hasStyledComponentChanges ||
479
- hasTailwindChanges || hasInlineStyleChanges ||
480
- hasThemeChanges || hasSxPropChanges;
481
- // NEVER classify as style if it's just JS without CSS
482
- // This prevents console.log, debug statements, comments, test prints from being labeled as style
483
- if (hasActualStyleChanges && !this.hasOnlyNonStyleJsChanges(diff, filePaths)) {
484
- return 'style';
485
- }
486
- // === PERFORMANCE DETECTION ===
487
- const perfPatterns = [
488
- /\bperformance\b/i,
489
- /\boptimiz(e|ation|ing)\b/i,
490
- /\bfaster\b/i,
491
- /\bspeed\s*(up|improvement)\b/i,
492
- /\bcach(e|ing)\b/i,
493
- /\bmemoiz(e|ation)\b/i,
494
- /\blazy\s*load/i,
495
- /\basync\b.*\bawait\b/i,
496
- /\bparallel\b/i,
497
- /\bbatch(ing)?\b/i,
498
- ];
499
- if (perfPatterns.some(p => p.test(diffLower))) {
500
- return 'perf';
501
- }
502
- // === DETECT NEW FUNCTIONALITY FIRST ===
503
- // This must come BEFORE fix detection to avoid false positives when new functions contain validation
504
- const hasNewFiles = stagedFiles.some((f) => f.status === 'A');
505
- // Comprehensive new code detection
506
- const hasNewExports = /^\+\s*export\s+(function|class|const|let|var|interface|type|default)/m.test(diff);
507
- const hasNewFunctions = /^\+\s*(async\s+)?function\s+\w+/m.test(diff);
508
- const hasNewArrowFunctions = /^\+\s*(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>/m.test(diff);
509
- const hasNewClasses = /^\+\s*(export\s+)?(class|abstract\s+class)\s+\w+/m.test(diff);
510
- // Detect new class/object methods - exclude control flow keywords (if, for, while, etc.)
511
- // which share the `keyword(...) {` shape but are NOT method declarations
512
- const hasNewMethods = /^\+\s+(async\s+)?(?!if|else|for|while|do|switch|catch|return|throw|new|typeof|instanceof|delete|void|yield|await\b)\w+\s*\([^)]*\)\s*{/m.test(diff);
513
- const hasNewInterfaces = /^\+\s*(export\s+)?(interface|type)\s+\w+/m.test(diff);
514
- const hasNewComponents = /^\+\s*(export\s+)?(const|function)\s+[A-Z]\w+/m.test(diff); // React components start with capital
515
- const hasNewImports = /^\+\s*import\s+/m.test(diff);
516
- // Count significant additions vs removals
517
- const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
518
- const removedLines = (diff.match(/^-[^-]/gm) || []).length;
519
- const netNewLines = addedLines - removedLines;
520
- // Detect if we're adding new functionality
521
- const hasNewFunctionality = hasNewExports || hasNewFunctions || hasNewArrowFunctions ||
522
- hasNewClasses || hasNewMethods || hasNewInterfaces || hasNewComponents;
523
- // === FEAT DETECTION (export-aware, AST-based) - CHECK BEFORE FIX ===
524
- // Only treat as feat when the PUBLIC export surface grows
525
- const removedExportNames = new Set(exportDiff.removed.map(e => e.name));
526
- const hasNewExportedSymbols = exportDiff.added.some(e => !removedExportNames.has(e.name));
527
- const hasAnyExportChanges = exportDiff.added.length > 0 || exportDiff.removed.length > 0;
528
- if (hasNewFiles) {
529
- return 'feat';
530
- }
531
- // New EXPORTED symbols = feat (public API surface grows)
532
- if (hasNewExportedSymbols) {
533
- return 'feat';
534
- }
535
- // New functions/classes that are NOT exported: internal refactoring
536
- if (hasNewFunctionality && !hasNewExportedSymbols && !hasAnyExportChanges) {
537
- // Balanced add/remove ratio indicates extraction/reorganization โ†’ refactor
538
- if (addedLines > 0 && removedLines > 0) {
539
- const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
540
- if (ratio > 0.3) {
541
- return 'refactor';
542
- }
543
- }
544
- // Heavily additive internal code falls through to feat patterns / fallback below
545
- }
546
- // Significant net additions with new imports often means new feature
547
- if (hasNewImports && netNewLines > 5) {
548
- return 'feat';
549
- }
550
- // Check for new feature indicators in comments/strings
551
- const featPatterns = [
552
- /\badd(s|ed|ing)?\s+(new\s+)?(feature|support|ability|option|functionality)/i,
553
- /\bimplement(s|ed|ing)?\s+\w+/i,
554
- /\bintroduc(e|es|ed|ing)\s+(new\s+)?\w+/i,
555
- /\benable(s|d|ing)?\s+(new\s+)?\w+/i,
556
- /\bnew\s+(feature|function|method|api|endpoint)/i,
557
- ];
558
- if (featPatterns.some(p => p.test(diff))) {
559
- return 'feat';
560
- }
561
- // === FIX DETECTION ===
562
- // Only check for validation patterns AFTER confirming no new functions/classes were added
563
- // Check if this is adding validation/guards to existing code (defensive coding = fix)
564
- const hasOnlyModifications = stagedFiles.every((f) => f.status === 'M');
565
- const validationPatterns = [
566
- /^\+.*\btypeof\s+\w+\s*===?\s*['"`]\w+['"`]/m, // typeof checks
567
- /^\+.*\binstanceof\s+\w+/m, // instanceof checks
568
- /^\+.*\bArray\.isArray\s*\(/m, // Array.isArray checks
569
- /^\+.*\b(Number|String|Boolean)\.is\w+\s*\(/m, // Number.isNaN, etc.
570
- /^\+.*\bif\s*\(\s*typeof\b/m, // if (typeof ...
571
- /^\+.*\bif\s*\(\s*!\w+\s*\)/m, // if (!var) guards
572
- /^\+.*\bif\s*\(\s*\w+\s*(===?|!==?)\s*(null|undefined)\s*\)/m, // null/undefined checks
573
- /^\+.*\bif\s*\(\s*(null|undefined)\s*(===?|!==?)\s*\w+\s*\)/m, // null/undefined checks (reversed)
574
- /^\+.*\bif\s*\(\s*[\w.\[\]]+\s*[<>]=?\s*-?\d/m, // numeric boundary checks: if (n < 0), if (arr[i] <= 0)
575
- /^\+.*\bif\s*\(\s*-?\d+\s*[<>]=?\s*[\w.\[\]]+/m, // numeric boundary checks (reversed): if (0 > n)
576
- /^\+.*\?\?/m, // Nullish coalescing
577
- /^\+.*\?\./m, // Optional chaining
578
- /^\+.*\|\|/m, // Default value patterns (when combined with guards)
579
- ];
580
- // Check if this is a simplification refactor (using modern syntax to replace verbose code)
581
- // When deletions > additions significantly, it's likely simplifying existing code, not adding new checks
582
- const isSimplificationRefactor = removedLines > addedLines &&
583
- (removedLines - addedLines) >= 3 && // At least 3 net lines removed (significant simplification)
584
- (/^\+.*\?\./m.test(diff) || // Using optional chaining
585
- /^\+.*\?\?/m.test(diff) // Using nullish coalescing
586
- ) &&
587
- (/^-.*\bif\s*\(/m.test(diff) // Removing if statements
588
- );
589
- // If it's a simplification (replacing verbose ifs with optional chaining), it's refactor
590
- if (isSimplificationRefactor) {
591
- return 'refactor';
592
- }
593
- // If only modifying files (no new functions detected) and adding validation patterns, it's likely a fix
594
- if (hasOnlyModifications && validationPatterns.some(p => p.test(diff))) {
595
- return 'fix';
596
- }
597
- const fixPatterns = [
598
- /\bfix(es|ed|ing)?\s*(the\s*)?(bug|issue|error|problem|crash)/i,
599
- /\bfix(es|ed|ing)?\b/i, // Simple "fix" or "fixed" alone
600
- /\bbug\s*fix/i,
601
- /\bBUG:/i, // Bug comment markers
602
- /\bhotfix\b/i,
603
- /\bpatch(es|ed|ing)?\b/i,
604
- /\bresolv(e|es|ed|ing)\s*(the\s*)?(issue|bug|error)/i,
605
- /\bcorrect(s|ed|ing)?\s*(the\s*)?(bug|issue|error|problem)/i,
606
- /\brepair(s|ed|ing)?\b/i,
607
- /\bhandle\s*(error|exception|null|undefined)/i,
608
- /\bnull\s*check/i,
609
- /\bundefined\s*check/i,
610
- /\btry\s*{\s*.*\s*}\s*catch/i,
611
- /\bif\s*\(\s*!\s*\w+\s*\)/, // Null/undefined guards
612
- /\bwas\s*broken\b/i, // "was broken" indicates fixing
613
- /\bbroken\b.*\bfix/i, // broken...fix pattern
614
- ];
615
- if (fixPatterns.some(p => p.test(diff))) {
616
- return 'fix';
617
- }
618
- // === REFACTOR DETECTION ===
619
- const refactorPatterns = [
620
- /\brefactor(s|ed|ing)?\b/i,
621
- /\brestructur(e|es|ed|ing)\b/i,
622
- /\bclean\s*up\b/i,
623
- /\bsimplif(y|ies|ied|ying)\b/i,
624
- /\brenam(e|es|ed|ing)\b/i,
625
- /\bmov(e|es|ed|ing)\s*(to|from|into)\b/i,
626
- /\bextract(s|ed|ing)?\s*(function|method|class|component)/i,
627
- /\binline(s|d|ing)?\b/i,
628
- /\bdedup(licate)?\b/i,
629
- /\bDRY\b/,
630
- /\breorganiz(e|es|ed|ing)\b/i,
631
- ];
632
- if (refactorPatterns.some(p => p.test(diff))) {
633
- return 'refactor';
634
- }
635
- // If modifications with balanced adds/removes and no new functionality, likely refactor
636
- // UNLESS the change modifies conditional logic (if/while/for/switch conditions),
637
- // which typically alters behavior and should be treated as a fix, not a refactor
638
- if (hasOnlyModifications && !hasNewFunctionality) {
639
- // Check if the diff modifies conditions inside control flow statements.
640
- // When both the removed and added lines contain the SAME control flow keyword
641
- // but with different conditions, it's a behavioral change (fix), not a refactor.
642
- const removedCondition = /^-\s*(if|while|for|switch)\s*\(/m.test(diff);
643
- const addedCondition = /^\+\s*(if|while|for|switch)\s*\(/m.test(diff);
644
- const isConditionChange = removedCondition && addedCondition;
645
- if (isConditionChange) {
646
- return 'fix';
647
- }
648
- // If roughly equal adds and removes, it's likely refactoring
649
- if (addedLines > 0 && removedLines > 0) {
650
- const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
651
- if (ratio > 0.5) { // More strict ratio - must be very balanced
652
- return 'refactor';
653
- }
654
- }
655
- }
656
- // === CHORE DETECTION ===
657
- const chorePatterns = [
658
- /\bdependenc(y|ies)\b/i,
659
- /\bupgrade\b/i,
660
- /\bupdate\s*(version|dep)/i,
661
- /\bbump\b/i,
662
- /\bpackage\.json\b/i,
663
- /\bpackage-lock\.json\b/i,
664
- /\byarn\.lock\b/i,
665
- /\b\.gitignore\b/i,
666
- /\bci\b.*\b(config|setup)\b/i,
667
- /\blint(er|ing)?\b/i,
668
- ];
669
- if (chorePatterns.some(p => p.test(diff)) || chorePatterns.some(p => filePaths.some(f => p.test(f)))) {
670
- return 'chore';
671
- }
672
- // === FALLBACK ===
673
- // If we have more additions than removals, lean towards feat
674
- if (filesAffected.source > 0) {
675
- if (netNewLines > 10) {
676
- return 'feat'; // Significant new code added
677
- }
678
- if (hasOnlyModifications) {
679
- return 'refactor'; // Modifications without clear new functionality
680
- }
681
- return 'feat'; // Default for source changes with new files
682
- }
683
- return 'chore';
684
- }
685
- /**
686
- * Check if diff contains actual logic changes (not just formatting)
687
- */
688
- static hasLogicChanges(diff) {
689
- // Remove formatting-only changes and check if there's real code
690
- const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
691
- !line.startsWith('+++') &&
692
- !line.startsWith('---'));
693
- for (const line of lines) {
694
- const content = line.substring(1).trim();
695
- // Skip empty lines, comments, and whitespace-only
696
- if (content.length === 0 ||
697
- content.startsWith('//') ||
698
- content.startsWith('/*') ||
699
- content.startsWith('*') ||
700
- content === '{' ||
701
- content === '}' ||
702
- content === ';') {
703
- continue;
704
- }
705
- // Has actual code change
706
- return true;
707
- }
708
- return false;
709
- }
710
- /**
711
- * Check if the diff contains ONLY formatting changes (no logic impact).
712
- * Formatting changes include: semicolons, blank lines, indentation, trailing commas, braces.
713
- * Per Conventional Commits, these should be classified as "style".
714
- */
715
- static isFormattingOnlyChange(diff) {
716
- const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
717
- !line.startsWith('+++') &&
718
- !line.startsWith('---'));
719
- if (lines.length === 0) {
720
- return false;
721
- }
722
- // Pair up removed and added lines to detect indentation-only changes
723
- const removed = [];
724
- const added = [];
725
- for (const line of lines) {
726
- const content = line.substring(1);
727
- const trimmed = content.trim();
728
- // Empty / whitespace-only lines are always formatting
729
- if (trimmed.length === 0) {
730
- continue;
731
- }
732
- // Standalone semicolons, braces, trailing commas are formatting
733
- if (trimmed === ';' || trimmed === '{' || trimmed === '}' || trimmed === ',') {
734
- continue;
735
- }
736
- // Collect lines for indentation-only and semicolon-only comparison
737
- if (line.startsWith('-')) {
738
- removed.push(trimmed);
739
- }
740
- else {
741
- added.push(trimmed);
742
- }
743
- }
744
- // If no added/removed content lines remain, it's purely blank line changes
745
- if (removed.length === 0 && added.length === 0) {
746
- return true;
747
- }
748
- // Check if each added line corresponds to a removed line that differs only by
749
- // formatting (trailing semicolons, trailing commas, whitespace)
750
- if (removed.length !== added.length) {
751
- return false;
752
- }
753
- for (let i = 0; i < removed.length; i++) {
754
- // Normalize: strip trailing semicolons, commas, and whitespace
755
- const normalizeFormatting = (s) => s.replace(/[;\s,]+$/g, '').replace(/^\s+/, '');
756
- const normRemoved = normalizeFormatting(removed[i]);
757
- const normAdded = normalizeFormatting(added[i]);
758
- if (normRemoved !== normAdded) {
759
- return false;
760
- }
761
- }
762
- return true;
763
- }
764
- /**
765
- * Generate a description for formatting-only changes
766
- */
767
- static generateFormattingDescription(diff, stagedFiles) {
768
- const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
769
- !line.startsWith('+++') &&
770
- !line.startsWith('---'));
771
- let hasSemicolonChanges = false;
772
- let hasBlankLineChanges = false;
773
- let hasIndentationChanges = false;
774
- let hasBraceChanges = false;
775
- let hasTrailingCommaChanges = false;
776
- const removed = [];
777
- const added = [];
778
- for (const line of lines) {
779
- const trimmed = line.substring(1).trim();
780
- if (trimmed.length === 0) {
781
- hasBlankLineChanges = true;
782
- continue;
783
- }
784
- if (trimmed === ';') {
785
- hasSemicolonChanges = true;
786
- continue;
787
- }
788
- if (trimmed === '{' || trimmed === '}') {
789
- hasBraceChanges = true;
790
- continue;
791
- }
792
- if (trimmed === ',') {
793
- hasTrailingCommaChanges = true;
794
- continue;
795
- }
796
- if (line.startsWith('-')) {
797
- removed.push(trimmed);
798
- }
799
- else {
800
- added.push(trimmed);
801
- }
802
- }
803
- // Check paired lines for semicolon-only or indentation-only diffs
804
- for (let i = 0; i < removed.length && i < added.length; i++) {
805
- const r = removed[i];
806
- const a = added[i];
807
- // If trimmed content is identical, the only difference was indentation
808
- if (r === a) {
809
- hasIndentationChanges = true;
810
- }
811
- // Semicolon difference (one has semicolon, the other doesn't)
812
- else if (r + ';' === a || a + ';' === r) {
813
- hasSemicolonChanges = true;
814
- }
815
- // Trailing comma difference
816
- else if (r + ',' === a || a + ',' === r) {
817
- hasTrailingCommaChanges = true;
818
- }
819
- else {
820
- hasIndentationChanges = true;
821
- }
822
- }
823
- // Build description based on what changed
824
- let description;
825
- if (hasSemicolonChanges && !hasBlankLineChanges && !hasIndentationChanges && !hasBraceChanges && !hasTrailingCommaChanges) {
826
- description = 'add missing semicolons';
827
- }
828
- else if (hasBlankLineChanges && !hasSemicolonChanges && !hasIndentationChanges && !hasBraceChanges && !hasTrailingCommaChanges) {
829
- description = 'remove blank lines';
830
- }
831
- else if (hasIndentationChanges && !hasSemicolonChanges && !hasBlankLineChanges && !hasBraceChanges && !hasTrailingCommaChanges) {
832
- description = 'fix indentation';
833
- }
834
- else if (hasBraceChanges && !hasSemicolonChanges && !hasBlankLineChanges && !hasIndentationChanges && !hasTrailingCommaChanges) {
835
- description = 'fix brace formatting';
836
- }
837
- else if (hasTrailingCommaChanges && !hasSemicolonChanges && !hasBlankLineChanges && !hasIndentationChanges && !hasBraceChanges) {
838
- description = 'fix trailing commas';
839
- }
840
- else {
841
- description = 'fix code formatting';
842
- }
843
- // Append filename for single-file changes
844
- if (stagedFiles.length === 1) {
845
- const fileName = stagedFiles[0].path.split('/').pop() || stagedFiles[0].path;
846
- description += ` in ${fileName}`;
847
- }
848
- return description;
849
- }
850
- /**
851
- * Check if the changes are ONLY non-style JavaScript changes
852
- * (console.log, debug statements, test prints, comments, etc.)
853
- * Returns true if JS files have changes but NO actual styling changes
854
- */
855
- static hasOnlyNonStyleJsChanges(diff, filePaths) {
856
- // Check if all files are JavaScript/TypeScript (not CSS)
857
- const hasOnlyJsFiles = filePaths.every(p => p.endsWith('.js') ||
858
- p.endsWith('.jsx') ||
859
- p.endsWith('.ts') ||
860
- p.endsWith('.tsx') ||
861
- p.endsWith('.mjs') ||
862
- p.endsWith('.cjs'));
863
- if (!hasOnlyJsFiles) {
864
- return false;
865
- }
866
- // Patterns that indicate NON-style changes (debug, logging, test output)
867
- const nonStylePatterns = [
868
- /console\.(log|debug|info|warn|error|trace|dir|table)/,
869
- /debugger\b/,
870
- /logger\.\w+/i,
871
- /debug\s*\(/,
872
- /print\s*\(/,
873
- /console\.assert/,
874
- /console\.time/,
875
- /console\.count/,
876
- /console\.group/,
877
- /process\.stdout/,
878
- /process\.stderr/,
879
- /\.toLog\(/,
880
- /\.log\(/,
881
- /winston\./,
882
- /pino\./,
883
- /bunyan\./,
884
- /log4js\./,
885
- ];
886
- // Get all added/changed lines
887
- const changedLines = diff.split('\n').filter(line => line.startsWith('+') && !line.startsWith('+++'));
888
- // If the changes match non-style patterns, return true
889
- const hasNonStyleChanges = changedLines.some(line => nonStylePatterns.some(pattern => pattern.test(line)));
890
- // Check if there are NO style-related patterns in the JS files
891
- const stylePatterns = [
892
- /styled\s*[.(]/,
893
- /css\s*`/,
894
- /\bstyle\s*=\s*\{\{/,
895
- /className\s*=/,
896
- /\bsx\s*=/,
897
- /theme\./,
898
- /colors\./,
899
- /palette\./,
900
- ];
901
- const hasStylePatterns = changedLines.some(line => stylePatterns.some(pattern => pattern.test(line)));
902
- // Return true only if we have non-style changes AND no style patterns
903
- return hasNonStyleChanges && !hasStylePatterns;
904
- }
905
- /**
906
- * Extract non-comment changed lines from a diff for keyword analysis.
907
- * Filters out comment lines to prevent false positives from descriptive text.
908
- */
909
- static extractNonCommentChanges(diff) {
910
- return diff.split('\n')
911
- .filter(line => (line.startsWith('+') || line.startsWith('-')) &&
912
- !line.startsWith('+++') &&
913
- !line.startsWith('---'))
914
- .map(line => line.substring(1))
915
- .filter(line => {
916
- const trimmed = line.trim();
917
- if (trimmed.length === 0)
918
- return false;
919
- // Filter out comment lines
920
- return !(trimmed.startsWith('//') ||
921
- trimmed.startsWith('/*') ||
922
- trimmed.startsWith('*') ||
923
- trimmed.startsWith('*/') ||
924
- trimmed.startsWith('#') ||
925
- trimmed.startsWith('<!--') ||
926
- trimmed.startsWith('-->') ||
927
- trimmed.startsWith('"""') ||
928
- trimmed.startsWith("'''") ||
929
- /^\/\*\*/.test(trimmed) ||
930
- /^\*\s*@\w+/.test(trimmed));
931
- })
932
- .join('\n');
933
- }
934
- /**
935
- * Check if the diff contains only comment changes (documentation)
936
- * Returns true if ALL changes are comments (no code changes)
937
- */
938
- static isCommentOnlyChange(diff) {
939
- // Get all changed lines (additions and deletions)
940
- const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
941
- !line.startsWith('+++') &&
942
- !line.startsWith('---'));
943
- if (lines.length === 0) {
944
- return false;
945
- }
946
- let hasCommentChanges = false;
947
- for (const line of lines) {
948
- const content = line.substring(1).trim();
949
- // Skip empty lines
950
- if (content.length === 0) {
951
- continue;
952
- }
953
- // Check if line is a comment
954
- const isComment = content.startsWith('//') || // Single-line comment
955
- content.startsWith('/*') || // Multi-line comment start
956
- content.startsWith('*') || // Multi-line comment body
957
- content.startsWith('*/') || // Multi-line comment end
958
- content.startsWith('#') || // Shell/Python/Ruby comments
959
- content.startsWith('<!--') || // HTML comments
960
- content.startsWith('-->') || // HTML comment end
961
- content.startsWith('"""') || // Python docstring
962
- content.startsWith("'''") || // Python docstring
963
- /^\/\*\*/.test(content) || // JSDoc start
964
- /^\*\s*@\w+/.test(content); // JSDoc tag
965
- if (isComment) {
966
- hasCommentChanges = true;
967
- }
968
- else {
969
- // Found a non-comment change - not a comment-only diff
970
- return false;
971
- }
972
- }
973
- return hasCommentChanges;
974
- }
975
- /**
976
- * Perform semantic analysis on the diff to understand the nature of changes
977
- * This provides intent-based understanding rather than line-count metrics
978
- */
979
- static analyzeSemanticChanges(diff, stagedFiles) {
980
- const roleChanges = this.detectRoleChanges(diff, stagedFiles);
981
- const primaryRole = this.determinePrimaryRole(roleChanges);
982
- let primaryIntent = this.determineIntent(diff, stagedFiles, roleChanges);
983
- const affectedElements = this.extractAffectedElements(diff);
984
- const hunkContext = this.extractHunkContext(diff);
985
- // Detect renames - this takes priority for determining intent
986
- const renames = this.detectRenames(diff);
987
- if (renames.length > 0) {
988
- primaryIntent = 'rename';
989
- }
990
- // Generate human-readable descriptions
991
- const intentDescription = this.generateIntentDescription(primaryIntent, primaryRole, roleChanges);
992
- const whatChanged = renames.length > 0
993
- ? `${renames[0].oldName} to ${renames[0].newName}`
994
- : this.generateWhatChanged(roleChanges, affectedElements, stagedFiles, hunkContext);
995
- return {
996
- primaryRole,
997
- primaryIntent,
998
- roleChanges,
999
- intentDescription,
1000
- whatChanged,
1001
- hasMultipleRoles: roleChanges.filter(r => r.significance > 20).length > 1,
1002
- renames: renames.length > 0 ? renames : undefined,
1003
- hunkContext: hunkContext.length > 0 ? hunkContext : undefined,
1004
- };
1005
- }
1006
- /**
1007
- * Detect which roles are affected by the changes and calculate semantic significance
1008
- * Significance is NOT based on line count - it's based on the semantic weight of patterns
1009
- */
1010
- static detectRoleChanges(diff, _stagedFiles) {
1011
- const roleChanges = [];
1012
- // Check each role for matches
1013
- for (const [role, patterns] of Object.entries(ROLE_PATTERNS)) {
1014
- if (role === 'unknown' || patterns.length === 0)
1015
- continue;
1016
- let matchCount = 0;
1017
- let highValueMatches = 0;
1018
- for (const pattern of patterns) {
1019
- pattern.lastIndex = 0;
1020
- const matches = diff.match(pattern);
1021
- if (matches) {
1022
- matchCount += matches.length;
1023
- // Some patterns indicate more significant changes
1024
- if (this.isHighValuePattern(pattern, role)) {
1025
- highValueMatches += matches.length;
1026
- }
1027
- }
1028
- }
1029
- if (matchCount > 0) {
1030
- // Calculate significance based on pattern matches, not line counts
1031
- // High-value patterns contribute more to significance
1032
- const baseSignificance = Math.min(matchCount * 10, 40);
1033
- const highValueBonus = highValueMatches * 15;
1034
- const significance = Math.min(baseSignificance + highValueBonus, 100);
1035
- const intent = this.detectRoleIntent(diff, role);
1036
- const summary = this.generateRoleSummary(role, intent, matchCount);
1037
- roleChanges.push({
1038
- role,
1039
- intent,
1040
- significance,
1041
- summary,
1042
- affectedElements: this.extractElementsForRole(diff, role),
1043
- });
1044
- }
1045
- }
1046
- // Sort by significance (highest first)
1047
- return roleChanges.sort((a, b) => b.significance - a.significance);
1048
- }
1049
- /**
1050
- * Determine if a pattern represents a high-value semantic change
1051
- */
1052
- static isHighValuePattern(pattern, role) {
1053
- const patternStr = pattern.source;
1054
- // High-value patterns for each role
1055
- const highValueIndicators = {
1056
- ui: ['<[A-Z]', 'className', 'onClick', 'onSubmit', 'aria-'],
1057
- logic: ['function', 'class', 'if\\s*\\(', 'switch', 'try\\s*\\{', 'throw'],
1058
- data: ['useState', 'useReducer', 'interface', 'type\\s+\\w+'],
1059
- style: ['styled\\.', 'css`', 'theme\\.'],
1060
- api: ['fetch\\s*\\(', 'axios', 'useQuery', 'useMutation', 'graphql'],
1061
- config: ['process\\.env', 'CONFIG\\.', 'export\\s+(const|let)\\s+[A-Z_]+'],
1062
- test: ['describe\\s*\\(', 'it\\s*\\(', 'test\\s*\\(', 'expect\\s*\\('],
1063
- unknown: [],
1064
- };
1065
- return highValueIndicators[role].some(indicator => patternStr.includes(indicator));
1066
- }
1067
- /**
1068
- * Detect the intent for changes in a specific role
1069
- */
1070
- static detectRoleIntent(diff, _role) {
1071
- // Check for intent patterns (role context reserved for future use)
1072
- const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
1073
- const removedLines = (diff.match(/^-[^-]/gm) || []).length;
1074
- // Check for add patterns in this role's context
1075
- for (const pattern of INTENT_PATTERNS.add) {
1076
- if (pattern.test(diff)) {
1077
- // If adding new constructs, it's an 'add' intent
1078
- return 'add';
1079
- }
1080
- }
1081
- // Check for remove patterns
1082
- for (const pattern of INTENT_PATTERNS.remove) {
1083
- if (pattern.test(diff)) {
1084
- return 'remove';
1085
- }
1086
- }
1087
- // Check for fix patterns
1088
- for (const pattern of INTENT_PATTERNS.fix) {
1089
- if (pattern.test(diff)) {
1090
- return 'fix';
1091
- }
1092
- }
1093
- // Check for enhance patterns
1094
- for (const pattern of INTENT_PATTERNS.enhance) {
1095
- if (pattern.test(diff)) {
1096
- return 'enhance';
1097
- }
1098
- }
1099
- // Determine based on add/remove ratio
1100
- if (addedLines > 0 && removedLines === 0) {
1101
- return 'add';
1102
- }
1103
- else if (removedLines > 0 && addedLines === 0) {
1104
- return 'remove';
1105
- }
1106
- else if (addedLines > 0 && removedLines > 0) {
1107
- const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
1108
- if (ratio > 0.5) {
1109
- return 'refactor'; // Balanced changes suggest refactoring
1110
- }
1111
- return addedLines > removedLines ? 'add' : 'modify';
1112
- }
1113
- return 'modify';
1114
- }
1115
- /**
1116
- * Determine the primary role from all detected role changes
1117
- */
1118
- static determinePrimaryRole(roleChanges) {
1119
- if (roleChanges.length === 0) {
1120
- return 'unknown';
1121
- }
1122
- // The role with highest significance is primary
1123
- // But if multiple roles have similar significance, prefer more specific ones
1124
- const topRole = roleChanges[0];
1125
- // If there's a close second that's more specific, consider it
1126
- if (roleChanges.length > 1) {
1127
- const secondRole = roleChanges[1];
1128
- const significanceDiff = topRole.significance - secondRole.significance;
1129
- // If within 15 points and second is more specific (api > logic > ui)
1130
- if (significanceDiff <= 15) {
1131
- const specificityOrder = ['api', 'data', 'style', 'logic', 'ui', 'config', 'test', 'unknown'];
1132
- const topIndex = specificityOrder.indexOf(topRole.role);
1133
- const secondIndex = specificityOrder.indexOf(secondRole.role);
1134
- if (secondIndex < topIndex) {
1135
- return secondRole.role;
1136
- }
1137
- }
1138
- }
1139
- return topRole.role;
1140
- }
1141
- /**
1142
- * Determine the overall intent of the changes
1143
- */
1144
- static determineIntent(diff, stagedFiles, roleChanges) {
1145
- // Check file statuses first
1146
- const hasOnlyAdded = stagedFiles.every(f => f.status === 'A');
1147
- const hasOnlyDeleted = stagedFiles.every(f => f.status === 'D');
1148
- const hasOnlyModified = stagedFiles.every(f => f.status === 'M');
1149
- if (hasOnlyAdded) {
1150
- return 'add';
1151
- }
1152
- if (hasOnlyDeleted) {
1153
- return 'remove';
1154
- }
1155
- // If we have role changes, use the most significant role's intent
1156
- if (roleChanges.length > 0) {
1157
- const primaryRoleChange = roleChanges[0];
1158
- // Special case: if the primary intent is 'fix' and we see validation patterns
1159
- // even in non-modified files, treat it as a fix
1160
- if (hasOnlyModified && primaryRoleChange.intent === 'fix') {
1161
- return 'fix';
1162
- }
1163
- // If we're enhancing (useMemo, useCallback, etc.), that takes precedence
1164
- if (roleChanges.some(r => r.intent === 'enhance')) {
1165
- return 'enhance';
1166
- }
1167
- return primaryRoleChange.intent;
1168
- }
1169
- // Fallback to diff analysis
1170
- const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
1171
- const removedLines = (diff.match(/^-[^-]/gm) || []).length;
1172
- if (addedLines > 0 && removedLines === 0) {
1173
- return 'add';
1174
- }
1175
- if (removedLines > 0 && addedLines === 0) {
1176
- return 'remove';
1177
- }
1178
- const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
1179
- if (ratio > 0.6) {
1180
- return 'refactor';
1181
- }
1182
- return 'modify';
1183
- }
1184
- /**
1185
- * Extract affected element names (components, functions, etc.) from the diff
1186
- */
1187
- static extractAffectedElements(diff) {
1188
- const elements = [];
1189
- // Helper to add unique element names
1190
- const addUnique = (name) => {
1191
- if (!elements.includes(name)) {
1192
- elements.push(name);
1193
- }
1194
- };
1195
- // Match both added (+) and modified (-) top-level declarations.
1196
- // For fix/refactor commits, the declaration line often appears as a removed line
1197
- // (the old version) or only in the - side of the diff. Using [+-] ensures we
1198
- // capture the affected function/class/component name regardless of whether
1199
- // the declaration itself was added, removed, or modified.
1200
- // (?!\s) after [+-] rejects indented lines (local variables inside function bodies)
1201
- // Extract component names (PascalCase function/const declarations)
1202
- const componentFuncMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+([A-Z][a-zA-Z0-9]*)/gm);
1203
- if (componentFuncMatches) {
1204
- for (const match of componentFuncMatches) {
1205
- const nameMatch = match.match(/function\s+([A-Z][a-zA-Z0-9]*)/);
1206
- if (nameMatch)
1207
- addUnique(nameMatch[1]);
1208
- }
1209
- }
1210
- const componentConstMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?const\s+([A-Z][a-zA-Z0-9]*)\s*=\s*(?:async\s+)?(?:function|\()/gm);
1211
- if (componentConstMatches) {
1212
- for (const match of componentConstMatches) {
1213
- const nameMatch = match.match(/const\s+([A-Z][a-zA-Z0-9]*)/);
1214
- if (nameMatch)
1215
- addUnique(nameMatch[1]);
1216
- }
1217
- }
1218
- // Extract function names (camelCase declarations)
1219
- const funcKeywordMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+([a-z][a-zA-Z0-9]*)\s*\(/gm);
1220
- if (funcKeywordMatches) {
1221
- for (const match of funcKeywordMatches) {
1222
- const nameMatch = match.match(/function\s+([a-z][a-zA-Z0-9]*)/);
1223
- if (nameMatch)
1224
- addUnique(nameMatch[1]);
1225
- }
1226
- }
1227
- // const/let arrow or function expression: const foo = (, export const foo = async (
1228
- const funcConstMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?const\s+([a-z][a-zA-Z0-9]*)\s*=\s*(?:async\s+)?(?:function|\()/gm);
1229
- if (funcConstMatches) {
1230
- for (const match of funcConstMatches) {
1231
- const nameMatch = match.match(/const\s+([a-z][a-zA-Z0-9]*)/);
1232
- if (nameMatch)
1233
- addUnique(nameMatch[1]);
1234
- }
1235
- }
1236
- // Extract interface/type names
1237
- const typeMatches = diff.match(/^[+-](?!\s)(?:export\s+)?(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/gm);
1238
- if (typeMatches) {
1239
- for (const match of typeMatches) {
1240
- const nameMatch = match.match(/(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/);
1241
- if (nameMatch)
1242
- addUnique(nameMatch[1]);
1243
- }
1244
- }
1245
- // Extract class names
1246
- const classMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Z][a-zA-Z0-9]*)/gm);
1247
- if (classMatches) {
1248
- for (const match of classMatches) {
1249
- const nameMatch = match.match(/class\s+([A-Z][a-zA-Z0-9]*)/);
1250
- if (nameMatch)
1251
- addUnique(nameMatch[1]);
1252
- }
1253
- }
1254
- return elements.slice(0, 5); // Limit to top 5 elements
1255
- }
1256
- /**
1257
- * Extract function/class/method names from git diff by analyzing both hunk headers
1258
- * and the actual context lines around changes.
1259
- *
1260
- * This method prioritizes function declarations found in context lines near the
1261
- * actual changes, which is more accurate than just using hunk headers (which may
1262
- * show preceding functions instead of the one being modified).
1263
- */
1264
- static extractHunkContext(diff) {
1265
- const names = [];
1266
- // Split diff into hunks
1267
- const hunks = diff.split(/^@@/gm).slice(1); // Skip header before first @@
1268
- for (const hunk of hunks) {
1269
- const lines = hunk.split('\n');
1270
- let lastFunctionName = null;
1271
- let foundChange = false;
1272
- // Skip first line (hunk header like " -425,7 +425,7 @@ function normalize...")
1273
- for (let i = 1; i < lines.length; i++) {
1274
- const line = lines[i];
1275
- // Check if this is a context line (starts with space) or added line (starts with +)
1276
- // Context lines show surrounding code that hasn't changed
1277
- if (line.startsWith(' ') || (line.startsWith('+') && !line.startsWith('+++'))) {
1278
- const extractedName = this.extractNameFromLine(line);
1279
- if (extractedName) {
1280
- lastFunctionName = extractedName;
1281
- }
1282
- }
1283
- // If we find an actual change (+/-) and we have a function name, record it
1284
- if ((line.startsWith('+') || line.startsWith('-')) &&
1285
- !line.startsWith('+++') && !line.startsWith('---') &&
1286
- lastFunctionName &&
1287
- !foundChange) {
1288
- if (!names.includes(lastFunctionName)) {
1289
- names.push(lastFunctionName);
1290
- }
1291
- foundChange = true; // Only record once per hunk
1292
- }
1293
- }
1294
- // Fallback: if no function found in context lines, try hunk header
1295
- if (!foundChange && lines[0]) {
1296
- const headerMatch = lines[0].match(/^@?\s+[^@]+@@\s+(.+)$/);
1297
- if (headerMatch) {
1298
- const name = this.extractNameFromLine(headerMatch[1]);
1299
- if (name && !names.includes(name)) {
1300
- names.push(name);
1301
- }
1302
- }
1303
- }
1304
- }
1305
- return names.slice(0, 5);
1306
- }
1307
- /**
1308
- * Extract a function/class/method name from a single line of code
1309
- */
1310
- static extractNameFromLine(line) {
1311
- // Remove leading diff markers and whitespace
1312
- const cleanLine = line.replace(/^[+\-\s@]*/, '').trim();
1313
- // Skip lines that are clearly control flow (for, if, while, etc.)
1314
- if (/^\s*(if|else|for|while|do|switch|catch|return|throw)\s*\(/.test(cleanLine)) {
1315
- return null;
1316
- }
1317
- // function declarations: function foo(, async function foo(, export function foo(
1318
- const funcMatch = cleanLine.match(/(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
1319
- if (funcMatch)
1320
- return funcMatch[1];
1321
- // const/let/var arrow or function expression: const foo = (, export const foo = async (
1322
- // Only match if it looks like a function (has = followed by function keyword, arrow, or paren)
1323
- 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_$])/);
1324
- if (constMatch)
1325
- return constMatch[1];
1326
- // class declarations: class Foo {, export class Foo extends Bar {
1327
- const classMatch = cleanLine.match(/(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Z][a-zA-Z0-9]*)/);
1328
- if (classMatch)
1329
- return classMatch[1];
1330
- // class method: methodName(, async methodName(, private methodName(, static async methodName(
1331
- // Exclude control flow keywords (if, for, while, etc.) which share the `keyword(` shape
1332
- 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*\(/);
1333
- if (methodMatch)
1334
- return methodMatch[1];
1335
- // interface/type declarations
1336
- const typeMatch = cleanLine.match(/(?:export\s+)?(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/);
1337
- if (typeMatch)
1338
- return typeMatch[1];
1339
- return null;
1340
- }
1341
- /**
1342
- * Detect function/class/type renames by comparing removed and added names
1343
- * Returns an array of { oldName, newName } objects
1344
- */
1345
- static detectRenames(diff) {
1346
- const renames = [];
1347
- // Patterns for extracting function/class/type names from removed and added lines
1348
- const patterns = [
1349
- // Function declarations: function name( or const name = or const name: Type =
1350
- { regex: /^[-+].*(?:function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm, type: 'function' },
1351
- { regex: /^[-+].*(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[=:]/gm, type: 'function' },
1352
- // Class declarations
1353
- { regex: /^[-+].*class\s+([A-Z][a-zA-Z0-9]*)/gm, type: 'class' },
1354
- // Interface declarations
1355
- { regex: /^[-+].*interface\s+([A-Z][a-zA-Z0-9]*)/gm, type: 'interface' },
1356
- // Type declarations
1357
- { regex: /^[-+].*type\s+([A-Z][a-zA-Z0-9]*)\s*=/gm, type: 'type' },
1358
- ];
1359
- for (const { regex, type } of patterns) {
1360
- const removed = [];
1361
- const added = [];
1362
- let match;
1363
- regex.lastIndex = 0;
1364
- while ((match = regex.exec(diff)) !== null) {
1365
- const line = match[0];
1366
- const name = match[1];
1367
- if (line.startsWith('-') && !line.startsWith('---')) {
1368
- removed.push(name);
1369
- }
1370
- else if (line.startsWith('+') && !line.startsWith('+++')) {
1371
- added.push(name);
1372
- }
1373
- }
1374
- // If we have exactly one removed and one added of the same type,
1375
- // and they're different names, it's likely a rename
1376
- if (removed.length === 1 && added.length === 1 && removed[0] !== added[0]) {
1377
- renames.push({ oldName: removed[0], newName: added[0], type });
1378
- }
1379
- }
1380
- return renames;
1381
- }
1382
- /**
1383
- * Extract affected elements for a specific role
1384
- */
1385
- static extractElementsForRole(diff, role) {
1386
- const elements = [];
1387
- switch (role) {
1388
- case 'ui':
1389
- // Extract JSX component names being used
1390
- const jsxMatches = diff.match(/^\+.*<([A-Z][a-zA-Z0-9]*)/gm);
1391
- if (jsxMatches) {
1392
- for (const match of jsxMatches) {
1393
- const nameMatch = match.match(/<([A-Z][a-zA-Z0-9]*)/);
1394
- if (nameMatch && !elements.includes(nameMatch[1])) {
1395
- elements.push(nameMatch[1]);
1396
- }
1397
- }
1398
- }
1399
- break;
1400
- case 'data':
1401
- // Extract state variable names
1402
- const stateMatches = diff.match(/^\+.*const\s+\[([a-z][a-zA-Z0-9]*),/gm);
1403
- if (stateMatches) {
1404
- for (const match of stateMatches) {
1405
- const nameMatch = match.match(/const\s+\[([a-z][a-zA-Z0-9]*)/);
1406
- if (nameMatch) {
1407
- elements.push(nameMatch[1]);
1408
- }
1409
- }
1410
- }
1411
- break;
1412
- case 'api':
1413
- // Extract API endpoints
1414
- const apiMatches = diff.match(/['"`]\/api\/[^'"`]+['"`]/g);
1415
- if (apiMatches) {
1416
- elements.push(...apiMatches.map(m => m.replace(/['"`]/g, '')));
1417
- }
1418
- break;
1419
- default:
1420
- // Use generic extraction
1421
- return this.extractAffectedElements(diff).slice(0, 3);
1422
- }
1423
- return elements.slice(0, 3);
1424
- }
1425
- /**
1426
- * Generate a human-readable summary for a role change
1427
- */
1428
- static generateRoleSummary(role, intent, matchCount) {
1429
- const roleDesc = ROLE_DESCRIPTIONS[role];
1430
- const intentVerb = INTENT_VERBS[intent].past;
1431
- if (matchCount === 1) {
1432
- return `${intentVerb} ${roleDesc}`;
1433
- }
1434
- return `${intentVerb} ${roleDesc} (${matchCount} changes)`;
1435
- }
1436
- /**
1437
- * Generate the WHY description for the commit
1438
- */
1439
- static generateIntentDescription(intent, role, roleChanges) {
1440
- const roleDesc = ROLE_DESCRIPTIONS[role];
1441
- switch (intent) {
1442
- case 'add':
1443
- return `to add new ${roleDesc}`;
1444
- case 'fix':
1445
- return `to fix ${roleDesc} issues`;
1446
- case 'refactor':
1447
- return `to improve ${roleDesc} structure`;
1448
- case 'enhance':
1449
- return `to optimize ${roleDesc} performance`;
1450
- case 'remove':
1451
- return `to remove unused ${roleDesc}`;
1452
- case 'modify':
1453
- default:
1454
- if (roleChanges.length > 1) {
1455
- return `to update ${roleDesc} and related code`;
1456
- }
1457
- return `to update ${roleDesc}`;
1458
- }
1459
- }
1460
- /**
1461
- * Generate the WHAT changed description
1462
- */
1463
- static generateWhatChanged(roleChanges, affectedElements, stagedFiles, hunkContext) {
1464
- // If we have specific elements from declarations, use them
1465
- if (affectedElements.length > 0) {
1466
- if (affectedElements.length === 1) {
1467
- return affectedElements[0];
1468
- }
1469
- if (affectedElements.length <= 3) {
1470
- return affectedElements.join(', ');
1471
- }
1472
- return `${affectedElements.slice(0, 2).join(', ')} and ${affectedElements.length - 2} more`;
1473
- }
1474
- // Fall back to hunk context (function/class names from @@ headers)
1475
- // This is especially useful for fix/refactor where changes are inside
1476
- // function bodies and the declaration line itself isn't in the diff
1477
- if (hunkContext && hunkContext.length > 0) {
1478
- if (hunkContext.length === 1) {
1479
- return hunkContext[0];
1480
- }
1481
- if (hunkContext.length <= 3) {
1482
- return hunkContext.join(', ');
1483
- }
1484
- return `${hunkContext.slice(0, 2).join(', ')} and ${hunkContext.length - 2} more`;
1485
- }
1486
- // Fall back to role-based description
1487
- if (roleChanges.length > 0) {
1488
- const primaryRole = roleChanges[0];
1489
- if (primaryRole.affectedElements.length > 0) {
1490
- return primaryRole.affectedElements[0];
1491
- }
1492
- return ROLE_DESCRIPTIONS[primaryRole.role];
1493
- }
1494
- // Fall back to file names
1495
- if (stagedFiles.length === 1) {
1496
- const parts = stagedFiles[0].path.split('/');
1497
- return parts[parts.length - 1].replace(/\.\w+$/, '');
1498
- }
1499
- return 'code';
1500
- }
1501
- /**
1502
- * Generate a description from AST analysis results.
1503
- * Produces specific, human-readable descriptions based on structural signals.
1504
- * Returns empty string if AST data is insufficient for a good description.
1505
- */
1506
- static generateASTDescription(astResult, commitType) {
1507
- const { declarations, signals } = astResult;
1508
- // Handle renames โ€” most specific and readable
1509
- if (declarations.renamed.length > 0) {
1510
- const r = declarations.renamed[0];
1511
- if (declarations.renamed.length === 1) {
1512
- return `rename ${r.oldName} to ${r.newName}`;
1513
- }
1514
- return `rename ${r.oldName} to ${r.newName} and ${declarations.renamed.length - 1} more`;
1515
- }
1516
- // Handle removed exported declarations (breaking refactor)
1517
- const removedExported = declarations.removed.filter(d => d.exported);
1518
- if (removedExported.length > 0) {
1519
- if (removedExported.length === 1) {
1520
- return `remove ${removedExported[0].name} export`;
1521
- }
1522
- return `remove ${removedExported[0].name} and ${removedExported.length - 1} more exports`;
1523
- }
1524
- // Handle breaking signature changes
1525
- const sigChangeSignals = signals.filter(s => s.signal === 'changed-exported-sig');
1526
- if (sigChangeSignals.length > 0) {
1527
- const nameMatch = sigChangeSignals[0].detail.match(/on '([^']+)'/);
1528
- if (nameMatch) {
1529
- return `change ${nameMatch[1]} signature`;
1530
- }
1531
- }
1532
- // Handle new declarations (feat)
1533
- if (commitType === 'feat' && declarations.added.length > 0) {
1534
- const exported = declarations.added.filter(d => d.exported);
1535
- const target = exported.length > 0 ? exported : declarations.added;
1536
- if (target.length === 1) {
1537
- return `add ${target[0].name} ${target[0].kind}`;
1538
- }
1539
- if (target.length === 2) {
1540
- return `add ${target[0].name} and ${target[1].name}`;
1541
- }
1542
- return `add ${target[0].name}, ${target[1].name} and ${target.length - 2} more`;
1543
- }
1544
- // Handle fix signals (error handling, guards)
1545
- if (commitType === 'fix') {
1546
- const errorSignals = signals.filter(s => s.signal === 'error-handling-change');
1547
- if (errorSignals.length > 0) {
1548
- const nameMatch = errorSignals[0].detail.match(/in '([^']+)'/);
1549
- if (nameMatch) {
1550
- return `handle errors in ${nameMatch[1]}`;
1551
- }
1552
- }
1553
- const guardSignals = signals.filter(s => s.signal === 'guard-added');
1554
- if (guardSignals.length > 0) {
1555
- const nameMatch = guardSignals[0].detail.match(/in '([^']+)'/);
1556
- if (nameMatch) {
1557
- return `add validation to ${nameMatch[1]}`;
1558
- }
1559
- }
1560
- // Literal-only changes
1561
- const literalSignals = signals.filter(s => s.signal === 'literal-only-change');
1562
- if (literalSignals.length > 0) {
1563
- const nameMatch = literalSignals[0].detail.match(/in '([^']+)'/);
1564
- if (nameMatch) {
1565
- return `fix values in ${nameMatch[1]}`;
1566
- }
1567
- }
1568
- }
1569
- // Handle perf signals
1570
- if (commitType === 'perf') {
1571
- const perfSignals = signals.filter(s => s.signal === 'perf-pattern');
1572
- if (perfSignals.length > 0) {
1573
- const nameMatch = perfSignals[0].detail.match(/in '([^']+)'/);
1574
- if (nameMatch) {
1575
- return `optimize ${nameMatch[1]} performance`;
1576
- }
1577
- }
1578
- }
1579
- // Handle refactor signals (control flow, modified)
1580
- if (commitType === 'refactor') {
1581
- const controlFlowSignals = signals.filter(s => s.signal === 'control-flow-restructured');
1582
- if (controlFlowSignals.length > 0) {
1583
- const nameMatch = controlFlowSignals[0].detail.match(/in '([^']+)'/);
1584
- if (nameMatch) {
1585
- return `simplify ${nameMatch[1]} control flow`;
1586
- }
1587
- }
1588
- if (declarations.modified.length > 0) {
1589
- const names = declarations.modified.map(m => m.name);
1590
- if (names.length === 1) {
1591
- return `refactor ${names[0]}`;
1592
- }
1593
- if (names.length === 2) {
1594
- return `refactor ${names[0]} and ${names[1]}`;
1595
- }
1596
- return `refactor ${names[0]}, ${names[1]} and ${names.length - 2} more`;
1597
- }
1598
- }
1599
- // Generic fallback using affectedElements
1600
- if (astResult.affectedElements.length > 0) {
1601
- const verb = commitType === 'feat' ? 'add' :
1602
- commitType === 'fix' ? 'fix' :
1603
- commitType === 'refactor' ? 'refactor' :
1604
- commitType === 'perf' ? 'optimize' : 'update';
1605
- const els = astResult.affectedElements;
1606
- if (els.length === 1) {
1607
- return `${verb} ${els[0]}`;
1608
- }
1609
- if (els.length === 2) {
1610
- return `${verb} ${els[0]} and ${els[1]}`;
1611
- }
1612
- return `${verb} ${els[0]}, ${els[1]} and ${els.length - 2} more`;
1613
- }
1614
- return '';
1615
- }
1616
- /**
1617
- * Generate a descriptive commit message
1618
- * Uses AST analysis for specific descriptions when available,
1619
- * falls back to semantic analysis and regex-based descriptions.
1620
- */
1621
- static generateDescription(filesAffected, fileStatuses, stagedFiles, diff, semanticAnalysis, astResult, commitType) {
1622
- // Check for comment-only changes (documentation in source files)
1623
- if (this.isCommentOnlyChange(diff)) {
1624
- const fileName = stagedFiles.length === 1 ? this.getFileName(stagedFiles[0].path) : null;
1625
- // Detect type of comments being added
1626
- const hasJSDoc = /^\+\s*\/\*\*/.test(diff) || /^\+\s*\*\s*@\w+/.test(diff);
1627
- const hasFunctionComments = /^\+\s*\/\/.*\b(function|method|param|return|arg)/i.test(diff);
1628
- if (hasJSDoc) {
1629
- return fileName ? `add JSDoc comments to ${fileName}` : 'add JSDoc documentation';
1630
- }
1631
- else if (hasFunctionComments) {
1632
- return fileName ? `add function documentation to ${fileName}` : 'add function documentation';
1633
- }
1634
- else {
1635
- return fileName ? `add comments to ${fileName}` : 'add code comments';
1636
- }
1637
- }
1638
- // Check for formatting-only changes (semicolons, whitespace, etc.)
1639
- if (this.isFormattingOnlyChange(diff)) {
1640
- return this.generateFormattingDescription(diff, stagedFiles);
1641
- }
1642
- // Use AST analysis for specific, human-readable descriptions when available
1643
- // AST descriptions are preferred because they name exact functions/classes
1644
- if (astResult && commitType) {
1645
- const astMaxWeight = astResult.signals.length > 0
1646
- ? Math.max(...astResult.signals.map(s => s.weight), 0)
1647
- : 0;
1648
- if (astMaxWeight > 1 || astResult.declarations.renamed.length > 0) {
1649
- const astDesc = this.generateASTDescription(astResult, commitType);
1650
- if (astDesc) {
1651
- return astDesc;
1652
- }
1653
- }
1654
- }
1655
- // Use semantic analysis for intent-based descriptions when available
1656
- if (semanticAnalysis && semanticAnalysis.roleChanges.length > 0) {
1657
- return this.generateSemanticDescription(semanticAnalysis, stagedFiles);
1658
- }
1659
- // Single file changes (fallback)
1660
- if (stagedFiles.length === 1) {
1661
- const file = stagedFiles[0];
1662
- const fileName = this.getFileName(file.path);
1663
- if (file.status === 'A') {
1664
- return `add ${fileName}`;
1665
- }
1666
- else if (file.status === 'D') {
1667
- return `remove ${fileName}`;
1668
- }
1669
- else if (file.status === 'M') {
1670
- return `update ${fileName}`;
1671
- }
1672
- else if (file.status === 'R') {
1673
- return `rename ${fileName}`;
1674
- }
1675
- }
1676
- // Multiple files of the same type
1677
- if (filesAffected.test > 0 && filesAffected.source === 0) {
1678
- return `update test files`;
1679
- }
1680
- if (filesAffected.docs > 0 && filesAffected.source === 0) {
1681
- return `update documentation`;
1682
- }
1683
- if (filesAffected.config > 0 && filesAffected.source === 0) {
1684
- return `update configuration`;
1685
- }
1686
- // Mixed changes - try to be descriptive
1687
- const parts = [];
1688
- if (fileStatuses.added > 0) {
1689
- parts.push(`add ${fileStatuses.added} file${fileStatuses.added > 1 ? 's' : ''}`);
1690
- }
1691
- if (fileStatuses.modified > 0) {
1692
- if (parts.length === 0) {
1693
- parts.push(`update ${fileStatuses.modified} file${fileStatuses.modified > 1 ? 's' : ''}`);
1694
- }
1695
- }
1696
- if (fileStatuses.deleted > 0) {
1697
- parts.push(`remove ${fileStatuses.deleted} file${fileStatuses.deleted > 1 ? 's' : ''}`);
1698
- }
1699
- if (parts.length > 0) {
1700
- return parts.join(' and ');
1701
- }
1702
- // Fallback
1703
- return `update ${stagedFiles.length} file${stagedFiles.length > 1 ? 's' : ''}`;
1704
- }
1705
- /**
1706
- * Generate description based on semantic analysis
1707
- * Creates intent-based messages like "update UserProfile validation logic"
1708
- */
1709
- static generateSemanticDescription(semantic, stagedFiles) {
1710
- const intent = semantic.primaryIntent;
1711
- const role = semantic.primaryRole;
1712
- const verb = INTENT_VERBS[intent].present;
1713
- const roleDesc = ROLE_DESCRIPTIONS[role];
1714
- const hunkContext = semantic.hunkContext;
1715
- // Handle renames specially - use "rename X to Y" format
1716
- if (intent === 'rename' && semantic.renames && semantic.renames.length > 0) {
1717
- const rename = semantic.renames[0];
1718
- return `${verb} ${rename.oldName} to ${rename.newName}`;
1719
- }
1720
- // If we have specific elements affected, include them
1721
- const whatChanged = semantic.whatChanged;
1722
- // For single file with identified elements
1723
- if (stagedFiles.length === 1 && whatChanged && whatChanged !== 'code') {
1724
- // Check if whatChanged is a component/function name
1725
- if (/^[A-Z][a-zA-Z0-9]*$/.test(whatChanged)) {
1726
- // It's a component name
1727
- return `${verb} ${whatChanged} ${roleDesc}`;
1728
- }
1729
- if (/^[a-z][a-zA-Z0-9]*$/.test(whatChanged)) {
1730
- // It's a function name
1731
- return `${verb} ${whatChanged} ${roleDesc}`;
1732
- }
1733
- // Multiple elements or descriptive text
1734
- return `${verb} ${whatChanged}`;
1735
- }
1736
- // For multiple files with identified elements
1737
- if (stagedFiles.length > 1 && whatChanged && whatChanged !== 'code') {
1738
- // whatChanged contains specific element names (from extractAffectedElements or hunkContext)
1739
- if (/^[a-zA-Z_$][a-zA-Z0-9_$,\s]*$/.test(whatChanged) && !Object.values(ROLE_DESCRIPTIONS).includes(whatChanged)) {
1740
- return `${verb} ${whatChanged} ${roleDesc}`;
1741
- }
1742
- }
1743
- // When whatChanged is generic but hunk context provides specific function/class names
1744
- if (hunkContext && hunkContext.length > 0) {
1745
- const contextNames = hunkContext.length <= 2
1746
- ? hunkContext.join(' and ')
1747
- : `${hunkContext[0]} and ${hunkContext.length - 1} more`;
1748
- return `${verb} ${contextNames} ${roleDesc}`;
1749
- }
1750
- // For multiple files or when we only have role info
1751
- if (semantic.hasMultipleRoles) {
1752
- // Changes span multiple concerns
1753
- const roles = semantic.roleChanges
1754
- .filter(r => r.significance > 20)
1755
- .slice(0, 2)
1756
- .map(r => ROLE_DESCRIPTIONS[r.role]);
1757
- if (roles.length > 1) {
1758
- return `${verb} ${roles.join(' and ')}`;
1759
- }
1760
- }
1761
- // Single role with file context
1762
- if (stagedFiles.length === 1) {
1763
- const fileName = this.getFileName(stagedFiles[0].path).replace(/\.\w+$/, '');
1764
- return `${verb} ${fileName} ${roleDesc}`;
1765
- }
1766
- // Multiple files, same role
1767
- return `${verb} ${roleDesc}`;
1768
- }
1769
- /**
1770
- * Determine scope from file paths
1771
- */
1772
- static determineScope(stagedFiles) {
1773
- if (stagedFiles.length === 0)
1774
- return undefined;
1775
- const config = configService_1.ConfigService.getConfig();
1776
- const paths = stagedFiles.map((f) => f.path);
1777
- // Check config-based scope mappings first
1778
- if (config.scopes && config.scopes.length > 0) {
1779
- for (const mapping of config.scopes) {
1780
- const matchingFiles = paths.filter((p) => p.includes(mapping.pattern));
1781
- if (matchingFiles.length === paths.length) {
1782
- return mapping.scope;
1783
- }
1784
- }
1785
- // If most files match a pattern, use that scope
1786
- for (const mapping of config.scopes) {
1787
- const matchingFiles = paths.filter((p) => p.includes(mapping.pattern));
1788
- if (matchingFiles.length > paths.length / 2) {
1789
- return mapping.scope;
1790
- }
1791
- }
1792
- }
1793
- // Fallback to default heuristic
1794
- const firstPath = paths[0];
1795
- const parts = firstPath.split('/');
1796
- if (parts.length > 1) {
1797
- const potentialScope = parts[0];
1798
- // Common scope names to look for
1799
- const validScopes = [
1800
- 'api',
1801
- 'ui',
1802
- 'auth',
1803
- 'db',
1804
- 'core',
1805
- 'utils',
1806
- 'components',
1807
- 'services',
1808
- ];
1809
- if (validScopes.includes(potentialScope.toLowerCase())) {
1810
- return potentialScope;
1811
- }
1812
- }
1813
- return undefined;
1814
- }
1815
165
  /**
1816
166
  * Extract file name from path
1817
167
  */
@@ -1819,123 +169,6 @@ class AnalyzerService {
1819
169
  const parts = path.split('/');
1820
170
  return parts[parts.length - 1];
1821
171
  }
1822
- /**
1823
- * Generate commit body for larger changes
1824
- * Includes semantic role context when available
1825
- */
1826
- static generateBody(analysis) {
1827
- const lines = [];
1828
- const semantic = analysis.semanticAnalysis;
1829
- // Include semantic context (WHY the change was made) for multi-role changes
1830
- if (semantic && semantic.hasMultipleRoles) {
1831
- lines.push('Changes:');
1832
- for (const roleChange of semantic.roleChanges.filter(r => r.significance > 15).slice(0, 4)) {
1833
- const elements = roleChange.affectedElements.length > 0
1834
- ? ` (${roleChange.affectedElements.slice(0, 2).join(', ')})`
1835
- : '';
1836
- lines.push(`- ${roleChange.summary}${elements}`);
1837
- }
1838
- lines.push('');
1839
- }
1840
- // Only add file details for truly large changes
1841
- if (!analysis.isLargeChange && lines.length === 0) {
1842
- return undefined;
1843
- }
1844
- // Add file change details for large changes
1845
- if (analysis.isLargeChange) {
1846
- if (lines.length > 0) {
1847
- lines.push('Files:');
1848
- }
1849
- if (analysis.fileChanges.added.length > 0) {
1850
- lines.push(`- Add ${analysis.fileChanges.added.join(', ')}`);
1851
- }
1852
- if (analysis.fileChanges.modified.length > 0) {
1853
- const files = analysis.fileChanges.modified.slice(0, 5);
1854
- const suffix = analysis.fileChanges.modified.length > 5
1855
- ? ` and ${analysis.fileChanges.modified.length - 5} more`
1856
- : '';
1857
- lines.push(`- Update ${files.join(', ')}${suffix}`);
1858
- }
1859
- if (analysis.fileChanges.deleted.length > 0) {
1860
- lines.push(`- Remove ${analysis.fileChanges.deleted.join(', ')}`);
1861
- }
1862
- if (analysis.fileChanges.renamed.length > 0) {
1863
- lines.push(`- Rename ${analysis.fileChanges.renamed.join(', ')}`);
1864
- }
1865
- }
1866
- return lines.length > 0 ? lines.join('\n') : undefined;
1867
- }
1868
- /**
1869
- * Apply a template to build the commit message subject line
1870
- */
1871
- static applyTemplate(template, type, scope, description, includeEmoji, isBreaking) {
1872
- const emoji = includeEmoji ? COMMIT_EMOJIS[type] : '';
1873
- const breakingIndicator = isBreaking ? '!' : '';
1874
- let result = template
1875
- .replace('{emoji}', emoji)
1876
- .replace('{type}', type + breakingIndicator)
1877
- .replace('{description}', description);
1878
- // Handle scope - if no scope, use noScope template or remove scope placeholder
1879
- if (scope) {
1880
- result = result.replace('{scope}', scope);
1881
- }
1882
- else {
1883
- // Remove scope and parentheses if no scope
1884
- result = result.replace('({scope})', '').replace('{scope}', '');
1885
- }
1886
- // Clean up extra spaces
1887
- result = result.replace(/\s+/g, ' ').trim();
1888
- return result;
1889
- }
1890
- /**
1891
- * Build full commit message string
1892
- */
1893
- static buildFullMessage(type, scope, description, body, includeEmoji, ticketInfo, isBreaking, breakingReasons) {
1894
- const config = configService_1.ConfigService.getConfig();
1895
- const includeBreakingFooter = config.breakingChangeDetection?.includeFooter !== false;
1896
- const templates = config.templates;
1897
- let full = '';
1898
- // Use template if available
1899
- if (templates) {
1900
- const template = scope
1901
- ? (templates.default || '{emoji} {type}({scope}): {description}')
1902
- : (templates.noScope || '{emoji} {type}: {description}');
1903
- full = this.applyTemplate(template, type, scope, description, includeEmoji, isBreaking);
1904
- }
1905
- else {
1906
- // Fallback to original logic
1907
- if (includeEmoji) {
1908
- full += `${COMMIT_EMOJIS[type]} `;
1909
- }
1910
- full += type;
1911
- if (scope) {
1912
- full += `(${scope})`;
1913
- }
1914
- // Add breaking change indicator
1915
- if (isBreaking) {
1916
- full += '!';
1917
- }
1918
- full += `: ${description}`;
1919
- }
1920
- if (body) {
1921
- full += `\n\n${body}`;
1922
- }
1923
- // Add BREAKING CHANGE footer if enabled and breaking
1924
- if (isBreaking && includeBreakingFooter && breakingReasons && breakingReasons.length > 0) {
1925
- full += '\n\nBREAKING CHANGE: ' + breakingReasons[0];
1926
- if (breakingReasons.length > 1) {
1927
- for (let i = 1; i < breakingReasons.length; i++) {
1928
- full += `\n- ${breakingReasons[i]}`;
1929
- }
1930
- }
1931
- }
1932
- // Add ticket reference as footer
1933
- if (ticketInfo) {
1934
- const prefix = ticketInfo.prefix || 'Refs:';
1935
- full += `\n\n${prefix} ${ticketInfo.id}`;
1936
- }
1937
- return full;
1938
- }
1939
172
  /**
1940
173
  * Generate the final commit message
1941
174
  */
@@ -1947,9 +180,9 @@ class AnalyzerService {
1947
180
  if (includeEmoji === undefined) {
1948
181
  includeEmoji = historyService_1.HistoryService.projectUsesEmojis();
1949
182
  }
1950
- const body = this.generateBody(analysis);
183
+ const body = (0, messageBuilder_1.generateBody)(analysis);
1951
184
  const ticketInfo = historyService_1.HistoryService.detectTicketFromBranch();
1952
- const full = this.buildFullMessage(analysis.commitType, analysis.scope, analysis.description, body, includeEmoji, ticketInfo, analysis.isBreakingChange, analysis.breakingChangeReasons);
185
+ const full = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, analysis.scope, analysis.description, body, includeEmoji, ticketInfo, analysis.isBreakingChange, analysis.breakingChangeReasons);
1953
186
  return {
1954
187
  type: analysis.commitType,
1955
188
  scope: analysis.scope,
@@ -1971,7 +204,7 @@ class AnalyzerService {
1971
204
  includeEmoji = historyService_1.HistoryService.projectUsesEmojis();
1972
205
  }
1973
206
  const suggestions = [];
1974
- const body = this.generateBody(analysis);
207
+ const body = (0, messageBuilder_1.generateBody)(analysis);
1975
208
  const ticketInfo = historyService_1.HistoryService.detectTicketFromBranch();
1976
209
  const { isBreakingChange, breakingChangeReasons } = analysis;
1977
210
  // Try to get a better scope from history if none detected
@@ -1982,7 +215,7 @@ class AnalyzerService {
1982
215
  scope = historyService_1.HistoryService.getSuggestedScope(filePaths);
1983
216
  }
1984
217
  // Suggestion 1: Default (with scope if detected, with ticket, with breaking change)
1985
- const defaultFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
218
+ const defaultFull = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, scope, analysis.description, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
1986
219
  suggestions.push({
1987
220
  id: 1,
1988
221
  label: isBreakingChange ? 'Breaking Change' : 'Recommended',
@@ -1997,7 +230,7 @@ class AnalyzerService {
1997
230
  });
1998
231
  // Suggestion 2: Without scope (more concise)
1999
232
  if (scope) {
2000
- const noScopeFull = this.buildFullMessage(analysis.commitType, undefined, analysis.description, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
233
+ const noScopeFull = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, undefined, analysis.description, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
2001
234
  suggestions.push({
2002
235
  id: 2,
2003
236
  label: 'Concise',
@@ -2012,9 +245,9 @@ class AnalyzerService {
2012
245
  });
2013
246
  }
2014
247
  // Suggestion 3: Alternative description style
2015
- const altDescription = this.generateAlternativeDescription(analysis, gitService_1.GitService.getDiff());
248
+ const altDescription = (0, descriptionGenerator_1.generateAlternativeDescription)(analysis, gitService_1.GitService.getDiff());
2016
249
  if (altDescription && altDescription !== analysis.description) {
2017
- const altFull = this.buildFullMessage(analysis.commitType, scope, altDescription, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
250
+ const altFull = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, scope, altDescription, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
2018
251
  suggestions.push({
2019
252
  id: suggestions.length + 1,
2020
253
  label: 'Detailed',
@@ -2030,7 +263,7 @@ class AnalyzerService {
2030
263
  }
2031
264
  // Suggestion 4: Without body (compact) - only if body exists
2032
265
  if (body) {
2033
- const compactFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, undefined, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
266
+ const compactFull = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, scope, analysis.description, undefined, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
2034
267
  suggestions.push({
2035
268
  id: suggestions.length + 1,
2036
269
  label: 'Compact',
@@ -2046,7 +279,7 @@ class AnalyzerService {
2046
279
  }
2047
280
  // Suggestion 5: Without ticket reference (if ticket was detected)
2048
281
  if (ticketInfo) {
2049
- const noTicketFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, body, includeEmoji, null, isBreakingChange, breakingChangeReasons);
282
+ const noTicketFull = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, scope, analysis.description, body, includeEmoji, null, isBreakingChange, breakingChangeReasons);
2050
283
  suggestions.push({
2051
284
  id: suggestions.length + 1,
2052
285
  label: 'No Ticket',
@@ -2062,7 +295,7 @@ class AnalyzerService {
2062
295
  }
2063
296
  // Suggestion 6: Without breaking change indicator (if breaking change detected)
2064
297
  if (isBreakingChange) {
2065
- const noBreakingFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, body, includeEmoji, ticketInfo, false, []);
298
+ const noBreakingFull = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, scope, analysis.description, body, includeEmoji, ticketInfo, false, []);
2066
299
  suggestions.push({
2067
300
  id: suggestions.length + 1,
2068
301
  label: 'No Breaking Flag',
@@ -2096,7 +329,7 @@ class AnalyzerService {
2096
329
  if (includeEmoji === undefined) {
2097
330
  includeEmoji = historyService_1.HistoryService.projectUsesEmojis();
2098
331
  }
2099
- const body = this.generateBody(analysis);
332
+ const body = (0, messageBuilder_1.generateBody)(analysis);
2100
333
  const ticketInfo = historyService_1.HistoryService.detectTicketFromBranch();
2101
334
  const { isBreakingChange, breakingChangeReasons } = analysis;
2102
335
  // Get scope
@@ -2110,7 +343,7 @@ class AnalyzerService {
2110
343
  const aiResponse = await aiService_1.AIService.generateDescription(analysis, diff);
2111
344
  if (aiResponse && aiResponse.description) {
2112
345
  const aiDescription = aiResponse.description;
2113
- const aiFull = this.buildFullMessage(analysis.commitType, scope, aiDescription, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
346
+ const aiFull = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, scope, aiDescription, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
2114
347
  // Insert AI suggestion at the beginning
2115
348
  suggestions.unshift({
2116
349
  id: 0,
@@ -2136,96 +369,6 @@ class AnalyzerService {
2136
369
  }
2137
370
  return suggestions;
2138
371
  }
2139
- /**
2140
- * Generate an alternative description style
2141
- * Uses AST data when available, otherwise falls back to regex extraction.
2142
- */
2143
- static generateAlternativeDescription(analysis, diff) {
2144
- const { fileChanges, filesAffected } = analysis;
2145
- const totalFiles = fileChanges.added.length + fileChanges.modified.length +
2146
- fileChanges.deleted.length + fileChanges.renamed.length;
2147
- // Use AST data to build a detailed alternative with file context
2148
- if (analysis.astResult && analysis.astResult.affectedElements.length > 0) {
2149
- const elements = analysis.astResult.affectedElements;
2150
- const verb = analysis.commitType === 'feat' ? 'add' :
2151
- analysis.commitType === 'fix' ? 'fix' :
2152
- analysis.commitType === 'refactor' ? 'refactor' : 'update';
2153
- // For single file, include file name for extra context
2154
- if (totalFiles === 1) {
2155
- const fileName = fileChanges.added[0] || fileChanges.modified[0] || fileChanges.deleted[0] || fileChanges.renamed[0];
2156
- if (elements.length === 1) {
2157
- return `${verb} ${elements[0]} in ${fileName}`;
2158
- }
2159
- if (elements.length === 2) {
2160
- return `${verb} ${elements[0]} and ${elements[1]} in ${fileName}`;
2161
- }
2162
- return `${verb} ${elements[0]}, ${elements[1]} and ${elements.length - 2} more in ${fileName}`;
2163
- }
2164
- // For multiple files, list elements across files
2165
- if (elements.length === 1) {
2166
- return `${verb} ${elements[0]}`;
2167
- }
2168
- if (elements.length === 2) {
2169
- return `${verb} ${elements[0]} and ${elements[1]}`;
2170
- }
2171
- return `${verb} ${elements[0]}, ${elements[1]} and ${elements.length - 2} more`;
2172
- }
2173
- // For single file, provide more detail using extracted elements when available
2174
- if (totalFiles === 1) {
2175
- const elements = this.extractAffectedElements(diff);
2176
- const fileName = fileChanges.added[0] || fileChanges.modified[0] || fileChanges.deleted[0] || fileChanges.renamed[0];
2177
- if (elements.length > 0) {
2178
- const verb = analysis.commitType === 'feat' ? 'add' :
2179
- analysis.commitType === 'fix' ? 'fix' :
2180
- analysis.commitType === 'refactor' ? 'refactor' : 'update';
2181
- if (elements.length === 1) {
2182
- return `${verb} ${elements[0]} in ${fileName}`;
2183
- }
2184
- if (elements.length === 2) {
2185
- return `${verb} ${elements[0]} and ${elements[1]} in ${fileName}`;
2186
- }
2187
- return `${verb} ${elements[0]}, ${elements[1]} and ${elements.length - 2} more in ${fileName}`;
2188
- }
2189
- // Fall back to hunk context for modified files where declarations aren't in the diff
2190
- const hunkNames = this.extractHunkContext(diff);
2191
- if (hunkNames.length > 0) {
2192
- const verb = analysis.commitType === 'feat' ? 'add' :
2193
- analysis.commitType === 'fix' ? 'fix' :
2194
- analysis.commitType === 'refactor' ? 'refactor' : 'update';
2195
- if (hunkNames.length === 1) {
2196
- return `${verb} ${hunkNames[0]} in ${fileName}`;
2197
- }
2198
- if (hunkNames.length === 2) {
2199
- return `${verb} ${hunkNames[0]} and ${hunkNames[1]} in ${fileName}`;
2200
- }
2201
- return `${verb} ${hunkNames[0]}, ${hunkNames[1]} and ${hunkNames.length - 2} more in ${fileName}`;
2202
- }
2203
- if (fileChanges.added.length === 1) {
2204
- return `implement ${fileChanges.added[0]}`;
2205
- }
2206
- if (fileChanges.modified.length === 1) {
2207
- return `update ${fileChanges.modified[0]}`;
2208
- }
2209
- }
2210
- // For multiple files, be more descriptive about categories
2211
- const parts = [];
2212
- if (filesAffected.source > 0) {
2213
- parts.push(`${filesAffected.source} source file${filesAffected.source > 1 ? 's' : ''}`);
2214
- }
2215
- if (filesAffected.test > 0) {
2216
- parts.push(`${filesAffected.test} test${filesAffected.test > 1 ? 's' : ''}`);
2217
- }
2218
- if (filesAffected.docs > 0) {
2219
- parts.push(`${filesAffected.docs} doc${filesAffected.docs > 1 ? 's' : ''}`);
2220
- }
2221
- if (filesAffected.config > 0) {
2222
- parts.push(`${filesAffected.config} config${filesAffected.config > 1 ? 's' : ''}`);
2223
- }
2224
- if (parts.length > 0) {
2225
- return `update ${parts.join(', ')}`;
2226
- }
2227
- return analysis.description;
2228
- }
2229
372
  }
2230
373
  exports.AnalyzerService = AnalyzerService;
2231
374
  //# sourceMappingURL=analyzerService.js.map