@m3hti/commit-genie 1.2.5 → 1.2.7

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 +139 -0
  34. package/dist/services/analyzerService.d.ts.map +1 -0
  35. package/dist/services/analyzerService.js +1811 -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 +577 -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,1811 @@
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
+ // === FORMATTING-ONLY DETECTION (code style, not CSS) ===
409
+ // Per Conventional Commits: "style" = changes that do not affect the meaning of the code
410
+ // (white-space, formatting, missing semi-colons, etc.)
411
+ if (this.isFormattingOnlyChange(diff)) {
412
+ return 'style';
413
+ }
414
+ // === STYLE DETECTION (CSS/UI) ===
415
+ // CSS/UI styling changes also classify as "style"
416
+ // Check for CSS/styling file extensions
417
+ const isStyleFile = filePaths.some(p => p.endsWith('.css') ||
418
+ p.endsWith('.scss') ||
419
+ p.endsWith('.sass') ||
420
+ p.endsWith('.less') ||
421
+ p.endsWith('.styl') ||
422
+ p.endsWith('.stylus') ||
423
+ p.includes('.styles.') ||
424
+ p.includes('.style.') ||
425
+ p.includes('styles/') ||
426
+ p.includes('/theme') ||
427
+ p.includes('theme.') ||
428
+ p.includes('.theme.'));
429
+ // Check for styled-components, Tailwind, or inline style changes in diff content
430
+ const hasStyledComponentChanges = /^\+.*\bstyled\s*[.(]/m.test(diff) ||
431
+ /^\+.*\bcss\s*`/m.test(diff) ||
432
+ /^\+.*\@emotion\/styled/m.test(diff);
433
+ 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);
434
+ const hasInlineStyleChanges = /^\+.*\bstyle\s*=\s*\{\{/m.test(diff);
435
+ const hasThemeChanges = /^\+.*(theme\s*[:=]|colors\s*[:=]|palette\s*[:=]|spacing\s*[:=]|typography\s*[:=])/m.test(diff);
436
+ const hasSxPropChanges = /^\+.*\bsx\s*=\s*\{/m.test(diff);
437
+ // Only classify as "style" if it's a CSS file OR contains actual CSS/styling code
438
+ const hasActualStyleChanges = isStyleFile || hasStyledComponentChanges ||
439
+ hasTailwindChanges || hasInlineStyleChanges ||
440
+ hasThemeChanges || hasSxPropChanges;
441
+ // NEVER classify as style if it's just JS without CSS
442
+ // This prevents console.log, debug statements, comments, test prints from being labeled as style
443
+ if (hasActualStyleChanges && !this.hasOnlyNonStyleJsChanges(diff, filePaths)) {
444
+ return 'style';
445
+ }
446
+ // === PERFORMANCE DETECTION ===
447
+ const perfPatterns = [
448
+ /\bperformance\b/i,
449
+ /\boptimiz(e|ation|ing)\b/i,
450
+ /\bfaster\b/i,
451
+ /\bspeed\s*(up|improvement)\b/i,
452
+ /\bcach(e|ing)\b/i,
453
+ /\bmemoiz(e|ation)\b/i,
454
+ /\blazy\s*load/i,
455
+ /\basync\b.*\bawait\b/i,
456
+ /\bparallel\b/i,
457
+ /\bbatch(ing)?\b/i,
458
+ ];
459
+ if (perfPatterns.some(p => p.test(diffLower))) {
460
+ return 'perf';
461
+ }
462
+ // === DETECT NEW FUNCTIONALITY FIRST ===
463
+ // This must come BEFORE fix detection to avoid false positives when new functions contain validation
464
+ const hasNewFiles = stagedFiles.some((f) => f.status === 'A');
465
+ // Comprehensive new code detection
466
+ const hasNewExports = /^\+\s*export\s+(function|class|const|let|var|interface|type|default)/m.test(diff);
467
+ const hasNewFunctions = /^\+\s*(async\s+)?function\s+\w+/m.test(diff);
468
+ const hasNewArrowFunctions = /^\+\s*(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>/m.test(diff);
469
+ const hasNewClasses = /^\+\s*(export\s+)?(class|abstract\s+class)\s+\w+/m.test(diff);
470
+ const hasNewMethods = /^\+\s+(async\s+)?\w+\s*\([^)]*\)\s*{/m.test(diff);
471
+ const hasNewInterfaces = /^\+\s*(export\s+)?(interface|type)\s+\w+/m.test(diff);
472
+ const hasNewComponents = /^\+\s*(export\s+)?(const|function)\s+[A-Z]\w+/m.test(diff); // React components start with capital
473
+ const hasNewImports = /^\+\s*import\s+/m.test(diff);
474
+ // Count significant additions vs removals
475
+ const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
476
+ const removedLines = (diff.match(/^-[^-]/gm) || []).length;
477
+ const netNewLines = addedLines - removedLines;
478
+ // Detect if we're adding new functionality
479
+ const hasNewFunctionality = hasNewExports || hasNewFunctions || hasNewArrowFunctions ||
480
+ hasNewClasses || hasNewMethods || hasNewInterfaces || hasNewComponents;
481
+ // === FEAT DETECTION (new functionality) - CHECK BEFORE FIX ===
482
+ if (hasNewFiles) {
483
+ return 'feat';
484
+ }
485
+ // New functions, classes, components = feat (even if they contain validation code)
486
+ if (hasNewFunctionality) {
487
+ return 'feat';
488
+ }
489
+ // Significant net additions with new imports often means new feature
490
+ if (hasNewImports && netNewLines > 5) {
491
+ return 'feat';
492
+ }
493
+ // Check for new feature indicators in comments/strings
494
+ const featPatterns = [
495
+ /\badd(s|ed|ing)?\s+(new\s+)?(feature|support|ability|option|functionality)/i,
496
+ /\bimplement(s|ed|ing)?\s+\w+/i,
497
+ /\bintroduc(e|es|ed|ing)\s+(new\s+)?\w+/i,
498
+ /\benable(s|d|ing)?\s+(new\s+)?\w+/i,
499
+ /\bnew\s+(feature|function|method|api|endpoint)/i,
500
+ ];
501
+ if (featPatterns.some(p => p.test(diff))) {
502
+ return 'feat';
503
+ }
504
+ // === FIX DETECTION ===
505
+ // Only check for validation patterns AFTER confirming no new functions/classes were added
506
+ // Check if this is adding validation/guards to existing code (defensive coding = fix)
507
+ const hasOnlyModifications = stagedFiles.every((f) => f.status === 'M');
508
+ const validationPatterns = [
509
+ /^\+.*\btypeof\s+\w+\s*===?\s*['"`]\w+['"`]/m, // typeof checks
510
+ /^\+.*\binstanceof\s+\w+/m, // instanceof checks
511
+ /^\+.*\bArray\.isArray\s*\(/m, // Array.isArray checks
512
+ /^\+.*\b(Number|String|Boolean)\.is\w+\s*\(/m, // Number.isNaN, etc.
513
+ /^\+.*\bif\s*\(\s*typeof\b/m, // if (typeof ...
514
+ /^\+.*\bif\s*\(\s*!\w+\s*\)/m, // if (!var) guards
515
+ /^\+.*\bif\s*\(\s*\w+\s*(===?|!==?)\s*(null|undefined)\s*\)/m, // null/undefined checks
516
+ /^\+.*\bif\s*\(\s*(null|undefined)\s*(===?|!==?)\s*\w+\s*\)/m, // null/undefined checks (reversed)
517
+ /^\+.*\bif\s*\(\s*\w+\s*[<>]=?\s*-?\d/m, // numeric boundary checks: if (n < 0), if (x > 10)
518
+ /^\+.*\bif\s*\(\s*-?\d+\s*[<>]=?\s*\w+/m, // numeric boundary checks (reversed): if (0 > n)
519
+ /^\+.*\?\?/m, // Nullish coalescing
520
+ /^\+.*\?\./m, // Optional chaining
521
+ /^\+.*\|\|/m, // Default value patterns (when combined with guards)
522
+ ];
523
+ // Check if this is a simplification refactor (using modern syntax to replace verbose code)
524
+ // When deletions > additions significantly, it's likely simplifying existing code, not adding new checks
525
+ const isSimplificationRefactor = removedLines > addedLines &&
526
+ (removedLines - addedLines) >= 3 && // At least 3 net lines removed (significant simplification)
527
+ (/^\+.*\?\./m.test(diff) || // Using optional chaining
528
+ /^\+.*\?\?/m.test(diff) // Using nullish coalescing
529
+ ) &&
530
+ (/^-.*\bif\s*\(/m.test(diff) // Removing if statements
531
+ );
532
+ // If it's a simplification (replacing verbose ifs with optional chaining), it's refactor
533
+ if (isSimplificationRefactor) {
534
+ return 'refactor';
535
+ }
536
+ // If only modifying files (no new functions detected) and adding validation patterns, it's likely a fix
537
+ if (hasOnlyModifications && validationPatterns.some(p => p.test(diff))) {
538
+ return 'fix';
539
+ }
540
+ const fixPatterns = [
541
+ /\bfix(es|ed|ing)?\s*(the\s*)?(bug|issue|error|problem|crash)/i,
542
+ /\bfix(es|ed|ing)?\b/i, // Simple "fix" or "fixed" alone
543
+ /\bbug\s*fix/i,
544
+ /\bBUG:/i, // Bug comment markers
545
+ /\bhotfix\b/i,
546
+ /\bpatch(es|ed|ing)?\b/i,
547
+ /\bresolv(e|es|ed|ing)\s*(the\s*)?(issue|bug|error)/i,
548
+ /\bcorrect(s|ed|ing)?\s*(the\s*)?(bug|issue|error|problem)/i,
549
+ /\brepair(s|ed|ing)?\b/i,
550
+ /\bhandle\s*(error|exception|null|undefined)/i,
551
+ /\bnull\s*check/i,
552
+ /\bundefined\s*check/i,
553
+ /\btry\s*{\s*.*\s*}\s*catch/i,
554
+ /\bif\s*\(\s*!\s*\w+\s*\)/, // Null/undefined guards
555
+ /\bwas\s*broken\b/i, // "was broken" indicates fixing
556
+ /\bbroken\b.*\bfix/i, // broken...fix pattern
557
+ ];
558
+ if (fixPatterns.some(p => p.test(diff))) {
559
+ return 'fix';
560
+ }
561
+ // === REFACTOR DETECTION ===
562
+ const refactorPatterns = [
563
+ /\brefactor(s|ed|ing)?\b/i,
564
+ /\brestructur(e|es|ed|ing)\b/i,
565
+ /\bclean\s*up\b/i,
566
+ /\bsimplif(y|ies|ied|ying)\b/i,
567
+ /\brenam(e|es|ed|ing)\b/i,
568
+ /\bmov(e|es|ed|ing)\s*(to|from|into)\b/i,
569
+ /\bextract(s|ed|ing)?\s*(function|method|class|component)/i,
570
+ /\binline(s|d|ing)?\b/i,
571
+ /\bdedup(licate)?\b/i,
572
+ /\bDRY\b/,
573
+ /\breorganiz(e|es|ed|ing)\b/i,
574
+ ];
575
+ if (refactorPatterns.some(p => p.test(diff))) {
576
+ return 'refactor';
577
+ }
578
+ // If modifications with balanced adds/removes and no new functionality, likely refactor
579
+ if (hasOnlyModifications && !hasNewFunctionality) {
580
+ // If roughly equal adds and removes, it's likely refactoring
581
+ if (addedLines > 0 && removedLines > 0) {
582
+ const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
583
+ if (ratio > 0.5) { // More strict ratio - must be very balanced
584
+ return 'refactor';
585
+ }
586
+ }
587
+ }
588
+ // === CHORE DETECTION ===
589
+ const chorePatterns = [
590
+ /\bdependenc(y|ies)\b/i,
591
+ /\bupgrade\b/i,
592
+ /\bupdate\s*(version|dep)/i,
593
+ /\bbump\b/i,
594
+ /\bpackage\.json\b/i,
595
+ /\bpackage-lock\.json\b/i,
596
+ /\byarn\.lock\b/i,
597
+ /\b\.gitignore\b/i,
598
+ /\bci\b.*\b(config|setup)\b/i,
599
+ /\blint(er|ing)?\b/i,
600
+ ];
601
+ if (chorePatterns.some(p => p.test(diff)) || chorePatterns.some(p => filePaths.some(f => p.test(f)))) {
602
+ return 'chore';
603
+ }
604
+ // === FALLBACK ===
605
+ // If we have more additions than removals, lean towards feat
606
+ if (filesAffected.source > 0) {
607
+ if (netNewLines > 10) {
608
+ return 'feat'; // Significant new code added
609
+ }
610
+ if (hasOnlyModifications) {
611
+ return 'refactor'; // Modifications without clear new functionality
612
+ }
613
+ return 'feat'; // Default for source changes with new files
614
+ }
615
+ return 'chore';
616
+ }
617
+ /**
618
+ * Check if diff contains actual logic changes (not just formatting)
619
+ */
620
+ static hasLogicChanges(diff) {
621
+ // Remove formatting-only changes and check if there's real code
622
+ const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
623
+ !line.startsWith('+++') &&
624
+ !line.startsWith('---'));
625
+ for (const line of lines) {
626
+ const content = line.substring(1).trim();
627
+ // Skip empty lines, comments, and whitespace-only
628
+ if (content.length === 0 ||
629
+ content.startsWith('//') ||
630
+ content.startsWith('/*') ||
631
+ content.startsWith('*') ||
632
+ content === '{' ||
633
+ content === '}' ||
634
+ content === ';') {
635
+ continue;
636
+ }
637
+ // Has actual code change
638
+ return true;
639
+ }
640
+ return false;
641
+ }
642
+ /**
643
+ * Check if the diff contains ONLY formatting changes (no logic impact).
644
+ * Formatting changes include: semicolons, blank lines, indentation, trailing commas, braces.
645
+ * Per Conventional Commits, these should be classified as "style".
646
+ */
647
+ static isFormattingOnlyChange(diff) {
648
+ const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
649
+ !line.startsWith('+++') &&
650
+ !line.startsWith('---'));
651
+ if (lines.length === 0) {
652
+ return false;
653
+ }
654
+ // Pair up removed and added lines to detect indentation-only changes
655
+ const removed = [];
656
+ const added = [];
657
+ for (const line of lines) {
658
+ const content = line.substring(1);
659
+ const trimmed = content.trim();
660
+ // Empty / whitespace-only lines are always formatting
661
+ if (trimmed.length === 0) {
662
+ continue;
663
+ }
664
+ // Standalone semicolons, braces, trailing commas are formatting
665
+ if (trimmed === ';' || trimmed === '{' || trimmed === '}' || trimmed === ',') {
666
+ continue;
667
+ }
668
+ // Collect lines for indentation-only and semicolon-only comparison
669
+ if (line.startsWith('-')) {
670
+ removed.push(trimmed);
671
+ }
672
+ else {
673
+ added.push(trimmed);
674
+ }
675
+ }
676
+ // If no added/removed content lines remain, it's purely blank line changes
677
+ if (removed.length === 0 && added.length === 0) {
678
+ return true;
679
+ }
680
+ // Check if each added line corresponds to a removed line that differs only by
681
+ // formatting (trailing semicolons, trailing commas, whitespace)
682
+ if (removed.length !== added.length) {
683
+ return false;
684
+ }
685
+ for (let i = 0; i < removed.length; i++) {
686
+ // Normalize: strip trailing semicolons, commas, and whitespace
687
+ const normalizeFormatting = (s) => s.replace(/[;\s,]+$/g, '').replace(/^\s+/, '');
688
+ const normRemoved = normalizeFormatting(removed[i]);
689
+ const normAdded = normalizeFormatting(added[i]);
690
+ if (normRemoved !== normAdded) {
691
+ return false;
692
+ }
693
+ }
694
+ return true;
695
+ }
696
+ /**
697
+ * Generate a description for formatting-only changes
698
+ */
699
+ static generateFormattingDescription(diff, stagedFiles) {
700
+ const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
701
+ !line.startsWith('+++') &&
702
+ !line.startsWith('---'));
703
+ let hasSemicolonChanges = false;
704
+ let hasBlankLineChanges = false;
705
+ let hasIndentationChanges = false;
706
+ let hasBraceChanges = false;
707
+ let hasTrailingCommaChanges = false;
708
+ const removed = [];
709
+ const added = [];
710
+ for (const line of lines) {
711
+ const trimmed = line.substring(1).trim();
712
+ if (trimmed.length === 0) {
713
+ hasBlankLineChanges = true;
714
+ continue;
715
+ }
716
+ if (trimmed === ';') {
717
+ hasSemicolonChanges = true;
718
+ continue;
719
+ }
720
+ if (trimmed === '{' || trimmed === '}') {
721
+ hasBraceChanges = true;
722
+ continue;
723
+ }
724
+ if (trimmed === ',') {
725
+ hasTrailingCommaChanges = true;
726
+ continue;
727
+ }
728
+ if (line.startsWith('-')) {
729
+ removed.push(trimmed);
730
+ }
731
+ else {
732
+ added.push(trimmed);
733
+ }
734
+ }
735
+ // Check paired lines for semicolon-only or indentation-only diffs
736
+ for (let i = 0; i < removed.length && i < added.length; i++) {
737
+ const r = removed[i];
738
+ const a = added[i];
739
+ // If trimmed content is identical, the only difference was indentation
740
+ if (r === a) {
741
+ hasIndentationChanges = true;
742
+ }
743
+ // Semicolon difference (one has semicolon, the other doesn't)
744
+ else if (r + ';' === a || a + ';' === r) {
745
+ hasSemicolonChanges = true;
746
+ }
747
+ // Trailing comma difference
748
+ else if (r + ',' === a || a + ',' === r) {
749
+ hasTrailingCommaChanges = true;
750
+ }
751
+ else {
752
+ hasIndentationChanges = true;
753
+ }
754
+ }
755
+ // Build description based on what changed
756
+ let description;
757
+ if (hasSemicolonChanges && !hasBlankLineChanges && !hasIndentationChanges && !hasBraceChanges && !hasTrailingCommaChanges) {
758
+ description = 'add missing semicolons';
759
+ }
760
+ else if (hasBlankLineChanges && !hasSemicolonChanges && !hasIndentationChanges && !hasBraceChanges && !hasTrailingCommaChanges) {
761
+ description = 'remove blank lines';
762
+ }
763
+ else if (hasIndentationChanges && !hasSemicolonChanges && !hasBlankLineChanges && !hasBraceChanges && !hasTrailingCommaChanges) {
764
+ description = 'fix indentation';
765
+ }
766
+ else if (hasBraceChanges && !hasSemicolonChanges && !hasBlankLineChanges && !hasIndentationChanges && !hasTrailingCommaChanges) {
767
+ description = 'fix brace formatting';
768
+ }
769
+ else if (hasTrailingCommaChanges && !hasSemicolonChanges && !hasBlankLineChanges && !hasIndentationChanges && !hasBraceChanges) {
770
+ description = 'fix trailing commas';
771
+ }
772
+ else {
773
+ description = 'fix code formatting';
774
+ }
775
+ // Append filename for single-file changes
776
+ if (stagedFiles.length === 1) {
777
+ const fileName = stagedFiles[0].path.split('/').pop() || stagedFiles[0].path;
778
+ description += ` in ${fileName}`;
779
+ }
780
+ return description;
781
+ }
782
+ /**
783
+ * Check if the changes are ONLY non-style JavaScript changes
784
+ * (console.log, debug statements, test prints, comments, etc.)
785
+ * Returns true if JS files have changes but NO actual styling changes
786
+ */
787
+ static hasOnlyNonStyleJsChanges(diff, filePaths) {
788
+ // Check if all files are JavaScript/TypeScript (not CSS)
789
+ const hasOnlyJsFiles = filePaths.every(p => p.endsWith('.js') ||
790
+ p.endsWith('.jsx') ||
791
+ p.endsWith('.ts') ||
792
+ p.endsWith('.tsx') ||
793
+ p.endsWith('.mjs') ||
794
+ p.endsWith('.cjs'));
795
+ if (!hasOnlyJsFiles) {
796
+ return false;
797
+ }
798
+ // Patterns that indicate NON-style changes (debug, logging, test output)
799
+ const nonStylePatterns = [
800
+ /console\.(log|debug|info|warn|error|trace|dir|table)/,
801
+ /debugger\b/,
802
+ /logger\.\w+/i,
803
+ /debug\s*\(/,
804
+ /print\s*\(/,
805
+ /console\.assert/,
806
+ /console\.time/,
807
+ /console\.count/,
808
+ /console\.group/,
809
+ /process\.stdout/,
810
+ /process\.stderr/,
811
+ /\.toLog\(/,
812
+ /\.log\(/,
813
+ /winston\./,
814
+ /pino\./,
815
+ /bunyan\./,
816
+ /log4js\./,
817
+ ];
818
+ // Get all added/changed lines
819
+ const changedLines = diff.split('\n').filter(line => line.startsWith('+') && !line.startsWith('+++'));
820
+ // If the changes match non-style patterns, return true
821
+ const hasNonStyleChanges = changedLines.some(line => nonStylePatterns.some(pattern => pattern.test(line)));
822
+ // Check if there are NO style-related patterns in the JS files
823
+ const stylePatterns = [
824
+ /styled\s*[.(]/,
825
+ /css\s*`/,
826
+ /\bstyle\s*=\s*\{\{/,
827
+ /className\s*=/,
828
+ /\bsx\s*=/,
829
+ /theme\./,
830
+ /colors\./,
831
+ /palette\./,
832
+ ];
833
+ const hasStylePatterns = changedLines.some(line => stylePatterns.some(pattern => pattern.test(line)));
834
+ // Return true only if we have non-style changes AND no style patterns
835
+ return hasNonStyleChanges && !hasStylePatterns;
836
+ }
837
+ /**
838
+ * Check if the diff contains only comment changes (documentation)
839
+ * Returns true if ALL changes are comments (no code changes)
840
+ */
841
+ static isCommentOnlyChange(diff) {
842
+ // Get all changed lines (additions and deletions)
843
+ const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
844
+ !line.startsWith('+++') &&
845
+ !line.startsWith('---'));
846
+ if (lines.length === 0) {
847
+ return false;
848
+ }
849
+ let hasCommentChanges = false;
850
+ for (const line of lines) {
851
+ const content = line.substring(1).trim();
852
+ // Skip empty lines
853
+ if (content.length === 0) {
854
+ continue;
855
+ }
856
+ // Check if line is a comment
857
+ const isComment = content.startsWith('//') || // Single-line comment
858
+ content.startsWith('/*') || // Multi-line comment start
859
+ content.startsWith('*') || // Multi-line comment body
860
+ content.startsWith('*/') || // Multi-line comment end
861
+ content.startsWith('#') || // Shell/Python/Ruby comments
862
+ content.startsWith('<!--') || // HTML comments
863
+ content.startsWith('-->') || // HTML comment end
864
+ content.startsWith('"""') || // Python docstring
865
+ content.startsWith("'''") || // Python docstring
866
+ /^\/\*\*/.test(content) || // JSDoc start
867
+ /^\*\s*@\w+/.test(content); // JSDoc tag
868
+ if (isComment) {
869
+ hasCommentChanges = true;
870
+ }
871
+ else {
872
+ // Found a non-comment change - not a comment-only diff
873
+ return false;
874
+ }
875
+ }
876
+ return hasCommentChanges;
877
+ }
878
+ /**
879
+ * Perform semantic analysis on the diff to understand the nature of changes
880
+ * This provides intent-based understanding rather than line-count metrics
881
+ */
882
+ static analyzeSemanticChanges(diff, stagedFiles) {
883
+ const roleChanges = this.detectRoleChanges(diff, stagedFiles);
884
+ const primaryRole = this.determinePrimaryRole(roleChanges);
885
+ let primaryIntent = this.determineIntent(diff, stagedFiles, roleChanges);
886
+ const affectedElements = this.extractAffectedElements(diff);
887
+ // Detect renames - this takes priority for determining intent
888
+ const renames = this.detectRenames(diff);
889
+ if (renames.length > 0) {
890
+ primaryIntent = 'rename';
891
+ }
892
+ // Generate human-readable descriptions
893
+ const intentDescription = this.generateIntentDescription(primaryIntent, primaryRole, roleChanges);
894
+ const whatChanged = renames.length > 0
895
+ ? `${renames[0].oldName} to ${renames[0].newName}`
896
+ : this.generateWhatChanged(roleChanges, affectedElements, stagedFiles);
897
+ return {
898
+ primaryRole,
899
+ primaryIntent,
900
+ roleChanges,
901
+ intentDescription,
902
+ whatChanged,
903
+ hasMultipleRoles: roleChanges.filter(r => r.significance > 20).length > 1,
904
+ renames: renames.length > 0 ? renames : undefined,
905
+ };
906
+ }
907
+ /**
908
+ * Detect which roles are affected by the changes and calculate semantic significance
909
+ * Significance is NOT based on line count - it's based on the semantic weight of patterns
910
+ */
911
+ static detectRoleChanges(diff, _stagedFiles) {
912
+ const roleChanges = [];
913
+ // Check each role for matches
914
+ for (const [role, patterns] of Object.entries(ROLE_PATTERNS)) {
915
+ if (role === 'unknown' || patterns.length === 0)
916
+ continue;
917
+ let matchCount = 0;
918
+ let highValueMatches = 0;
919
+ for (const pattern of patterns) {
920
+ pattern.lastIndex = 0;
921
+ const matches = diff.match(pattern);
922
+ if (matches) {
923
+ matchCount += matches.length;
924
+ // Some patterns indicate more significant changes
925
+ if (this.isHighValuePattern(pattern, role)) {
926
+ highValueMatches += matches.length;
927
+ }
928
+ }
929
+ }
930
+ if (matchCount > 0) {
931
+ // Calculate significance based on pattern matches, not line counts
932
+ // High-value patterns contribute more to significance
933
+ const baseSignificance = Math.min(matchCount * 10, 40);
934
+ const highValueBonus = highValueMatches * 15;
935
+ const significance = Math.min(baseSignificance + highValueBonus, 100);
936
+ const intent = this.detectRoleIntent(diff, role);
937
+ const summary = this.generateRoleSummary(role, intent, matchCount);
938
+ roleChanges.push({
939
+ role,
940
+ intent,
941
+ significance,
942
+ summary,
943
+ affectedElements: this.extractElementsForRole(diff, role),
944
+ });
945
+ }
946
+ }
947
+ // Sort by significance (highest first)
948
+ return roleChanges.sort((a, b) => b.significance - a.significance);
949
+ }
950
+ /**
951
+ * Determine if a pattern represents a high-value semantic change
952
+ */
953
+ static isHighValuePattern(pattern, role) {
954
+ const patternStr = pattern.source;
955
+ // High-value patterns for each role
956
+ const highValueIndicators = {
957
+ ui: ['<[A-Z]', 'className', 'onClick', 'onSubmit', 'aria-'],
958
+ logic: ['function', 'class', 'if\\s*\\(', 'switch', 'try\\s*\\{', 'throw'],
959
+ data: ['useState', 'useReducer', 'interface', 'type\\s+\\w+'],
960
+ style: ['styled\\.', 'css`', 'theme\\.'],
961
+ api: ['fetch\\s*\\(', 'axios', 'useQuery', 'useMutation', 'graphql'],
962
+ config: ['process\\.env', 'CONFIG\\.', 'export\\s+(const|let)\\s+[A-Z_]+'],
963
+ test: ['describe\\s*\\(', 'it\\s*\\(', 'test\\s*\\(', 'expect\\s*\\('],
964
+ unknown: [],
965
+ };
966
+ return highValueIndicators[role].some(indicator => patternStr.includes(indicator));
967
+ }
968
+ /**
969
+ * Detect the intent for changes in a specific role
970
+ */
971
+ static detectRoleIntent(diff, _role) {
972
+ // Check for intent patterns (role context reserved for future use)
973
+ const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
974
+ const removedLines = (diff.match(/^-[^-]/gm) || []).length;
975
+ // Check for add patterns in this role's context
976
+ for (const pattern of INTENT_PATTERNS.add) {
977
+ if (pattern.test(diff)) {
978
+ // If adding new constructs, it's an 'add' intent
979
+ return 'add';
980
+ }
981
+ }
982
+ // Check for remove patterns
983
+ for (const pattern of INTENT_PATTERNS.remove) {
984
+ if (pattern.test(diff)) {
985
+ return 'remove';
986
+ }
987
+ }
988
+ // Check for fix patterns
989
+ for (const pattern of INTENT_PATTERNS.fix) {
990
+ if (pattern.test(diff)) {
991
+ return 'fix';
992
+ }
993
+ }
994
+ // Check for enhance patterns
995
+ for (const pattern of INTENT_PATTERNS.enhance) {
996
+ if (pattern.test(diff)) {
997
+ return 'enhance';
998
+ }
999
+ }
1000
+ // Determine based on add/remove ratio
1001
+ if (addedLines > 0 && removedLines === 0) {
1002
+ return 'add';
1003
+ }
1004
+ else if (removedLines > 0 && addedLines === 0) {
1005
+ return 'remove';
1006
+ }
1007
+ else if (addedLines > 0 && removedLines > 0) {
1008
+ const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
1009
+ if (ratio > 0.5) {
1010
+ return 'refactor'; // Balanced changes suggest refactoring
1011
+ }
1012
+ return addedLines > removedLines ? 'add' : 'modify';
1013
+ }
1014
+ return 'modify';
1015
+ }
1016
+ /**
1017
+ * Determine the primary role from all detected role changes
1018
+ */
1019
+ static determinePrimaryRole(roleChanges) {
1020
+ if (roleChanges.length === 0) {
1021
+ return 'unknown';
1022
+ }
1023
+ // The role with highest significance is primary
1024
+ // But if multiple roles have similar significance, prefer more specific ones
1025
+ const topRole = roleChanges[0];
1026
+ // If there's a close second that's more specific, consider it
1027
+ if (roleChanges.length > 1) {
1028
+ const secondRole = roleChanges[1];
1029
+ const significanceDiff = topRole.significance - secondRole.significance;
1030
+ // If within 15 points and second is more specific (api > logic > ui)
1031
+ if (significanceDiff <= 15) {
1032
+ const specificityOrder = ['api', 'data', 'style', 'logic', 'ui', 'config', 'test', 'unknown'];
1033
+ const topIndex = specificityOrder.indexOf(topRole.role);
1034
+ const secondIndex = specificityOrder.indexOf(secondRole.role);
1035
+ if (secondIndex < topIndex) {
1036
+ return secondRole.role;
1037
+ }
1038
+ }
1039
+ }
1040
+ return topRole.role;
1041
+ }
1042
+ /**
1043
+ * Determine the overall intent of the changes
1044
+ */
1045
+ static determineIntent(diff, stagedFiles, roleChanges) {
1046
+ // Check file statuses first
1047
+ const hasOnlyAdded = stagedFiles.every(f => f.status === 'A');
1048
+ const hasOnlyDeleted = stagedFiles.every(f => f.status === 'D');
1049
+ const hasOnlyModified = stagedFiles.every(f => f.status === 'M');
1050
+ if (hasOnlyAdded) {
1051
+ return 'add';
1052
+ }
1053
+ if (hasOnlyDeleted) {
1054
+ return 'remove';
1055
+ }
1056
+ // If we have role changes, use the most significant role's intent
1057
+ if (roleChanges.length > 0) {
1058
+ const primaryRoleChange = roleChanges[0];
1059
+ // Special case: if the primary intent is 'fix' and we see validation patterns
1060
+ // even in non-modified files, treat it as a fix
1061
+ if (hasOnlyModified && primaryRoleChange.intent === 'fix') {
1062
+ return 'fix';
1063
+ }
1064
+ // If we're enhancing (useMemo, useCallback, etc.), that takes precedence
1065
+ if (roleChanges.some(r => r.intent === 'enhance')) {
1066
+ return 'enhance';
1067
+ }
1068
+ return primaryRoleChange.intent;
1069
+ }
1070
+ // Fallback to diff analysis
1071
+ const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
1072
+ const removedLines = (diff.match(/^-[^-]/gm) || []).length;
1073
+ if (addedLines > 0 && removedLines === 0) {
1074
+ return 'add';
1075
+ }
1076
+ if (removedLines > 0 && addedLines === 0) {
1077
+ return 'remove';
1078
+ }
1079
+ const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
1080
+ if (ratio > 0.6) {
1081
+ return 'refactor';
1082
+ }
1083
+ return 'modify';
1084
+ }
1085
+ /**
1086
+ * Extract affected element names (components, functions, etc.) from the diff
1087
+ */
1088
+ static extractAffectedElements(diff) {
1089
+ const elements = [];
1090
+ // Extract component names (React/JSX)
1091
+ const componentMatches = diff.match(/^\+.*(?:function|const)\s+([A-Z][a-zA-Z0-9]*)/gm);
1092
+ if (componentMatches) {
1093
+ for (const match of componentMatches) {
1094
+ const nameMatch = match.match(/(?:function|const)\s+([A-Z][a-zA-Z0-9]*)/);
1095
+ if (nameMatch) {
1096
+ elements.push(nameMatch[1]);
1097
+ }
1098
+ }
1099
+ }
1100
+ // Extract function names
1101
+ const functionMatches = diff.match(/^\+.*(?:function|const)\s+([a-z][a-zA-Z0-9]*)\s*[=(]/gm);
1102
+ if (functionMatches) {
1103
+ for (const match of functionMatches) {
1104
+ const nameMatch = match.match(/(?:function|const)\s+([a-z][a-zA-Z0-9]*)/);
1105
+ if (nameMatch && !elements.includes(nameMatch[1])) {
1106
+ elements.push(nameMatch[1]);
1107
+ }
1108
+ }
1109
+ }
1110
+ // Extract interface/type names
1111
+ const typeMatches = diff.match(/^\+.*(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/gm);
1112
+ if (typeMatches) {
1113
+ for (const match of typeMatches) {
1114
+ const nameMatch = match.match(/(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/);
1115
+ if (nameMatch && !elements.includes(nameMatch[1])) {
1116
+ elements.push(nameMatch[1]);
1117
+ }
1118
+ }
1119
+ }
1120
+ // Extract class names
1121
+ const classMatches = diff.match(/^\+.*class\s+([A-Z][a-zA-Z0-9]*)/gm);
1122
+ if (classMatches) {
1123
+ for (const match of classMatches) {
1124
+ const nameMatch = match.match(/class\s+([A-Z][a-zA-Z0-9]*)/);
1125
+ if (nameMatch && !elements.includes(nameMatch[1])) {
1126
+ elements.push(nameMatch[1]);
1127
+ }
1128
+ }
1129
+ }
1130
+ return elements.slice(0, 5); // Limit to top 5 elements
1131
+ }
1132
+ /**
1133
+ * Detect function/class/type renames by comparing removed and added names
1134
+ * Returns an array of { oldName, newName } objects
1135
+ */
1136
+ static detectRenames(diff) {
1137
+ const renames = [];
1138
+ // Patterns for extracting function/class/type names from removed and added lines
1139
+ const patterns = [
1140
+ // Function declarations: function name( or const name = or const name: Type =
1141
+ { regex: /^[-+].*(?:function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm, type: 'function' },
1142
+ { regex: /^[-+].*(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[=:]/gm, type: 'function' },
1143
+ // Class declarations
1144
+ { regex: /^[-+].*class\s+([A-Z][a-zA-Z0-9]*)/gm, type: 'class' },
1145
+ // Interface declarations
1146
+ { regex: /^[-+].*interface\s+([A-Z][a-zA-Z0-9]*)/gm, type: 'interface' },
1147
+ // Type declarations
1148
+ { regex: /^[-+].*type\s+([A-Z][a-zA-Z0-9]*)\s*=/gm, type: 'type' },
1149
+ ];
1150
+ for (const { regex, type } of patterns) {
1151
+ const removed = [];
1152
+ const added = [];
1153
+ let match;
1154
+ regex.lastIndex = 0;
1155
+ while ((match = regex.exec(diff)) !== null) {
1156
+ const line = match[0];
1157
+ const name = match[1];
1158
+ if (line.startsWith('-') && !line.startsWith('---')) {
1159
+ removed.push(name);
1160
+ }
1161
+ else if (line.startsWith('+') && !line.startsWith('+++')) {
1162
+ added.push(name);
1163
+ }
1164
+ }
1165
+ // If we have exactly one removed and one added of the same type,
1166
+ // and they're different names, it's likely a rename
1167
+ if (removed.length === 1 && added.length === 1 && removed[0] !== added[0]) {
1168
+ renames.push({ oldName: removed[0], newName: added[0], type });
1169
+ }
1170
+ }
1171
+ return renames;
1172
+ }
1173
+ /**
1174
+ * Extract affected elements for a specific role
1175
+ */
1176
+ static extractElementsForRole(diff, role) {
1177
+ const elements = [];
1178
+ switch (role) {
1179
+ case 'ui':
1180
+ // Extract JSX component names being used
1181
+ const jsxMatches = diff.match(/^\+.*<([A-Z][a-zA-Z0-9]*)/gm);
1182
+ if (jsxMatches) {
1183
+ for (const match of jsxMatches) {
1184
+ const nameMatch = match.match(/<([A-Z][a-zA-Z0-9]*)/);
1185
+ if (nameMatch && !elements.includes(nameMatch[1])) {
1186
+ elements.push(nameMatch[1]);
1187
+ }
1188
+ }
1189
+ }
1190
+ break;
1191
+ case 'data':
1192
+ // Extract state variable names
1193
+ const stateMatches = diff.match(/^\+.*const\s+\[([a-z][a-zA-Z0-9]*),/gm);
1194
+ if (stateMatches) {
1195
+ for (const match of stateMatches) {
1196
+ const nameMatch = match.match(/const\s+\[([a-z][a-zA-Z0-9]*)/);
1197
+ if (nameMatch) {
1198
+ elements.push(nameMatch[1]);
1199
+ }
1200
+ }
1201
+ }
1202
+ break;
1203
+ case 'api':
1204
+ // Extract API endpoints
1205
+ const apiMatches = diff.match(/['"`]\/api\/[^'"`]+['"`]/g);
1206
+ if (apiMatches) {
1207
+ elements.push(...apiMatches.map(m => m.replace(/['"`]/g, '')));
1208
+ }
1209
+ break;
1210
+ default:
1211
+ // Use generic extraction
1212
+ return this.extractAffectedElements(diff).slice(0, 3);
1213
+ }
1214
+ return elements.slice(0, 3);
1215
+ }
1216
+ /**
1217
+ * Generate a human-readable summary for a role change
1218
+ */
1219
+ static generateRoleSummary(role, intent, matchCount) {
1220
+ const roleDesc = ROLE_DESCRIPTIONS[role];
1221
+ const intentVerb = INTENT_VERBS[intent].past;
1222
+ if (matchCount === 1) {
1223
+ return `${intentVerb} ${roleDesc}`;
1224
+ }
1225
+ return `${intentVerb} ${roleDesc} (${matchCount} changes)`;
1226
+ }
1227
+ /**
1228
+ * Generate the WHY description for the commit
1229
+ */
1230
+ static generateIntentDescription(intent, role, roleChanges) {
1231
+ const roleDesc = ROLE_DESCRIPTIONS[role];
1232
+ switch (intent) {
1233
+ case 'add':
1234
+ return `to add new ${roleDesc}`;
1235
+ case 'fix':
1236
+ return `to fix ${roleDesc} issues`;
1237
+ case 'refactor':
1238
+ return `to improve ${roleDesc} structure`;
1239
+ case 'enhance':
1240
+ return `to optimize ${roleDesc} performance`;
1241
+ case 'remove':
1242
+ return `to remove unused ${roleDesc}`;
1243
+ case 'modify':
1244
+ default:
1245
+ if (roleChanges.length > 1) {
1246
+ return `to update ${roleDesc} and related code`;
1247
+ }
1248
+ return `to update ${roleDesc}`;
1249
+ }
1250
+ }
1251
+ /**
1252
+ * Generate the WHAT changed description
1253
+ */
1254
+ static generateWhatChanged(roleChanges, affectedElements, stagedFiles) {
1255
+ // If we have specific elements, use them
1256
+ if (affectedElements.length > 0) {
1257
+ if (affectedElements.length === 1) {
1258
+ return affectedElements[0];
1259
+ }
1260
+ if (affectedElements.length <= 3) {
1261
+ return affectedElements.join(', ');
1262
+ }
1263
+ return `${affectedElements.slice(0, 2).join(', ')} and ${affectedElements.length - 2} more`;
1264
+ }
1265
+ // Fall back to role-based description
1266
+ if (roleChanges.length > 0) {
1267
+ const primaryRole = roleChanges[0];
1268
+ if (primaryRole.affectedElements.length > 0) {
1269
+ return primaryRole.affectedElements[0];
1270
+ }
1271
+ return ROLE_DESCRIPTIONS[primaryRole.role];
1272
+ }
1273
+ // Fall back to file names
1274
+ if (stagedFiles.length === 1) {
1275
+ const parts = stagedFiles[0].path.split('/');
1276
+ return parts[parts.length - 1].replace(/\.\w+$/, '');
1277
+ }
1278
+ return 'code';
1279
+ }
1280
+ /**
1281
+ * Generate a descriptive commit message
1282
+ * Uses semantic analysis when available for intent-based descriptions
1283
+ */
1284
+ static generateDescription(filesAffected, fileStatuses, stagedFiles, diff, semanticAnalysis) {
1285
+ // Check for comment-only changes (documentation in source files)
1286
+ if (this.isCommentOnlyChange(diff)) {
1287
+ const fileName = stagedFiles.length === 1 ? this.getFileName(stagedFiles[0].path) : null;
1288
+ // Detect type of comments being added
1289
+ const hasJSDoc = /^\+\s*\/\*\*/.test(diff) || /^\+\s*\*\s*@\w+/.test(diff);
1290
+ const hasFunctionComments = /^\+\s*\/\/.*\b(function|method|param|return|arg)/i.test(diff);
1291
+ if (hasJSDoc) {
1292
+ return fileName ? `add JSDoc comments to ${fileName}` : 'add JSDoc documentation';
1293
+ }
1294
+ else if (hasFunctionComments) {
1295
+ return fileName ? `add function documentation to ${fileName}` : 'add function documentation';
1296
+ }
1297
+ else {
1298
+ return fileName ? `add comments to ${fileName}` : 'add code comments';
1299
+ }
1300
+ }
1301
+ // Check for formatting-only changes (semicolons, whitespace, etc.)
1302
+ if (this.isFormattingOnlyChange(diff)) {
1303
+ return this.generateFormattingDescription(diff, stagedFiles);
1304
+ }
1305
+ // Use semantic analysis for intent-based descriptions when available
1306
+ if (semanticAnalysis && semanticAnalysis.roleChanges.length > 0) {
1307
+ return this.generateSemanticDescription(semanticAnalysis, stagedFiles);
1308
+ }
1309
+ // Single file changes (fallback)
1310
+ if (stagedFiles.length === 1) {
1311
+ const file = stagedFiles[0];
1312
+ const fileName = this.getFileName(file.path);
1313
+ if (file.status === 'A') {
1314
+ return `add ${fileName}`;
1315
+ }
1316
+ else if (file.status === 'D') {
1317
+ return `remove ${fileName}`;
1318
+ }
1319
+ else if (file.status === 'M') {
1320
+ return `update ${fileName}`;
1321
+ }
1322
+ else if (file.status === 'R') {
1323
+ return `rename ${fileName}`;
1324
+ }
1325
+ }
1326
+ // Multiple files of the same type
1327
+ if (filesAffected.test > 0 && filesAffected.source === 0) {
1328
+ return `update test files`;
1329
+ }
1330
+ if (filesAffected.docs > 0 && filesAffected.source === 0) {
1331
+ return `update documentation`;
1332
+ }
1333
+ if (filesAffected.config > 0 && filesAffected.source === 0) {
1334
+ return `update configuration`;
1335
+ }
1336
+ // Mixed changes - try to be descriptive
1337
+ const parts = [];
1338
+ if (fileStatuses.added > 0) {
1339
+ parts.push(`add ${fileStatuses.added} file${fileStatuses.added > 1 ? 's' : ''}`);
1340
+ }
1341
+ if (fileStatuses.modified > 0) {
1342
+ if (parts.length === 0) {
1343
+ parts.push(`update ${fileStatuses.modified} file${fileStatuses.modified > 1 ? 's' : ''}`);
1344
+ }
1345
+ }
1346
+ if (fileStatuses.deleted > 0) {
1347
+ parts.push(`remove ${fileStatuses.deleted} file${fileStatuses.deleted > 1 ? 's' : ''}`);
1348
+ }
1349
+ if (parts.length > 0) {
1350
+ return parts.join(' and ');
1351
+ }
1352
+ // Fallback
1353
+ return `update ${stagedFiles.length} file${stagedFiles.length > 1 ? 's' : ''}`;
1354
+ }
1355
+ /**
1356
+ * Generate description based on semantic analysis
1357
+ * Creates intent-based messages like "update UserProfile validation logic"
1358
+ */
1359
+ static generateSemanticDescription(semantic, stagedFiles) {
1360
+ const intent = semantic.primaryIntent;
1361
+ const role = semantic.primaryRole;
1362
+ const verb = INTENT_VERBS[intent].present;
1363
+ const roleDesc = ROLE_DESCRIPTIONS[role];
1364
+ // Handle renames specially - use "rename X to Y" format
1365
+ if (intent === 'rename' && semantic.renames && semantic.renames.length > 0) {
1366
+ const rename = semantic.renames[0];
1367
+ return `${verb} ${rename.oldName} to ${rename.newName}`;
1368
+ }
1369
+ // If we have specific elements affected, include them
1370
+ const whatChanged = semantic.whatChanged;
1371
+ // For single file with identified elements
1372
+ if (stagedFiles.length === 1 && whatChanged && whatChanged !== 'code') {
1373
+ // Check if whatChanged is a component/function name
1374
+ if (/^[A-Z][a-zA-Z0-9]*$/.test(whatChanged)) {
1375
+ // It's a component name
1376
+ return `${verb} ${whatChanged} ${roleDesc}`;
1377
+ }
1378
+ if (/^[a-z][a-zA-Z0-9]*$/.test(whatChanged)) {
1379
+ // It's a function name
1380
+ return `${verb} ${whatChanged} ${roleDesc}`;
1381
+ }
1382
+ // Multiple elements or descriptive text
1383
+ return `${verb} ${whatChanged}`;
1384
+ }
1385
+ // For multiple files or when we only have role info
1386
+ if (semantic.hasMultipleRoles) {
1387
+ // Changes span multiple concerns
1388
+ const roles = semantic.roleChanges
1389
+ .filter(r => r.significance > 20)
1390
+ .slice(0, 2)
1391
+ .map(r => ROLE_DESCRIPTIONS[r.role]);
1392
+ if (roles.length > 1) {
1393
+ return `${verb} ${roles.join(' and ')}`;
1394
+ }
1395
+ }
1396
+ // Single role with file context
1397
+ if (stagedFiles.length === 1) {
1398
+ const fileName = this.getFileName(stagedFiles[0].path).replace(/\.\w+$/, '');
1399
+ return `${verb} ${fileName} ${roleDesc}`;
1400
+ }
1401
+ // Multiple files, same role
1402
+ return `${verb} ${roleDesc}`;
1403
+ }
1404
+ /**
1405
+ * Determine scope from file paths
1406
+ */
1407
+ static determineScope(stagedFiles) {
1408
+ if (stagedFiles.length === 0)
1409
+ return undefined;
1410
+ const config = configService_1.ConfigService.getConfig();
1411
+ const paths = stagedFiles.map((f) => f.path);
1412
+ // Check config-based scope mappings first
1413
+ if (config.scopes && config.scopes.length > 0) {
1414
+ for (const mapping of config.scopes) {
1415
+ const matchingFiles = paths.filter((p) => p.includes(mapping.pattern));
1416
+ if (matchingFiles.length === paths.length) {
1417
+ return mapping.scope;
1418
+ }
1419
+ }
1420
+ // If most files match a pattern, use that scope
1421
+ for (const mapping of config.scopes) {
1422
+ const matchingFiles = paths.filter((p) => p.includes(mapping.pattern));
1423
+ if (matchingFiles.length > paths.length / 2) {
1424
+ return mapping.scope;
1425
+ }
1426
+ }
1427
+ }
1428
+ // Fallback to default heuristic
1429
+ const firstPath = paths[0];
1430
+ const parts = firstPath.split('/');
1431
+ if (parts.length > 1) {
1432
+ const potentialScope = parts[0];
1433
+ // Common scope names to look for
1434
+ const validScopes = [
1435
+ 'api',
1436
+ 'ui',
1437
+ 'auth',
1438
+ 'db',
1439
+ 'core',
1440
+ 'utils',
1441
+ 'components',
1442
+ 'services',
1443
+ ];
1444
+ if (validScopes.includes(potentialScope.toLowerCase())) {
1445
+ return potentialScope;
1446
+ }
1447
+ }
1448
+ return undefined;
1449
+ }
1450
+ /**
1451
+ * Extract file name from path
1452
+ */
1453
+ static getFileName(path) {
1454
+ const parts = path.split('/');
1455
+ return parts[parts.length - 1];
1456
+ }
1457
+ /**
1458
+ * Generate commit body for larger changes
1459
+ * Includes semantic role context when available
1460
+ */
1461
+ static generateBody(analysis) {
1462
+ const lines = [];
1463
+ const semantic = analysis.semanticAnalysis;
1464
+ // Include semantic context (WHY the change was made) for multi-role changes
1465
+ if (semantic && semantic.hasMultipleRoles) {
1466
+ lines.push('Changes:');
1467
+ for (const roleChange of semantic.roleChanges.filter(r => r.significance > 15).slice(0, 4)) {
1468
+ const elements = roleChange.affectedElements.length > 0
1469
+ ? ` (${roleChange.affectedElements.slice(0, 2).join(', ')})`
1470
+ : '';
1471
+ lines.push(`- ${roleChange.summary}${elements}`);
1472
+ }
1473
+ lines.push('');
1474
+ }
1475
+ // Only add file details for truly large changes
1476
+ if (!analysis.isLargeChange && lines.length === 0) {
1477
+ return undefined;
1478
+ }
1479
+ // Add file change details for large changes
1480
+ if (analysis.isLargeChange) {
1481
+ if (lines.length > 0) {
1482
+ lines.push('Files:');
1483
+ }
1484
+ if (analysis.fileChanges.added.length > 0) {
1485
+ lines.push(`- Add ${analysis.fileChanges.added.join(', ')}`);
1486
+ }
1487
+ if (analysis.fileChanges.modified.length > 0) {
1488
+ const files = analysis.fileChanges.modified.slice(0, 5);
1489
+ const suffix = analysis.fileChanges.modified.length > 5
1490
+ ? ` and ${analysis.fileChanges.modified.length - 5} more`
1491
+ : '';
1492
+ lines.push(`- Update ${files.join(', ')}${suffix}`);
1493
+ }
1494
+ if (analysis.fileChanges.deleted.length > 0) {
1495
+ lines.push(`- Remove ${analysis.fileChanges.deleted.join(', ')}`);
1496
+ }
1497
+ if (analysis.fileChanges.renamed.length > 0) {
1498
+ lines.push(`- Rename ${analysis.fileChanges.renamed.join(', ')}`);
1499
+ }
1500
+ }
1501
+ return lines.length > 0 ? lines.join('\n') : undefined;
1502
+ }
1503
+ /**
1504
+ * Apply a template to build the commit message subject line
1505
+ */
1506
+ static applyTemplate(template, type, scope, description, includeEmoji, isBreaking) {
1507
+ const emoji = includeEmoji ? COMMIT_EMOJIS[type] : '';
1508
+ const breakingIndicator = isBreaking ? '!' : '';
1509
+ let result = template
1510
+ .replace('{emoji}', emoji)
1511
+ .replace('{type}', type + breakingIndicator)
1512
+ .replace('{description}', description);
1513
+ // Handle scope - if no scope, use noScope template or remove scope placeholder
1514
+ if (scope) {
1515
+ result = result.replace('{scope}', scope);
1516
+ }
1517
+ else {
1518
+ // Remove scope and parentheses if no scope
1519
+ result = result.replace('({scope})', '').replace('{scope}', '');
1520
+ }
1521
+ // Clean up extra spaces
1522
+ result = result.replace(/\s+/g, ' ').trim();
1523
+ return result;
1524
+ }
1525
+ /**
1526
+ * Build full commit message string
1527
+ */
1528
+ static buildFullMessage(type, scope, description, body, includeEmoji, ticketInfo, isBreaking, breakingReasons) {
1529
+ const config = configService_1.ConfigService.getConfig();
1530
+ const includeBreakingFooter = config.breakingChangeDetection?.includeFooter !== false;
1531
+ const templates = config.templates;
1532
+ let full = '';
1533
+ // Use template if available
1534
+ if (templates) {
1535
+ const template = scope
1536
+ ? (templates.default || '{emoji} {type}({scope}): {description}')
1537
+ : (templates.noScope || '{emoji} {type}: {description}');
1538
+ full = this.applyTemplate(template, type, scope, description, includeEmoji, isBreaking);
1539
+ }
1540
+ else {
1541
+ // Fallback to original logic
1542
+ if (includeEmoji) {
1543
+ full += `${COMMIT_EMOJIS[type]} `;
1544
+ }
1545
+ full += type;
1546
+ if (scope) {
1547
+ full += `(${scope})`;
1548
+ }
1549
+ // Add breaking change indicator
1550
+ if (isBreaking) {
1551
+ full += '!';
1552
+ }
1553
+ full += `: ${description}`;
1554
+ }
1555
+ if (body) {
1556
+ full += `\n\n${body}`;
1557
+ }
1558
+ // Add BREAKING CHANGE footer if enabled and breaking
1559
+ if (isBreaking && includeBreakingFooter && breakingReasons && breakingReasons.length > 0) {
1560
+ full += '\n\nBREAKING CHANGE: ' + breakingReasons[0];
1561
+ if (breakingReasons.length > 1) {
1562
+ for (let i = 1; i < breakingReasons.length; i++) {
1563
+ full += `\n- ${breakingReasons[i]}`;
1564
+ }
1565
+ }
1566
+ }
1567
+ // Add ticket reference as footer
1568
+ if (ticketInfo) {
1569
+ const prefix = ticketInfo.prefix || 'Refs:';
1570
+ full += `\n\n${prefix} ${ticketInfo.id}`;
1571
+ }
1572
+ return full;
1573
+ }
1574
+ /**
1575
+ * Generate the final commit message
1576
+ */
1577
+ static generateCommitMessage() {
1578
+ const analysis = this.analyzeChanges();
1579
+ const config = configService_1.ConfigService.getConfig();
1580
+ // Determine emoji usage: config overrides, then history learning
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 full = this.buildFullMessage(analysis.commitType, analysis.scope, analysis.description, body, includeEmoji, ticketInfo, analysis.isBreakingChange, analysis.breakingChangeReasons);
1588
+ return {
1589
+ type: analysis.commitType,
1590
+ scope: analysis.scope,
1591
+ description: analysis.description,
1592
+ body,
1593
+ full,
1594
+ isBreaking: analysis.isBreakingChange,
1595
+ };
1596
+ }
1597
+ /**
1598
+ * Generate multiple message suggestions
1599
+ */
1600
+ static generateMultipleSuggestions() {
1601
+ const analysis = this.analyzeChanges();
1602
+ const config = configService_1.ConfigService.getConfig();
1603
+ // Determine emoji usage: config overrides, then history learning
1604
+ let includeEmoji = config.includeEmoji;
1605
+ if (includeEmoji === undefined) {
1606
+ includeEmoji = historyService_1.HistoryService.projectUsesEmojis();
1607
+ }
1608
+ const suggestions = [];
1609
+ const body = this.generateBody(analysis);
1610
+ const ticketInfo = historyService_1.HistoryService.detectTicketFromBranch();
1611
+ const { isBreakingChange, breakingChangeReasons } = analysis;
1612
+ // Try to get a better scope from history if none detected
1613
+ let scope = analysis.scope;
1614
+ if (!scope) {
1615
+ const stagedFiles = gitService_1.GitService.getStagedFiles();
1616
+ const filePaths = stagedFiles.map(f => f.path);
1617
+ scope = historyService_1.HistoryService.getSuggestedScope(filePaths);
1618
+ }
1619
+ // Suggestion 1: Default (with scope if detected, with ticket, with breaking change)
1620
+ const defaultFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
1621
+ suggestions.push({
1622
+ id: 1,
1623
+ label: isBreakingChange ? 'Breaking Change' : 'Recommended',
1624
+ message: {
1625
+ type: analysis.commitType,
1626
+ scope: scope,
1627
+ description: analysis.description,
1628
+ body,
1629
+ full: defaultFull,
1630
+ isBreaking: isBreakingChange,
1631
+ },
1632
+ });
1633
+ // Suggestion 2: Without scope (more concise)
1634
+ if (scope) {
1635
+ const noScopeFull = this.buildFullMessage(analysis.commitType, undefined, analysis.description, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
1636
+ suggestions.push({
1637
+ id: 2,
1638
+ label: 'Concise',
1639
+ message: {
1640
+ type: analysis.commitType,
1641
+ scope: undefined,
1642
+ description: analysis.description,
1643
+ body,
1644
+ full: noScopeFull,
1645
+ isBreaking: isBreakingChange,
1646
+ },
1647
+ });
1648
+ }
1649
+ // Suggestion 3: Alternative description style
1650
+ const altDescription = this.generateAlternativeDescription(analysis);
1651
+ if (altDescription && altDescription !== analysis.description) {
1652
+ const altFull = this.buildFullMessage(analysis.commitType, scope, altDescription, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
1653
+ suggestions.push({
1654
+ id: suggestions.length + 1,
1655
+ label: 'Detailed',
1656
+ message: {
1657
+ type: analysis.commitType,
1658
+ scope: scope,
1659
+ description: altDescription,
1660
+ body,
1661
+ full: altFull,
1662
+ isBreaking: isBreakingChange,
1663
+ },
1664
+ });
1665
+ }
1666
+ // Suggestion 4: Without body (compact) - only if body exists
1667
+ if (body) {
1668
+ const compactFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, undefined, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
1669
+ suggestions.push({
1670
+ id: suggestions.length + 1,
1671
+ label: 'Compact',
1672
+ message: {
1673
+ type: analysis.commitType,
1674
+ scope: scope,
1675
+ description: analysis.description,
1676
+ body: undefined,
1677
+ full: compactFull,
1678
+ isBreaking: isBreakingChange,
1679
+ },
1680
+ });
1681
+ }
1682
+ // Suggestion 5: Without ticket reference (if ticket was detected)
1683
+ if (ticketInfo) {
1684
+ const noTicketFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, body, includeEmoji, null, isBreakingChange, breakingChangeReasons);
1685
+ suggestions.push({
1686
+ id: suggestions.length + 1,
1687
+ label: 'No Ticket',
1688
+ message: {
1689
+ type: analysis.commitType,
1690
+ scope: scope,
1691
+ description: analysis.description,
1692
+ body,
1693
+ full: noTicketFull,
1694
+ isBreaking: isBreakingChange,
1695
+ },
1696
+ });
1697
+ }
1698
+ // Suggestion 6: Without breaking change indicator (if breaking change detected)
1699
+ if (isBreakingChange) {
1700
+ const noBreakingFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, body, includeEmoji, ticketInfo, false, []);
1701
+ suggestions.push({
1702
+ id: suggestions.length + 1,
1703
+ label: 'No Breaking Flag',
1704
+ message: {
1705
+ type: analysis.commitType,
1706
+ scope: scope,
1707
+ description: analysis.description,
1708
+ body,
1709
+ full: noBreakingFull,
1710
+ isBreaking: false,
1711
+ },
1712
+ });
1713
+ }
1714
+ return suggestions;
1715
+ }
1716
+ /**
1717
+ * Generate suggestions with optional AI enhancement
1718
+ */
1719
+ static async generateSuggestionsWithAI(useAI = false) {
1720
+ const suggestions = this.generateMultipleSuggestions();
1721
+ // If AI is not requested or not enabled, return regular suggestions
1722
+ if (!useAI || !aiService_1.AIService.isEnabled()) {
1723
+ return suggestions;
1724
+ }
1725
+ try {
1726
+ const analysis = this.analyzeChanges();
1727
+ const diff = gitService_1.GitService.getDiff();
1728
+ const config = configService_1.ConfigService.getConfig();
1729
+ // Determine emoji usage
1730
+ let includeEmoji = config.includeEmoji;
1731
+ if (includeEmoji === undefined) {
1732
+ includeEmoji = historyService_1.HistoryService.projectUsesEmojis();
1733
+ }
1734
+ const body = this.generateBody(analysis);
1735
+ const ticketInfo = historyService_1.HistoryService.detectTicketFromBranch();
1736
+ const { isBreakingChange, breakingChangeReasons } = analysis;
1737
+ // Get scope
1738
+ let scope = analysis.scope;
1739
+ if (!scope) {
1740
+ const stagedFiles = gitService_1.GitService.getStagedFiles();
1741
+ const filePaths = stagedFiles.map(f => f.path);
1742
+ scope = historyService_1.HistoryService.getSuggestedScope(filePaths);
1743
+ }
1744
+ // Get AI-enhanced description
1745
+ const aiResponse = await aiService_1.AIService.generateDescription(analysis, diff);
1746
+ if (aiResponse && aiResponse.description) {
1747
+ const aiDescription = aiResponse.description;
1748
+ const aiFull = this.buildFullMessage(analysis.commitType, scope, aiDescription, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
1749
+ // Insert AI suggestion at the beginning
1750
+ suggestions.unshift({
1751
+ id: 0,
1752
+ label: 'AI Enhanced',
1753
+ message: {
1754
+ type: analysis.commitType,
1755
+ scope: scope,
1756
+ description: aiDescription,
1757
+ body,
1758
+ full: aiFull,
1759
+ isBreaking: isBreakingChange,
1760
+ },
1761
+ });
1762
+ // Re-number suggestions
1763
+ suggestions.forEach((s, i) => {
1764
+ s.id = i + 1;
1765
+ });
1766
+ }
1767
+ }
1768
+ catch (error) {
1769
+ // AI failed - error already logged by AIService
1770
+ console.log('Falling back to rule-based suggestions.\n');
1771
+ }
1772
+ return suggestions;
1773
+ }
1774
+ /**
1775
+ * Generate an alternative description style
1776
+ */
1777
+ static generateAlternativeDescription(analysis) {
1778
+ const { fileChanges, filesAffected } = analysis;
1779
+ const totalFiles = fileChanges.added.length + fileChanges.modified.length +
1780
+ fileChanges.deleted.length + fileChanges.renamed.length;
1781
+ // For single file, provide more detail
1782
+ if (totalFiles === 1) {
1783
+ if (fileChanges.added.length === 1) {
1784
+ return `implement ${fileChanges.added[0]}`;
1785
+ }
1786
+ if (fileChanges.modified.length === 1) {
1787
+ return `improve ${fileChanges.modified[0]}`;
1788
+ }
1789
+ }
1790
+ // For multiple files, be more descriptive about categories
1791
+ const parts = [];
1792
+ if (filesAffected.source > 0) {
1793
+ parts.push(`${filesAffected.source} source file${filesAffected.source > 1 ? 's' : ''}`);
1794
+ }
1795
+ if (filesAffected.test > 0) {
1796
+ parts.push(`${filesAffected.test} test${filesAffected.test > 1 ? 's' : ''}`);
1797
+ }
1798
+ if (filesAffected.docs > 0) {
1799
+ parts.push(`${filesAffected.docs} doc${filesAffected.docs > 1 ? 's' : ''}`);
1800
+ }
1801
+ if (filesAffected.config > 0) {
1802
+ parts.push(`${filesAffected.config} config${filesAffected.config > 1 ? 's' : ''}`);
1803
+ }
1804
+ if (parts.length > 0) {
1805
+ return `update ${parts.join(', ')}`;
1806
+ }
1807
+ return analysis.description;
1808
+ }
1809
+ }
1810
+ exports.AnalyzerService = AnalyzerService;
1811
+ //# sourceMappingURL=analyzerService.js.map