@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.
- package/README.md +41 -3
- package/dist/services/analyzerService.d.ts +53 -0
- package/dist/services/analyzerService.d.ts.map +1 -1
- package/dist/services/analyzerService.js +642 -25
- package/dist/services/analyzerService.js.map +1 -1
- package/dist/services/analyzerService.test.js +130 -0
- package/dist/services/analyzerService.test.js.map +1 -1
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
88
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
594
|
-
|
|
1190
|
+
// Only add file details for truly large changes
|
|
1191
|
+
if (!analysis.isLargeChange && lines.length === 0) {
|
|
1192
|
+
return undefined;
|
|
595
1193
|
}
|
|
596
|
-
|
|
597
|
-
|
|
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
|
}
|