@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.
- package/README.md +41 -3
- package/dist/services/analyzerService.d.ts +59 -0
- package/dist/services/analyzerService.d.ts.map +1 -1
- package/dist/services/analyzerService.js +723 -37
- 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
|
/**
|
|
@@ -200,22 +395,36 @@ class AnalyzerService {
|
|
|
200
395
|
return 'docs';
|
|
201
396
|
}
|
|
202
397
|
// === STYLE DETECTION ===
|
|
203
|
-
//
|
|
204
|
-
|
|
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.
|
|
210
|
-
p.includes('styles
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
594
|
-
|
|
1259
|
+
// Only add file details for truly large changes
|
|
1260
|
+
if (!analysis.isLargeChange && lines.length === 0) {
|
|
1261
|
+
return undefined;
|
|
595
1262
|
}
|
|
596
|
-
|
|
597
|
-
|
|
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
|
}
|