@m3hti/commit-genie 1.2.4 → 1.2.5

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