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