@m3hti/commit-genie 1.2.0 → 1.2.1

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
  /**
@@ -453,10 +648,364 @@ class AnalyzerService {
453
648
  }
454
649
  return hasCommentChanges;
455
650
  }
651
+ /**
652
+ * Perform semantic analysis on the diff to understand the nature of changes
653
+ * This provides intent-based understanding rather than line-count metrics
654
+ */
655
+ static analyzeSemanticChanges(diff, stagedFiles) {
656
+ const roleChanges = this.detectRoleChanges(diff, stagedFiles);
657
+ const primaryRole = this.determinePrimaryRole(roleChanges);
658
+ const primaryIntent = this.determineIntent(diff, stagedFiles, roleChanges);
659
+ const affectedElements = this.extractAffectedElements(diff);
660
+ // Generate human-readable descriptions
661
+ const intentDescription = this.generateIntentDescription(primaryIntent, primaryRole, roleChanges);
662
+ const whatChanged = this.generateWhatChanged(roleChanges, affectedElements, stagedFiles);
663
+ return {
664
+ primaryRole,
665
+ primaryIntent,
666
+ roleChanges,
667
+ intentDescription,
668
+ whatChanged,
669
+ hasMultipleRoles: roleChanges.filter(r => r.significance > 20).length > 1,
670
+ };
671
+ }
672
+ /**
673
+ * Detect which roles are affected by the changes and calculate semantic significance
674
+ * Significance is NOT based on line count - it's based on the semantic weight of patterns
675
+ */
676
+ static detectRoleChanges(diff, _stagedFiles) {
677
+ const roleChanges = [];
678
+ // Check each role for matches
679
+ for (const [role, patterns] of Object.entries(ROLE_PATTERNS)) {
680
+ if (role === 'unknown' || patterns.length === 0)
681
+ continue;
682
+ let matchCount = 0;
683
+ let highValueMatches = 0;
684
+ for (const pattern of patterns) {
685
+ pattern.lastIndex = 0;
686
+ const matches = diff.match(pattern);
687
+ if (matches) {
688
+ matchCount += matches.length;
689
+ // Some patterns indicate more significant changes
690
+ if (this.isHighValuePattern(pattern, role)) {
691
+ highValueMatches += matches.length;
692
+ }
693
+ }
694
+ }
695
+ if (matchCount > 0) {
696
+ // Calculate significance based on pattern matches, not line counts
697
+ // High-value patterns contribute more to significance
698
+ const baseSignificance = Math.min(matchCount * 10, 40);
699
+ const highValueBonus = highValueMatches * 15;
700
+ const significance = Math.min(baseSignificance + highValueBonus, 100);
701
+ const intent = this.detectRoleIntent(diff, role);
702
+ const summary = this.generateRoleSummary(role, intent, matchCount);
703
+ roleChanges.push({
704
+ role,
705
+ intent,
706
+ significance,
707
+ summary,
708
+ affectedElements: this.extractElementsForRole(diff, role),
709
+ });
710
+ }
711
+ }
712
+ // Sort by significance (highest first)
713
+ return roleChanges.sort((a, b) => b.significance - a.significance);
714
+ }
715
+ /**
716
+ * Determine if a pattern represents a high-value semantic change
717
+ */
718
+ static isHighValuePattern(pattern, role) {
719
+ const patternStr = pattern.source;
720
+ // High-value patterns for each role
721
+ const highValueIndicators = {
722
+ ui: ['<[A-Z]', 'className', 'onClick', 'onSubmit', 'aria-'],
723
+ logic: ['function', 'class', 'if\\s*\\(', 'switch', 'try\\s*\\{', 'throw'],
724
+ data: ['useState', 'useReducer', 'interface', 'type\\s+\\w+'],
725
+ style: ['styled\\.', 'css`', 'theme\\.'],
726
+ api: ['fetch\\s*\\(', 'axios', 'useQuery', 'useMutation', 'graphql'],
727
+ config: ['process\\.env', 'CONFIG\\.', 'export\\s+(const|let)\\s+[A-Z_]+'],
728
+ test: ['describe\\s*\\(', 'it\\s*\\(', 'test\\s*\\(', 'expect\\s*\\('],
729
+ unknown: [],
730
+ };
731
+ return highValueIndicators[role].some(indicator => patternStr.includes(indicator));
732
+ }
733
+ /**
734
+ * Detect the intent for changes in a specific role
735
+ */
736
+ static detectRoleIntent(diff, _role) {
737
+ // Check for intent patterns (role context reserved for future use)
738
+ const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
739
+ const removedLines = (diff.match(/^-[^-]/gm) || []).length;
740
+ // Check for add patterns in this role's context
741
+ for (const pattern of INTENT_PATTERNS.add) {
742
+ if (pattern.test(diff)) {
743
+ // If adding new constructs, it's an 'add' intent
744
+ return 'add';
745
+ }
746
+ }
747
+ // Check for remove patterns
748
+ for (const pattern of INTENT_PATTERNS.remove) {
749
+ if (pattern.test(diff)) {
750
+ return 'remove';
751
+ }
752
+ }
753
+ // Check for fix patterns
754
+ for (const pattern of INTENT_PATTERNS.fix) {
755
+ if (pattern.test(diff)) {
756
+ return 'fix';
757
+ }
758
+ }
759
+ // Check for enhance patterns
760
+ for (const pattern of INTENT_PATTERNS.enhance) {
761
+ if (pattern.test(diff)) {
762
+ return 'enhance';
763
+ }
764
+ }
765
+ // Determine based on add/remove ratio
766
+ if (addedLines > 0 && removedLines === 0) {
767
+ return 'add';
768
+ }
769
+ else if (removedLines > 0 && addedLines === 0) {
770
+ return 'remove';
771
+ }
772
+ else if (addedLines > 0 && removedLines > 0) {
773
+ const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
774
+ if (ratio > 0.5) {
775
+ return 'refactor'; // Balanced changes suggest refactoring
776
+ }
777
+ return addedLines > removedLines ? 'add' : 'modify';
778
+ }
779
+ return 'modify';
780
+ }
781
+ /**
782
+ * Determine the primary role from all detected role changes
783
+ */
784
+ static determinePrimaryRole(roleChanges) {
785
+ if (roleChanges.length === 0) {
786
+ return 'unknown';
787
+ }
788
+ // The role with highest significance is primary
789
+ // But if multiple roles have similar significance, prefer more specific ones
790
+ const topRole = roleChanges[0];
791
+ // If there's a close second that's more specific, consider it
792
+ if (roleChanges.length > 1) {
793
+ const secondRole = roleChanges[1];
794
+ const significanceDiff = topRole.significance - secondRole.significance;
795
+ // If within 15 points and second is more specific (api > logic > ui)
796
+ if (significanceDiff <= 15) {
797
+ const specificityOrder = ['api', 'data', 'style', 'logic', 'ui', 'config', 'test', 'unknown'];
798
+ const topIndex = specificityOrder.indexOf(topRole.role);
799
+ const secondIndex = specificityOrder.indexOf(secondRole.role);
800
+ if (secondIndex < topIndex) {
801
+ return secondRole.role;
802
+ }
803
+ }
804
+ }
805
+ return topRole.role;
806
+ }
807
+ /**
808
+ * Determine the overall intent of the changes
809
+ */
810
+ static determineIntent(diff, stagedFiles, roleChanges) {
811
+ // Check file statuses first
812
+ const hasOnlyAdded = stagedFiles.every(f => f.status === 'A');
813
+ const hasOnlyDeleted = stagedFiles.every(f => f.status === 'D');
814
+ const hasOnlyModified = stagedFiles.every(f => f.status === 'M');
815
+ if (hasOnlyAdded) {
816
+ return 'add';
817
+ }
818
+ if (hasOnlyDeleted) {
819
+ return 'remove';
820
+ }
821
+ // If we have role changes, use the most significant role's intent
822
+ if (roleChanges.length > 0) {
823
+ const primaryRoleChange = roleChanges[0];
824
+ // Special case: if the primary intent is 'fix' and we see validation patterns
825
+ // even in non-modified files, treat it as a fix
826
+ if (hasOnlyModified && primaryRoleChange.intent === 'fix') {
827
+ return 'fix';
828
+ }
829
+ // If we're enhancing (useMemo, useCallback, etc.), that takes precedence
830
+ if (roleChanges.some(r => r.intent === 'enhance')) {
831
+ return 'enhance';
832
+ }
833
+ return primaryRoleChange.intent;
834
+ }
835
+ // Fallback to diff analysis
836
+ const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
837
+ const removedLines = (diff.match(/^-[^-]/gm) || []).length;
838
+ if (addedLines > 0 && removedLines === 0) {
839
+ return 'add';
840
+ }
841
+ if (removedLines > 0 && addedLines === 0) {
842
+ return 'remove';
843
+ }
844
+ const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
845
+ if (ratio > 0.6) {
846
+ return 'refactor';
847
+ }
848
+ return 'modify';
849
+ }
850
+ /**
851
+ * Extract affected element names (components, functions, etc.) from the diff
852
+ */
853
+ static extractAffectedElements(diff) {
854
+ const elements = [];
855
+ // Extract component names (React/JSX)
856
+ const componentMatches = diff.match(/^\+.*(?:function|const)\s+([A-Z][a-zA-Z0-9]*)/gm);
857
+ if (componentMatches) {
858
+ for (const match of componentMatches) {
859
+ const nameMatch = match.match(/(?:function|const)\s+([A-Z][a-zA-Z0-9]*)/);
860
+ if (nameMatch) {
861
+ elements.push(nameMatch[1]);
862
+ }
863
+ }
864
+ }
865
+ // Extract function names
866
+ const functionMatches = diff.match(/^\+.*(?:function|const)\s+([a-z][a-zA-Z0-9]*)\s*[=(]/gm);
867
+ if (functionMatches) {
868
+ for (const match of functionMatches) {
869
+ const nameMatch = match.match(/(?:function|const)\s+([a-z][a-zA-Z0-9]*)/);
870
+ if (nameMatch && !elements.includes(nameMatch[1])) {
871
+ elements.push(nameMatch[1]);
872
+ }
873
+ }
874
+ }
875
+ // Extract interface/type names
876
+ const typeMatches = diff.match(/^\+.*(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/gm);
877
+ if (typeMatches) {
878
+ for (const match of typeMatches) {
879
+ const nameMatch = match.match(/(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/);
880
+ if (nameMatch && !elements.includes(nameMatch[1])) {
881
+ elements.push(nameMatch[1]);
882
+ }
883
+ }
884
+ }
885
+ // Extract class names
886
+ const classMatches = diff.match(/^\+.*class\s+([A-Z][a-zA-Z0-9]*)/gm);
887
+ if (classMatches) {
888
+ for (const match of classMatches) {
889
+ const nameMatch = match.match(/class\s+([A-Z][a-zA-Z0-9]*)/);
890
+ if (nameMatch && !elements.includes(nameMatch[1])) {
891
+ elements.push(nameMatch[1]);
892
+ }
893
+ }
894
+ }
895
+ return elements.slice(0, 5); // Limit to top 5 elements
896
+ }
897
+ /**
898
+ * Extract affected elements for a specific role
899
+ */
900
+ static extractElementsForRole(diff, role) {
901
+ const elements = [];
902
+ switch (role) {
903
+ case 'ui':
904
+ // Extract JSX component names being used
905
+ const jsxMatches = diff.match(/^\+.*<([A-Z][a-zA-Z0-9]*)/gm);
906
+ if (jsxMatches) {
907
+ for (const match of jsxMatches) {
908
+ const nameMatch = match.match(/<([A-Z][a-zA-Z0-9]*)/);
909
+ if (nameMatch && !elements.includes(nameMatch[1])) {
910
+ elements.push(nameMatch[1]);
911
+ }
912
+ }
913
+ }
914
+ break;
915
+ case 'data':
916
+ // Extract state variable names
917
+ const stateMatches = diff.match(/^\+.*const\s+\[([a-z][a-zA-Z0-9]*),/gm);
918
+ if (stateMatches) {
919
+ for (const match of stateMatches) {
920
+ const nameMatch = match.match(/const\s+\[([a-z][a-zA-Z0-9]*)/);
921
+ if (nameMatch) {
922
+ elements.push(nameMatch[1]);
923
+ }
924
+ }
925
+ }
926
+ break;
927
+ case 'api':
928
+ // Extract API endpoints
929
+ const apiMatches = diff.match(/['"`]\/api\/[^'"`]+['"`]/g);
930
+ if (apiMatches) {
931
+ elements.push(...apiMatches.map(m => m.replace(/['"`]/g, '')));
932
+ }
933
+ break;
934
+ default:
935
+ // Use generic extraction
936
+ return this.extractAffectedElements(diff).slice(0, 3);
937
+ }
938
+ return elements.slice(0, 3);
939
+ }
940
+ /**
941
+ * Generate a human-readable summary for a role change
942
+ */
943
+ static generateRoleSummary(role, intent, matchCount) {
944
+ const roleDesc = ROLE_DESCRIPTIONS[role];
945
+ const intentVerb = INTENT_VERBS[intent].past;
946
+ if (matchCount === 1) {
947
+ return `${intentVerb} ${roleDesc}`;
948
+ }
949
+ return `${intentVerb} ${roleDesc} (${matchCount} changes)`;
950
+ }
951
+ /**
952
+ * Generate the WHY description for the commit
953
+ */
954
+ static generateIntentDescription(intent, role, roleChanges) {
955
+ const roleDesc = ROLE_DESCRIPTIONS[role];
956
+ switch (intent) {
957
+ case 'add':
958
+ return `to add new ${roleDesc}`;
959
+ case 'fix':
960
+ return `to fix ${roleDesc} issues`;
961
+ case 'refactor':
962
+ return `to improve ${roleDesc} structure`;
963
+ case 'enhance':
964
+ return `to optimize ${roleDesc} performance`;
965
+ case 'remove':
966
+ return `to remove unused ${roleDesc}`;
967
+ case 'modify':
968
+ default:
969
+ if (roleChanges.length > 1) {
970
+ return `to update ${roleDesc} and related code`;
971
+ }
972
+ return `to update ${roleDesc}`;
973
+ }
974
+ }
975
+ /**
976
+ * Generate the WHAT changed description
977
+ */
978
+ static generateWhatChanged(roleChanges, affectedElements, stagedFiles) {
979
+ // If we have specific elements, use them
980
+ if (affectedElements.length > 0) {
981
+ if (affectedElements.length === 1) {
982
+ return affectedElements[0];
983
+ }
984
+ if (affectedElements.length <= 3) {
985
+ return affectedElements.join(', ');
986
+ }
987
+ return `${affectedElements.slice(0, 2).join(', ')} and ${affectedElements.length - 2} more`;
988
+ }
989
+ // Fall back to role-based description
990
+ if (roleChanges.length > 0) {
991
+ const primaryRole = roleChanges[0];
992
+ if (primaryRole.affectedElements.length > 0) {
993
+ return primaryRole.affectedElements[0];
994
+ }
995
+ return ROLE_DESCRIPTIONS[primaryRole.role];
996
+ }
997
+ // Fall back to file names
998
+ if (stagedFiles.length === 1) {
999
+ const parts = stagedFiles[0].path.split('/');
1000
+ return parts[parts.length - 1].replace(/\.\w+$/, '');
1001
+ }
1002
+ return 'code';
1003
+ }
456
1004
  /**
457
1005
  * Generate a descriptive commit message
1006
+ * Uses semantic analysis when available for intent-based descriptions
458
1007
  */
459
- static generateDescription(filesAffected, fileStatuses, stagedFiles, diff) {
1008
+ static generateDescription(filesAffected, fileStatuses, stagedFiles, diff, semanticAnalysis) {
460
1009
  // Check for comment-only changes (documentation in source files)
461
1010
  if (this.isCommentOnlyChange(diff)) {
462
1011
  const fileName = stagedFiles.length === 1 ? this.getFileName(stagedFiles[0].path) : null;
@@ -473,7 +1022,11 @@ class AnalyzerService {
473
1022
  return fileName ? `add comments to ${fileName}` : 'add code comments';
474
1023
  }
475
1024
  }
476
- // Single file changes
1025
+ // Use semantic analysis for intent-based descriptions when available
1026
+ if (semanticAnalysis && semanticAnalysis.roleChanges.length > 0) {
1027
+ return this.generateSemanticDescription(semanticAnalysis, stagedFiles);
1028
+ }
1029
+ // Single file changes (fallback)
477
1030
  if (stagedFiles.length === 1) {
478
1031
  const file = stagedFiles[0];
479
1032
  const fileName = this.getFileName(file.path);
@@ -519,6 +1072,50 @@ class AnalyzerService {
519
1072
  // Fallback
520
1073
  return `update ${stagedFiles.length} file${stagedFiles.length > 1 ? 's' : ''}`;
521
1074
  }
1075
+ /**
1076
+ * Generate description based on semantic analysis
1077
+ * Creates intent-based messages like "update UserProfile validation logic"
1078
+ */
1079
+ static generateSemanticDescription(semantic, stagedFiles) {
1080
+ const intent = semantic.primaryIntent;
1081
+ const role = semantic.primaryRole;
1082
+ const verb = INTENT_VERBS[intent].present;
1083
+ const roleDesc = ROLE_DESCRIPTIONS[role];
1084
+ // If we have specific elements affected, include them
1085
+ const whatChanged = semantic.whatChanged;
1086
+ // For single file with identified elements
1087
+ if (stagedFiles.length === 1 && whatChanged && whatChanged !== 'code') {
1088
+ // Check if whatChanged is a component/function name
1089
+ if (/^[A-Z][a-zA-Z0-9]*$/.test(whatChanged)) {
1090
+ // It's a component name
1091
+ return `${verb} ${whatChanged} ${roleDesc}`;
1092
+ }
1093
+ if (/^[a-z][a-zA-Z0-9]*$/.test(whatChanged)) {
1094
+ // It's a function name
1095
+ return `${verb} ${whatChanged} ${roleDesc}`;
1096
+ }
1097
+ // Multiple elements or descriptive text
1098
+ return `${verb} ${whatChanged}`;
1099
+ }
1100
+ // For multiple files or when we only have role info
1101
+ if (semantic.hasMultipleRoles) {
1102
+ // Changes span multiple concerns
1103
+ const roles = semantic.roleChanges
1104
+ .filter(r => r.significance > 20)
1105
+ .slice(0, 2)
1106
+ .map(r => ROLE_DESCRIPTIONS[r.role]);
1107
+ if (roles.length > 1) {
1108
+ return `${verb} ${roles.join(' and ')}`;
1109
+ }
1110
+ }
1111
+ // Single role with file context
1112
+ if (stagedFiles.length === 1) {
1113
+ const fileName = this.getFileName(stagedFiles[0].path).replace(/\.\w+$/, '');
1114
+ return `${verb} ${fileName} ${roleDesc}`;
1115
+ }
1116
+ // Multiple files, same role
1117
+ return `${verb} ${roleDesc}`;
1118
+ }
522
1119
  /**
523
1120
  * Determine scope from file paths
524
1121
  */
@@ -574,27 +1171,47 @@ class AnalyzerService {
574
1171
  }
575
1172
  /**
576
1173
  * Generate commit body for larger changes
1174
+ * Includes semantic role context when available
577
1175
  */
578
1176
  static generateBody(analysis) {
579
- if (!analysis.isLargeChange) {
580
- return undefined;
581
- }
582
1177
  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}`);
1178
+ const semantic = analysis.semanticAnalysis;
1179
+ // Include semantic context (WHY the change was made) for multi-role changes
1180
+ if (semantic && semantic.hasMultipleRoles) {
1181
+ lines.push('Changes:');
1182
+ for (const roleChange of semantic.roleChanges.filter(r => r.significance > 15).slice(0, 4)) {
1183
+ const elements = roleChange.affectedElements.length > 0
1184
+ ? ` (${roleChange.affectedElements.slice(0, 2).join(', ')})`
1185
+ : '';
1186
+ lines.push(`- ${roleChange.summary}${elements}`);
1187
+ }
1188
+ lines.push('');
592
1189
  }
593
- if (analysis.fileChanges.deleted.length > 0) {
594
- lines.push(`- Remove ${analysis.fileChanges.deleted.join(', ')}`);
1190
+ // Only add file details for truly large changes
1191
+ if (!analysis.isLargeChange && lines.length === 0) {
1192
+ return undefined;
595
1193
  }
596
- if (analysis.fileChanges.renamed.length > 0) {
597
- lines.push(`- Rename ${analysis.fileChanges.renamed.join(', ')}`);
1194
+ // Add file change details for large changes
1195
+ if (analysis.isLargeChange) {
1196
+ if (lines.length > 0) {
1197
+ lines.push('Files:');
1198
+ }
1199
+ if (analysis.fileChanges.added.length > 0) {
1200
+ lines.push(`- Add ${analysis.fileChanges.added.join(', ')}`);
1201
+ }
1202
+ if (analysis.fileChanges.modified.length > 0) {
1203
+ const files = analysis.fileChanges.modified.slice(0, 5);
1204
+ const suffix = analysis.fileChanges.modified.length > 5
1205
+ ? ` and ${analysis.fileChanges.modified.length - 5} more`
1206
+ : '';
1207
+ lines.push(`- Update ${files.join(', ')}${suffix}`);
1208
+ }
1209
+ if (analysis.fileChanges.deleted.length > 0) {
1210
+ lines.push(`- Remove ${analysis.fileChanges.deleted.join(', ')}`);
1211
+ }
1212
+ if (analysis.fileChanges.renamed.length > 0) {
1213
+ lines.push(`- Rename ${analysis.fileChanges.renamed.join(', ')}`);
1214
+ }
598
1215
  }
599
1216
  return lines.length > 0 ? lines.join('\n') : undefined;
600
1217
  }