@m3hti/commit-genie 1.2.5 → 1.2.6

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