@m3hti/commit-genie 3.1.0 โ†’ 3.1.1

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