@m3hti/commit-genie 1.2.0 → 1.2.2

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.
@@ -40,6 +40,181 @@ const BREAKING_PATTERNS = [
40
40
  // Major version bump in package.json
41
41
  /^-\s*"version":\s*"\d+/gm,
42
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
+ };
43
218
  class AnalyzerService {
44
219
  /**
45
220
  * Analyze staged changes and return structured analysis
@@ -84,18 +259,37 @@ class AnalyzerService {
84
259
  const totalChanges = stats.insertions + stats.deletions;
85
260
  const isLargeChange = stagedFiles.length >= 3 || totalChanges >= 100;
86
261
  // Determine commit type based on file types and changes
87
- const commitType = this.determineCommitType(filesAffected, diff, stagedFiles);
88
- // Generate description
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)
89
287
  const description = this.generateDescription(filesAffected, {
90
288
  added: fileChanges.added.length,
91
289
  modified: fileChanges.modified.length,
92
290
  deleted: fileChanges.deleted.length,
93
291
  renamed: fileChanges.renamed.length
94
- }, stagedFiles, diff);
95
- // Determine scope if applicable
96
- const scope = this.determineScope(stagedFiles);
97
- // Detect breaking changes
98
- const { isBreaking, reasons } = this.detectBreakingChanges(diff, stagedFiles);
292
+ }, stagedFiles, diff, semanticAnalysis);
99
293
  return {
100
294
  commitType,
101
295
  scope,
@@ -105,6 +299,7 @@ class AnalyzerService {
105
299
  isLargeChange,
106
300
  isBreakingChange: isBreaking,
107
301
  breakingChangeReasons: reasons,
302
+ semanticAnalysis,
108
303
  };
109
304
  }
110
305
  /**
@@ -200,22 +395,36 @@ class AnalyzerService {
200
395
  return 'docs';
201
396
  }
202
397
  // === STYLE DETECTION ===
203
- // Check for style/formatting files
204
- const isStyleChange = filePaths.some(p => p.endsWith('.css') ||
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') ||
205
402
  p.endsWith('.scss') ||
206
403
  p.endsWith('.sass') ||
207
404
  p.endsWith('.less') ||
208
405
  p.endsWith('.styl') ||
209
- p.includes('.style') ||
210
- p.includes('styles/'));
211
- // Check for formatting-only changes (whitespace, semicolons, quotes)
212
- const formattingPatterns = [
213
- /^[+-]\s*$/gm, // Only whitespace changes
214
- /^[+-]\s*['"`];?\s*$/gm, // Quote changes
215
- /^[+-].*;\s*$/gm, // Semicolon additions/removals
216
- ];
217
- const isFormattingChange = formattingPatterns.some(p => p.test(diff));
218
- if (isStyleChange || (isFormattingChange && !this.hasLogicChanges(diff))) {
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)) {
219
428
  return 'style';
220
429
  }
221
430
  // === PERFORMANCE DETECTION ===
@@ -412,6 +621,61 @@ class AnalyzerService {
412
621
  }
413
622
  return false;
414
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
+ }
415
679
  /**
416
680
  * Check if the diff contains only comment changes (documentation)
417
681
  * Returns true if ALL changes are comments (no code changes)
@@ -453,10 +717,364 @@ class AnalyzerService {
453
717
  }
454
718
  return hasCommentChanges;
455
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
+ }
456
1073
  /**
457
1074
  * Generate a descriptive commit message
1075
+ * Uses semantic analysis when available for intent-based descriptions
458
1076
  */
459
- static generateDescription(filesAffected, fileStatuses, stagedFiles, diff) {
1077
+ static generateDescription(filesAffected, fileStatuses, stagedFiles, diff, semanticAnalysis) {
460
1078
  // Check for comment-only changes (documentation in source files)
461
1079
  if (this.isCommentOnlyChange(diff)) {
462
1080
  const fileName = stagedFiles.length === 1 ? this.getFileName(stagedFiles[0].path) : null;
@@ -473,7 +1091,11 @@ class AnalyzerService {
473
1091
  return fileName ? `add comments to ${fileName}` : 'add code comments';
474
1092
  }
475
1093
  }
476
- // Single file changes
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)
477
1099
  if (stagedFiles.length === 1) {
478
1100
  const file = stagedFiles[0];
479
1101
  const fileName = this.getFileName(file.path);
@@ -519,6 +1141,50 @@ class AnalyzerService {
519
1141
  // Fallback
520
1142
  return `update ${stagedFiles.length} file${stagedFiles.length > 1 ? 's' : ''}`;
521
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
+ }
522
1188
  /**
523
1189
  * Determine scope from file paths
524
1190
  */
@@ -574,27 +1240,47 @@ class AnalyzerService {
574
1240
  }
575
1241
  /**
576
1242
  * Generate commit body for larger changes
1243
+ * Includes semantic role context when available
577
1244
  */
578
1245
  static generateBody(analysis) {
579
- if (!analysis.isLargeChange) {
580
- return undefined;
581
- }
582
1246
  const lines = [];
583
- if (analysis.fileChanges.added.length > 0) {
584
- lines.push(`- Add ${analysis.fileChanges.added.join(', ')}`);
585
- }
586
- if (analysis.fileChanges.modified.length > 0) {
587
- const files = analysis.fileChanges.modified.slice(0, 5);
588
- const suffix = analysis.fileChanges.modified.length > 5
589
- ? ` and ${analysis.fileChanges.modified.length - 5} more`
590
- : '';
591
- lines.push(`- Update ${files.join(', ')}${suffix}`);
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('');
592
1258
  }
593
- if (analysis.fileChanges.deleted.length > 0) {
594
- lines.push(`- Remove ${analysis.fileChanges.deleted.join(', ')}`);
1259
+ // Only add file details for truly large changes
1260
+ if (!analysis.isLargeChange && lines.length === 0) {
1261
+ return undefined;
595
1262
  }
596
- if (analysis.fileChanges.renamed.length > 0) {
597
- lines.push(`- Rename ${analysis.fileChanges.renamed.join(', ')}`);
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
+ }
598
1284
  }
599
1285
  return lines.length > 0 ? lines.join('\n') : undefined;
600
1286
  }