@m3hti/commit-genie 3.1.0 โ 3.2.0
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/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +10 -0
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/generate.test.js +38 -0
- package/dist/commands/generate.test.js.map +1 -1
- package/dist/services/analyzerService.d.ts +0 -157
- package/dist/services/analyzerService.d.ts.map +1 -1
- package/dist/services/analyzerService.js +75 -2010
- package/dist/services/analyzerService.js.map +1 -1
- package/dist/services/analyzerService.test.js +66 -4
- package/dist/services/analyzerService.test.js.map +1 -1
- package/dist/services/breakingChangeDetector.d.ts +9 -0
- package/dist/services/breakingChangeDetector.d.ts.map +1 -0
- package/dist/services/breakingChangeDetector.js +76 -0
- package/dist/services/breakingChangeDetector.js.map +1 -0
- package/dist/services/commitTypeDetector.d.ts +39 -0
- package/dist/services/commitTypeDetector.d.ts.map +1 -0
- package/dist/services/commitTypeDetector.js +510 -0
- package/dist/services/commitTypeDetector.js.map +1 -0
- package/dist/services/descriptionGenerator.d.ts +58 -0
- package/dist/services/descriptionGenerator.d.ts.map +1 -0
- package/dist/services/descriptionGenerator.js +566 -0
- package/dist/services/descriptionGenerator.js.map +1 -0
- package/dist/services/gitService.test.js +242 -24
- package/dist/services/gitService.test.js.map +1 -1
- package/dist/services/hookService.test.d.ts +2 -0
- package/dist/services/hookService.test.d.ts.map +1 -0
- package/dist/services/hookService.test.js +182 -0
- package/dist/services/hookService.test.js.map +1 -0
- package/dist/services/lintService.test.d.ts +2 -0
- package/dist/services/lintService.test.d.ts.map +1 -0
- package/dist/services/lintService.test.js +288 -0
- package/dist/services/lintService.test.js.map +1 -0
- package/dist/services/messageBuilder.d.ts +16 -0
- package/dist/services/messageBuilder.d.ts.map +1 -0
- package/dist/services/messageBuilder.js +135 -0
- package/dist/services/messageBuilder.js.map +1 -0
- package/dist/services/scopeDetector.d.ts +6 -0
- package/dist/services/scopeDetector.d.ts.map +1 -0
- package/dist/services/scopeDetector.js +51 -0
- package/dist/services/scopeDetector.js.map +1 -0
- package/dist/services/semanticAnalyzer.d.ts +25 -0
- package/dist/services/semanticAnalyzer.d.ts.map +1 -0
- package/dist/services/semanticAnalyzer.js +713 -0
- package/dist/services/semanticAnalyzer.js.map +1 -0
- package/dist/services/splitService.test.d.ts +2 -0
- package/dist/services/splitService.test.d.ts.map +1 -0
- package/dist/services/splitService.test.js +190 -0
- package/dist/services/splitService.test.js.map +1 -0
- package/dist/services/statsService.test.d.ts +2 -0
- package/dist/services/statsService.test.d.ts.map +1 -0
- package/dist/services/statsService.test.js +211 -0
- package/dist/services/statsService.test.js.map +1 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -6,28 +6,14 @@ const configService_1 = require("./configService");
|
|
|
6
6
|
const historyService_1 = require("./historyService");
|
|
7
7
|
const aiService_1 = require("./aiService");
|
|
8
8
|
const filePatterns_1 = require("../utils/filePatterns");
|
|
9
|
+
const commitTypeDetector_1 = require("./commitTypeDetector");
|
|
9
10
|
const astExportAnalyzer_1 = require("./astExportAnalyzer");
|
|
10
11
|
const astClassifier_1 = require("./astClassifier");
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
style: '๐',
|
|
17
|
-
refactor: 'โป๏ธ',
|
|
18
|
-
test: '๐งช',
|
|
19
|
-
chore: '๐ง',
|
|
20
|
-
perf: 'โก',
|
|
21
|
-
};
|
|
22
|
-
// Default keywords that indicate breaking changes
|
|
23
|
-
// Note: generic words like "removed" are excluded to avoid false positives
|
|
24
|
-
// from descriptive comments (e.g. "// Return array with zeros removed")
|
|
25
|
-
const DEFAULT_BREAKING_KEYWORDS = [
|
|
26
|
-
'breaking',
|
|
27
|
-
'breaking change',
|
|
28
|
-
'breaking-change',
|
|
29
|
-
'incompatible',
|
|
30
|
-
];
|
|
12
|
+
const breakingChangeDetector_1 = require("./breakingChangeDetector");
|
|
13
|
+
const descriptionGenerator_1 = require("./descriptionGenerator");
|
|
14
|
+
const semanticAnalyzer_1 = require("./semanticAnalyzer");
|
|
15
|
+
const scopeDetector_1 = require("./scopeDetector");
|
|
16
|
+
const messageBuilder_1 = require("./messageBuilder");
|
|
31
17
|
/**
|
|
32
18
|
* Compute the aggregate export diff across all staged source files
|
|
33
19
|
* using AST analysis on full file contents (before vs after).
|
|
@@ -48,186 +34,6 @@ function computeExportDiffForStagedFiles(stagedFiles) {
|
|
|
48
34
|
}
|
|
49
35
|
return { removed: allRemoved, added: allAdded };
|
|
50
36
|
}
|
|
51
|
-
// Role detection patterns for semantic analysis
|
|
52
|
-
const ROLE_PATTERNS = {
|
|
53
|
-
ui: [
|
|
54
|
-
// JSX elements and components
|
|
55
|
-
/^\+.*<[A-Z][a-zA-Z]*[\s/>]/m, // JSX component usage
|
|
56
|
-
/^\+.*<\/[A-Z][a-zA-Z]*>/m, // JSX component closing
|
|
57
|
-
/^\+.*<(div|span|p|h[1-6]|button|input|form|ul|li|table|img|a|section|header|footer|nav|main|article|aside)\b/mi,
|
|
58
|
-
/^\+.*className\s*=/m, // className attribute
|
|
59
|
-
/^\+.*style\s*=\s*\{/m, // inline styles
|
|
60
|
-
/^\+.*return\s*\(/m, // render return
|
|
61
|
-
/^\+.*render\s*\(\s*\)/m, // render method
|
|
62
|
-
/^\+.*(?:<>|<\/>)/m, // React fragments
|
|
63
|
-
/^\+.*aria-\w+=/m, // accessibility attributes
|
|
64
|
-
/^\+.*role\s*=/m, // role attribute
|
|
65
|
-
/^\+.*\b(?:onClick|onSubmit|onChange|onFocus|onBlur|onKeyDown|onKeyUp|onMouseEnter|onMouseLeave)\b/m,
|
|
66
|
-
],
|
|
67
|
-
logic: [
|
|
68
|
-
// Business logic patterns
|
|
69
|
-
/^\+.*\bif\s*\(/m, // conditionals
|
|
70
|
-
/^\+.*\bswitch\s*\(/m, // switch statements
|
|
71
|
-
/^\+.*\bfor\s*\(/m, // for loops
|
|
72
|
-
/^\+.*\bwhile\s*\(/m, // while loops
|
|
73
|
-
/^\+.*\.map\s*\(/m, // array operations
|
|
74
|
-
/^\+.*\.filter\s*\(/m,
|
|
75
|
-
/^\+.*\.reduce\s*\(/m,
|
|
76
|
-
/^\+.*\.find\s*\(/m,
|
|
77
|
-
/^\+.*\.some\s*\(/m,
|
|
78
|
-
/^\+.*\.every\s*\(/m,
|
|
79
|
-
/^\+.*\btry\s*\{/m, // error handling
|
|
80
|
-
/^\+.*\bcatch\s*\(/m,
|
|
81
|
-
/^\+.*\bthrow\s+/m,
|
|
82
|
-
/^\+.*\breturn\s+(?![\s(]?<)/m, // return (not JSX)
|
|
83
|
-
/^\+.*\bawait\s+/m, // async operations
|
|
84
|
-
/^\+.*\bnew\s+[A-Z]/m, // object instantiation
|
|
85
|
-
/^\+.*=>\s*\{/m, // arrow function body
|
|
86
|
-
],
|
|
87
|
-
data: [
|
|
88
|
-
// State and data management
|
|
89
|
-
/^\+.*\buseState\s*[<(]/m, // React state
|
|
90
|
-
/^\+.*\buseReducer\s*\(/m, // React reducer
|
|
91
|
-
/^\+.*\buseContext\s*\(/m, // React context
|
|
92
|
-
/^\+.*\buseSelector\s*\(/m, // Redux selector
|
|
93
|
-
/^\+.*\bdispatch\s*\(/m, // Redux dispatch
|
|
94
|
-
/^\+.*\bsetState\s*\(/m, // Class component state
|
|
95
|
-
/^\+.*\bthis\.state\b/m, // Class component state access
|
|
96
|
-
/^\+.*\bprops\./m, // Props access
|
|
97
|
-
/^\+.*interface\s+\w+Props/m, // Props interface
|
|
98
|
-
/^\+.*type\s+\w+Props/m, // Props type
|
|
99
|
-
/^\+.*\bconst\s+\[[a-z]+,\s*set[A-Z]/m, // State destructuring
|
|
100
|
-
/^\+.*:\s*\w+\[\]/m, // Array type
|
|
101
|
-
/^\+.*:\s*(string|number|boolean|object)/m, // Primitive types
|
|
102
|
-
/^\+.*\binterface\s+\w+/m, // Interface definition
|
|
103
|
-
/^\+.*\btype\s+\w+\s*=/m, // Type definition
|
|
104
|
-
],
|
|
105
|
-
style: [
|
|
106
|
-
// CSS and styling
|
|
107
|
-
/^\+.*\bstyles?\./m, // styles object access
|
|
108
|
-
/^\+.*\bstyled\./m, // styled-components
|
|
109
|
-
/^\+.*\bcss`/m, // CSS template literal
|
|
110
|
-
/^\+.*\bsx\s*=\s*\{/m, // MUI sx prop
|
|
111
|
-
/^\+.*:\s*['"]?[0-9]+(px|em|rem|%|vh|vw)/m, // CSS units
|
|
112
|
-
/^\+.*:\s*['"]?#[0-9a-fA-F]{3,8}/m, // Hex colors
|
|
113
|
-
/^\+.*:\s*['"]?rgb(a)?\s*\(/m, // RGB colors
|
|
114
|
-
/^\+.*(margin|padding|width|height|border|background|color|font|display|flex|grid)\s*:/m,
|
|
115
|
-
/^\+.*\btheme\./m, // Theme access
|
|
116
|
-
/^\+.*\.module\.css/m, // CSS modules import
|
|
117
|
-
],
|
|
118
|
-
api: [
|
|
119
|
-
// API and network calls
|
|
120
|
-
/^\+.*\bfetch\s*\(/m, // fetch API
|
|
121
|
-
/^\+.*\baxios\./m, // axios
|
|
122
|
-
/^\+.*\.get\s*\(/m, // HTTP GET
|
|
123
|
-
/^\+.*\.post\s*\(/m, // HTTP POST
|
|
124
|
-
/^\+.*\.put\s*\(/m, // HTTP PUT
|
|
125
|
-
/^\+.*\.delete\s*\(/m, // HTTP DELETE
|
|
126
|
-
/^\+.*\.patch\s*\(/m, // HTTP PATCH
|
|
127
|
-
/^\+.*\bapi\./m, // api object access
|
|
128
|
-
/^\+.*\/api\//m, // API path
|
|
129
|
-
/^\+.*\bendpoint/mi, // endpoint reference
|
|
130
|
-
/^\+.*\bheaders\s*:/m, // HTTP headers
|
|
131
|
-
/^\+.*\bAuthorization:/m, // Auth header
|
|
132
|
-
/^\+.*\buseQuery\s*\(/m, // React Query
|
|
133
|
-
/^\+.*\buseMutation\s*\(/m, // React Query mutation
|
|
134
|
-
/^\+.*\bswr\b/mi, // SWR
|
|
135
|
-
/^\+.*\bgraphql`/m, // GraphQL
|
|
136
|
-
/^\+.*\bquery\s*\{/m, // GraphQL query
|
|
137
|
-
/^\+.*\bmutation\s*\{/m, // GraphQL mutation
|
|
138
|
-
],
|
|
139
|
-
config: [
|
|
140
|
-
// Configuration
|
|
141
|
-
/^\+.*\bprocess\.env\./m, // Environment variables
|
|
142
|
-
/^\+.*\bimport\.meta\.env\./m, // Vite env
|
|
143
|
-
/^\+.*\bCONFIG\./m, // Config constant
|
|
144
|
-
/^\+.*\bsettings\./m, // Settings object
|
|
145
|
-
/^\+.*\boptions\s*:/m, // Options object
|
|
146
|
-
/^\+.*\bdefaultProps/m, // Default props
|
|
147
|
-
/^\+.*\bexport\s+(const|let)\s+[A-Z_]+\s*=/m, // Constant export
|
|
148
|
-
/^\+.*:\s*['"]?(development|production|test)['"]/m, // Environment strings
|
|
149
|
-
],
|
|
150
|
-
test: [
|
|
151
|
-
// Testing patterns
|
|
152
|
-
/^\+.*\bdescribe\s*\(/m, // Test suite
|
|
153
|
-
/^\+.*\bit\s*\(/m, // Test case
|
|
154
|
-
/^\+.*\btest\s*\(/m, // Test case
|
|
155
|
-
/^\+.*\bexpect\s*\(/m, // Assertion
|
|
156
|
-
/^\+.*\bjest\./m, // Jest
|
|
157
|
-
/^\+.*\bmock\(/m, // Mocking
|
|
158
|
-
/^\+.*\bspyOn\s*\(/m, // Spy
|
|
159
|
-
/^\+.*\bbeforeEach\s*\(/m, // Setup
|
|
160
|
-
/^\+.*\bafterEach\s*\(/m, // Teardown
|
|
161
|
-
/^\+.*\brender\s*\(/m, // React testing library
|
|
162
|
-
],
|
|
163
|
-
unknown: [],
|
|
164
|
-
};
|
|
165
|
-
// Intent detection patterns
|
|
166
|
-
const INTENT_PATTERNS = {
|
|
167
|
-
add: [
|
|
168
|
-
/^\+\s*export\s+(function|class|const|interface|type)/m,
|
|
169
|
-
/^\+\s*(async\s+)?function\s+\w+/m,
|
|
170
|
-
/^\+\s*const\s+\w+\s*=\s*(async\s+)?\(/m,
|
|
171
|
-
/^\+\s*class\s+\w+/m,
|
|
172
|
-
],
|
|
173
|
-
modify: [
|
|
174
|
-
// Changes that have both additions and deletions of similar patterns
|
|
175
|
-
],
|
|
176
|
-
fix: [
|
|
177
|
-
/^\+.*\btypeof\s+\w+\s*[!=]==?\s*['"`]/m,
|
|
178
|
-
/^\+.*\binstanceof\s+/m,
|
|
179
|
-
/^\+.*\bArray\.isArray\s*\(/m,
|
|
180
|
-
/^\+.*\bif\s*\(\s*!\w+\s*\)/m,
|
|
181
|
-
/^\+.*\?\?/m,
|
|
182
|
-
/^\+.*\?\./m,
|
|
183
|
-
/^\+.*\|\|\s*['"{\[0]/m, // Default values
|
|
184
|
-
/^\+.*\bcatch\s*\(/m,
|
|
185
|
-
/^\+.*\btry\s*\{/m,
|
|
186
|
-
],
|
|
187
|
-
remove: [
|
|
188
|
-
/^-\s*export\s+(function|class|const|interface|type)/m,
|
|
189
|
-
/^-\s*(async\s+)?function\s+\w+/m,
|
|
190
|
-
/^-\s*class\s+\w+/m,
|
|
191
|
-
],
|
|
192
|
-
refactor: [
|
|
193
|
-
/^\+.*=>/m, // Arrow function conversion
|
|
194
|
-
/^\+.*\.\.\./m, // Spread operator
|
|
195
|
-
/^\+.*`\$\{/m, // Template literal
|
|
196
|
-
/^\+.*Object\.(keys|values|entries)/m, // Object methods
|
|
197
|
-
],
|
|
198
|
-
enhance: [
|
|
199
|
-
/^\+.*\bmemo\s*\(/m, // React memo
|
|
200
|
-
/^\+.*\buseMemo\s*\(/m, // useMemo hook
|
|
201
|
-
/^\+.*\buseCallback\s*\(/m, // useCallback hook
|
|
202
|
-
/^\+.*\blazy\s*\(/m, // React lazy
|
|
203
|
-
/^\+.*\bSuspense\b/m, // React Suspense
|
|
204
|
-
],
|
|
205
|
-
rename: [
|
|
206
|
-
// Rename detection is handled by detectRenames() method
|
|
207
|
-
// which compares removed and added function/class/type names
|
|
208
|
-
],
|
|
209
|
-
};
|
|
210
|
-
// Role descriptions for commit messages
|
|
211
|
-
const ROLE_DESCRIPTIONS = {
|
|
212
|
-
ui: 'UI/rendering',
|
|
213
|
-
logic: 'business logic',
|
|
214
|
-
data: 'data/state management',
|
|
215
|
-
style: 'styling',
|
|
216
|
-
api: 'API integration',
|
|
217
|
-
config: 'configuration',
|
|
218
|
-
test: 'tests',
|
|
219
|
-
unknown: 'code',
|
|
220
|
-
};
|
|
221
|
-
// Intent verb mappings for commit messages
|
|
222
|
-
const INTENT_VERBS = {
|
|
223
|
-
add: { past: 'added', present: 'add' },
|
|
224
|
-
modify: { past: 'updated', present: 'update' },
|
|
225
|
-
fix: { past: 'fixed', present: 'fix' },
|
|
226
|
-
remove: { past: 'removed', present: 'remove' },
|
|
227
|
-
refactor: { past: 'refactored', present: 'refactor' },
|
|
228
|
-
enhance: { past: 'enhanced', present: 'improve' },
|
|
229
|
-
rename: { past: 'renamed', present: 'rename' },
|
|
230
|
-
};
|
|
231
37
|
class AnalyzerService {
|
|
232
38
|
/**
|
|
233
39
|
* Analyze staged changes and return structured analysis
|
|
@@ -284,22 +90,49 @@ class AnalyzerService {
|
|
|
284
90
|
catch {
|
|
285
91
|
// AST pipeline failure: fall through to regex analysis
|
|
286
92
|
}
|
|
287
|
-
// Determine commit type: AST
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
? Math.max(...astResult.signals.map(s => s.weight), 0)
|
|
291
|
-
: 0;
|
|
93
|
+
// Determine commit type: AST is primary for parseable files, regex is fallback
|
|
94
|
+
const allParseable = stagedFiles.every(f => (0, astExportAnalyzer_1.isParseableFile)(f.path));
|
|
95
|
+
const hasParseable = stagedFiles.some(f => (0, astExportAnalyzer_1.isParseableFile)(f.path));
|
|
292
96
|
let commitType;
|
|
293
|
-
|
|
294
|
-
|
|
97
|
+
let classificationSource;
|
|
98
|
+
let confidence;
|
|
99
|
+
if (astResult?.primaryType) {
|
|
100
|
+
// AST produced a classification
|
|
101
|
+
if (allParseable) {
|
|
102
|
+
// Pure JS/TS commit: AST is authoritative
|
|
103
|
+
commitType = astResult.primaryType;
|
|
104
|
+
classificationSource = 'ast';
|
|
105
|
+
confidence = astResult.confidence;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Mixed commit (JS/TS + non-parseable files like CSS, MD, JSON)
|
|
109
|
+
// File-type tiers (test-only, docs-only, config-only) are more specific
|
|
110
|
+
const fileTypeResult = (0, commitTypeDetector_1.determineCommitTypeFileOnly)(filesAffected);
|
|
111
|
+
if (fileTypeResult) {
|
|
112
|
+
commitType = fileTypeResult;
|
|
113
|
+
classificationSource = 'file-type';
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
commitType = astResult.primaryType;
|
|
117
|
+
classificationSource = 'ast';
|
|
118
|
+
confidence = astResult.confidence;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else if (hasParseable && !astResult) {
|
|
123
|
+
// Parseable files exist but AST produced nothing (parse failure or zero signals)
|
|
124
|
+
commitType = (0, commitTypeDetector_1.determineCommitType)(filesAffected, diff, stagedFiles, exportDiff);
|
|
125
|
+
classificationSource = 'regex';
|
|
295
126
|
}
|
|
296
127
|
else {
|
|
297
|
-
|
|
128
|
+
// No parseable files at all (CSS, MD, JSON, YAML, etc.)
|
|
129
|
+
commitType = (0, commitTypeDetector_1.determineCommitType)(filesAffected, diff, stagedFiles, exportDiff);
|
|
130
|
+
classificationSource = 'regex';
|
|
298
131
|
}
|
|
299
132
|
// Determine scope if applicable
|
|
300
|
-
const scope =
|
|
133
|
+
const scope = (0, scopeDetector_1.determineScope)(stagedFiles);
|
|
301
134
|
// Detect breaking changes (merge AST + regex results)
|
|
302
|
-
let { isBreaking, reasons } =
|
|
135
|
+
let { isBreaking, reasons } = (0, breakingChangeDetector_1.detectBreakingChanges)(diff, stagedFiles, exportDiff);
|
|
303
136
|
if (astResult?.isBreaking) {
|
|
304
137
|
isBreaking = true;
|
|
305
138
|
reasons = [...new Set([...reasons, ...astResult.breakingReasons])];
|
|
@@ -308,7 +141,7 @@ class AnalyzerService {
|
|
|
308
141
|
// Only perform for source files to avoid overhead on simple config/doc changes
|
|
309
142
|
let semanticAnalysis;
|
|
310
143
|
if (filesAffected.source > 0 && diff.length > 0) {
|
|
311
|
-
semanticAnalysis =
|
|
144
|
+
semanticAnalysis = (0, semanticAnalyzer_1.analyzeSemanticChanges)(diff, stagedFiles);
|
|
312
145
|
}
|
|
313
146
|
// Non-exported renames don't break consumers โ the function still exists with a new name
|
|
314
147
|
// But a renamed EXPORT is breaking because consumers' import statements break
|
|
@@ -324,8 +157,8 @@ class AnalyzerService {
|
|
|
324
157
|
// Apply semantic intent โ commit type mapping policy
|
|
325
158
|
// Only override heuristic 'feat' type when semantic analysis provides specific intent
|
|
326
159
|
// This preserves specialized types (perf, docs, test, style, chore) detected by heuristics
|
|
327
|
-
// Skip this override if AST pipeline determined the type
|
|
328
|
-
if (semanticAnalysis && commitType === 'feat' &&
|
|
160
|
+
// Skip this override if AST pipeline determined the type
|
|
161
|
+
if (semanticAnalysis && commitType === 'feat' && classificationSource !== 'ast') {
|
|
329
162
|
const intentToType = {
|
|
330
163
|
fix: 'fix',
|
|
331
164
|
refactor: 'refactor',
|
|
@@ -337,7 +170,7 @@ class AnalyzerService {
|
|
|
337
170
|
}
|
|
338
171
|
}
|
|
339
172
|
// Generate description (uses AST analysis, then semantic analysis as fallback)
|
|
340
|
-
const description =
|
|
173
|
+
const description = (0, descriptionGenerator_1.generateDescription)(filesAffected, {
|
|
341
174
|
added: fileChanges.added.length,
|
|
342
175
|
modified: fileChanges.modified.length,
|
|
343
176
|
deleted: fileChanges.deleted.length,
|
|
@@ -354,1585 +187,10 @@ class AnalyzerService {
|
|
|
354
187
|
breakingChangeReasons: reasons,
|
|
355
188
|
semanticAnalysis,
|
|
356
189
|
astResult,
|
|
190
|
+
classificationSource,
|
|
191
|
+
confidence,
|
|
357
192
|
};
|
|
358
193
|
}
|
|
359
|
-
/**
|
|
360
|
-
* Detect breaking changes from diff content and file changes
|
|
361
|
-
*/
|
|
362
|
-
static detectBreakingChanges(diff, stagedFiles, exportDiff) {
|
|
363
|
-
const config = configService_1.ConfigService.getConfig();
|
|
364
|
-
const breakingConfig = config.breakingChangeDetection;
|
|
365
|
-
// Check if breaking change detection is disabled
|
|
366
|
-
if (breakingConfig?.enabled === false) {
|
|
367
|
-
return { isBreaking: false, reasons: [] };
|
|
368
|
-
}
|
|
369
|
-
const reasons = [];
|
|
370
|
-
// Check for keyword-based breaking changes
|
|
371
|
-
// Only check against non-comment code changes to avoid false positives
|
|
372
|
-
// from descriptive comments (e.g. "// Return array with zeros removed")
|
|
373
|
-
const keywords = breakingConfig?.keywords || DEFAULT_BREAKING_KEYWORDS;
|
|
374
|
-
const codeChangesLower = this.extractNonCommentChanges(diff).toLowerCase();
|
|
375
|
-
for (const keyword of keywords) {
|
|
376
|
-
if (codeChangesLower.includes(keyword.toLowerCase())) {
|
|
377
|
-
reasons.push(`Contains "${keyword}" keyword`);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
// Check for deleted source files (potentially breaking)
|
|
381
|
-
const deletedSourceFiles = stagedFiles.filter(f => f.status === 'D' && (0, filePatterns_1.detectFileType)(f.path) === 'source');
|
|
382
|
-
if (deletedSourceFiles.length > 0) {
|
|
383
|
-
const fileNames = deletedSourceFiles.map(f => this.getFileName(f.path)).join(', ');
|
|
384
|
-
reasons.push(`Deleted source files: ${fileNames}`);
|
|
385
|
-
}
|
|
386
|
-
// Export-aware breaking change detection (AST-based)
|
|
387
|
-
const addedExportNames = new Set(exportDiff.added.map(e => e.name));
|
|
388
|
-
// Exported symbols that were removed entirely (not renamed/moved)
|
|
389
|
-
const trulyRemovedExports = exportDiff.removed.filter(e => !addedExportNames.has(e.name));
|
|
390
|
-
if (trulyRemovedExports.length > 0) {
|
|
391
|
-
const names = trulyRemovedExports.map(e => e.name).join(', ');
|
|
392
|
-
reasons.push(`Removed exported members: ${names}`);
|
|
393
|
-
}
|
|
394
|
-
// Check for breaking signature changes on exported functions
|
|
395
|
-
for (const removedExport of exportDiff.removed) {
|
|
396
|
-
const matchingAdded = exportDiff.added.find(e => e.name === removedExport.name);
|
|
397
|
-
if (matchingAdded && removedExport.params && matchingAdded.params) {
|
|
398
|
-
if ((0, astExportAnalyzer_1.isSignatureBreakingAST)(removedExport.params, matchingAdded.params)) {
|
|
399
|
-
reasons.push(`Breaking signature change on exported ${removedExport.kind}: ${removedExport.name}`);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
// Check for renamed files that might break imports
|
|
404
|
-
const renamedFiles = stagedFiles.filter(f => f.status === 'R');
|
|
405
|
-
if (renamedFiles.length > 0) {
|
|
406
|
-
const sourceRenames = renamedFiles.filter(f => (0, filePatterns_1.detectFileType)(f.path) === 'source');
|
|
407
|
-
if (sourceRenames.length > 0) {
|
|
408
|
-
reasons.push('Renamed source files (may break imports)');
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
// Deduplicate reasons
|
|
412
|
-
const uniqueReasons = [...new Set(reasons)];
|
|
413
|
-
return {
|
|
414
|
-
isBreaking: uniqueReasons.length > 0,
|
|
415
|
-
reasons: uniqueReasons,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* Determine the commit type based on analysis
|
|
420
|
-
*/
|
|
421
|
-
static determineCommitType(filesAffected, diff, stagedFiles, exportDiff) {
|
|
422
|
-
const diffLower = diff.toLowerCase();
|
|
423
|
-
const cleanedDiff = this.extractNonCommentChanges(diff);
|
|
424
|
-
const cleanedDiffLower = cleanedDiff.toLowerCase();
|
|
425
|
-
const filePaths = stagedFiles.map((f) => f.path.toLowerCase());
|
|
426
|
-
// === FILE TYPE BASED DETECTION (highest priority) ===
|
|
427
|
-
// If only test files changed
|
|
428
|
-
if (filesAffected.test > 0 &&
|
|
429
|
-
filesAffected.source === 0 &&
|
|
430
|
-
filesAffected.docs === 0) {
|
|
431
|
-
return 'test';
|
|
432
|
-
}
|
|
433
|
-
// If only docs changed
|
|
434
|
-
if (filesAffected.docs > 0 &&
|
|
435
|
-
filesAffected.source === 0 &&
|
|
436
|
-
filesAffected.test === 0) {
|
|
437
|
-
return 'docs';
|
|
438
|
-
}
|
|
439
|
-
// If only config files changed
|
|
440
|
-
if (filesAffected.config > 0 &&
|
|
441
|
-
filesAffected.source === 0 &&
|
|
442
|
-
filesAffected.test === 0 &&
|
|
443
|
-
filesAffected.docs === 0) {
|
|
444
|
-
return 'chore';
|
|
445
|
-
}
|
|
446
|
-
// === DOCS DETECTION (comment-only changes) ===
|
|
447
|
-
// Check if the changes are only adding/modifying comments (documentation)
|
|
448
|
-
if (this.isCommentOnlyChange(diff)) {
|
|
449
|
-
return 'docs';
|
|
450
|
-
}
|
|
451
|
-
// === FORMATTING-ONLY DETECTION (code style, not CSS) ===
|
|
452
|
-
// Per Conventional Commits: "style" = changes that do not affect the meaning of the code
|
|
453
|
-
// (white-space, formatting, missing semi-colons, etc.)
|
|
454
|
-
if (this.isFormattingOnlyChange(diff)) {
|
|
455
|
-
return 'style';
|
|
456
|
-
}
|
|
457
|
-
// === STYLE DETECTION (CSS/UI) ===
|
|
458
|
-
// CSS/UI styling changes also classify as "style"
|
|
459
|
-
// Check for CSS/styling file extensions
|
|
460
|
-
const isStyleFile = filePaths.some(p => p.endsWith('.css') ||
|
|
461
|
-
p.endsWith('.scss') ||
|
|
462
|
-
p.endsWith('.sass') ||
|
|
463
|
-
p.endsWith('.less') ||
|
|
464
|
-
p.endsWith('.styl') ||
|
|
465
|
-
p.endsWith('.stylus') ||
|
|
466
|
-
p.includes('.styles.') ||
|
|
467
|
-
p.includes('.style.') ||
|
|
468
|
-
p.includes('styles/') ||
|
|
469
|
-
p.includes('/theme') ||
|
|
470
|
-
p.includes('theme.') ||
|
|
471
|
-
p.includes('.theme.'));
|
|
472
|
-
// Check for styled-components, Tailwind, or inline style changes in diff content
|
|
473
|
-
const hasStyledComponentChanges = /^\+.*\bstyled\s*[.(]/m.test(diff) ||
|
|
474
|
-
/^\+.*\bcss\s*`/m.test(diff) ||
|
|
475
|
-
/^\+.*\@emotion\/styled/m.test(diff);
|
|
476
|
-
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);
|
|
477
|
-
const hasInlineStyleChanges = /^\+.*\bstyle\s*=\s*\{\{/m.test(diff);
|
|
478
|
-
const hasThemeChanges = /^\+.*(theme\s*[:=]|colors\s*[:=]|palette\s*[:=]|spacing\s*[:=]|typography\s*[:=])/m.test(diff);
|
|
479
|
-
const hasSxPropChanges = /^\+.*\bsx\s*=\s*\{/m.test(diff);
|
|
480
|
-
// Only classify as "style" if it's a CSS file OR contains actual CSS/styling code
|
|
481
|
-
const hasActualStyleChanges = isStyleFile || hasStyledComponentChanges ||
|
|
482
|
-
hasTailwindChanges || hasInlineStyleChanges ||
|
|
483
|
-
hasThemeChanges || hasSxPropChanges;
|
|
484
|
-
// NEVER classify as style if it's just JS without CSS
|
|
485
|
-
// This prevents console.log, debug statements, comments, test prints from being labeled as style
|
|
486
|
-
if (hasActualStyleChanges && !this.hasOnlyNonStyleJsChanges(diff, filePaths)) {
|
|
487
|
-
return 'style';
|
|
488
|
-
}
|
|
489
|
-
// === PERFORMANCE DETECTION ===
|
|
490
|
-
const perfPatterns = [
|
|
491
|
-
/\bperformance\b/i,
|
|
492
|
-
/\boptimiz(e|ation|ing)\b/i,
|
|
493
|
-
/\bfaster\b/i,
|
|
494
|
-
/\bspeed\s*(up|improvement)\b/i,
|
|
495
|
-
/\bcach(e|ing)\b/i,
|
|
496
|
-
/\bmemoiz(e|ation)\b/i,
|
|
497
|
-
/\blazy\s*load/i,
|
|
498
|
-
/\basync\b.*\bawait\b/i,
|
|
499
|
-
/\bparallel\b/i,
|
|
500
|
-
/\bbatch(ing)?\b/i,
|
|
501
|
-
];
|
|
502
|
-
if (perfPatterns.some(p => p.test(cleanedDiffLower))) {
|
|
503
|
-
return 'perf';
|
|
504
|
-
}
|
|
505
|
-
// === DETECT NEW FUNCTIONALITY FIRST ===
|
|
506
|
-
// This must come BEFORE fix detection to avoid false positives when new functions contain validation
|
|
507
|
-
const hasNewFiles = stagedFiles.some((f) => f.status === 'A');
|
|
508
|
-
// Comprehensive new code detection
|
|
509
|
-
const hasNewExports = /^\+\s*export\s+(function|class|const|let|var|interface|type|default)/m.test(diff);
|
|
510
|
-
const hasNewFunctions = /^\+\s*(async\s+)?function\s+\w+/m.test(diff);
|
|
511
|
-
const hasNewArrowFunctions = /^\+\s*(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>/m.test(diff);
|
|
512
|
-
const hasNewClasses = /^\+\s*(export\s+)?(class|abstract\s+class)\s+\w+/m.test(diff);
|
|
513
|
-
// Detect new class/object methods - exclude control flow keywords (if, for, while, etc.)
|
|
514
|
-
// which share the `keyword(...) {` shape but are NOT method declarations
|
|
515
|
-
const hasNewMethods = /^\+\s+(async\s+)?(?!if|else|for|while|do|switch|catch|return|throw|new|typeof|instanceof|delete|void|yield|await\b)\w+\s*\([^)]*\)\s*{/m.test(diff);
|
|
516
|
-
const hasNewInterfaces = /^\+\s*(export\s+)?(interface|type)\s+\w+/m.test(diff);
|
|
517
|
-
const hasNewComponents = /^\+\s*(export\s+)?(const|function)\s+[A-Z]\w+/m.test(diff); // React components start with capital
|
|
518
|
-
const hasNewImports = /^\+\s*import\s+/m.test(diff);
|
|
519
|
-
// Count significant additions vs removals
|
|
520
|
-
const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
|
|
521
|
-
const removedLines = (diff.match(/^-[^-]/gm) || []).length;
|
|
522
|
-
const netNewLines = addedLines - removedLines;
|
|
523
|
-
// Detect if we're adding new functionality
|
|
524
|
-
const hasNewFunctionality = hasNewExports || hasNewFunctions || hasNewArrowFunctions ||
|
|
525
|
-
hasNewClasses || hasNewMethods || hasNewInterfaces || hasNewComponents;
|
|
526
|
-
// === FEAT DETECTION (export-aware, AST-based) - CHECK BEFORE FIX ===
|
|
527
|
-
// Only treat as feat when the PUBLIC export surface grows
|
|
528
|
-
const removedExportNames = new Set(exportDiff.removed.map(e => e.name));
|
|
529
|
-
const hasNewExportedSymbols = exportDiff.added.some(e => !removedExportNames.has(e.name));
|
|
530
|
-
const hasAnyExportChanges = exportDiff.added.length > 0 || exportDiff.removed.length > 0;
|
|
531
|
-
if (hasNewFiles) {
|
|
532
|
-
return 'feat';
|
|
533
|
-
}
|
|
534
|
-
// New EXPORTED symbols = feat (public API surface grows)
|
|
535
|
-
if (hasNewExportedSymbols) {
|
|
536
|
-
return 'feat';
|
|
537
|
-
}
|
|
538
|
-
// New functions/classes that are NOT exported: internal refactoring
|
|
539
|
-
if (hasNewFunctionality && !hasNewExportedSymbols && !hasAnyExportChanges) {
|
|
540
|
-
// Balanced add/remove ratio indicates extraction/reorganization โ refactor
|
|
541
|
-
if (addedLines > 0 && removedLines > 0) {
|
|
542
|
-
const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
|
|
543
|
-
if (ratio > 0.3) {
|
|
544
|
-
return 'refactor';
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
// Heavily additive internal code falls through to feat patterns / fallback below
|
|
548
|
-
}
|
|
549
|
-
// Significant net additions with new imports often means new feature
|
|
550
|
-
if (hasNewImports && netNewLines > 5) {
|
|
551
|
-
return 'feat';
|
|
552
|
-
}
|
|
553
|
-
// Check for new feature indicators in comments/strings
|
|
554
|
-
const featPatterns = [
|
|
555
|
-
/\badd(s|ed|ing)?\s+(new\s+)?(feature|support|ability|option|functionality)/i,
|
|
556
|
-
/\bimplement(s|ed|ing)?\s+\w+/i,
|
|
557
|
-
/\bintroduc(e|es|ed|ing)\s+(new\s+)?\w+/i,
|
|
558
|
-
/\benable(s|d|ing)?\s+(new\s+)?\w+/i,
|
|
559
|
-
/\bnew\s+(feature|function|method|api|endpoint)/i,
|
|
560
|
-
];
|
|
561
|
-
if (featPatterns.some(p => p.test(cleanedDiff))) {
|
|
562
|
-
return 'feat';
|
|
563
|
-
}
|
|
564
|
-
// === FIX DETECTION ===
|
|
565
|
-
// Only check for validation patterns AFTER confirming no new functions/classes were added
|
|
566
|
-
// Check if this is adding validation/guards to existing code (defensive coding = fix)
|
|
567
|
-
const hasOnlyModifications = stagedFiles.every((f) => f.status === 'M');
|
|
568
|
-
const validationPatterns = [
|
|
569
|
-
/^\+.*\btypeof\s+\w+\s*===?\s*['"`]\w+['"`]/m, // typeof checks
|
|
570
|
-
/^\+.*\binstanceof\s+\w+/m, // instanceof checks
|
|
571
|
-
/^\+.*\bArray\.isArray\s*\(/m, // Array.isArray checks
|
|
572
|
-
/^\+.*\b(Number|String|Boolean)\.is\w+\s*\(/m, // Number.isNaN, etc.
|
|
573
|
-
/^\+.*\bif\s*\(\s*typeof\b/m, // if (typeof ...
|
|
574
|
-
/^\+.*\bif\s*\(\s*!\w+\s*\)/m, // if (!var) guards
|
|
575
|
-
/^\+.*\bif\s*\(\s*\w+\s*(===?|!==?)\s*(null|undefined)\s*\)/m, // null/undefined checks
|
|
576
|
-
/^\+.*\bif\s*\(\s*(null|undefined)\s*(===?|!==?)\s*\w+\s*\)/m, // null/undefined checks (reversed)
|
|
577
|
-
/^\+.*\bif\s*\(\s*[\w.\[\]]+\s*[<>]=?\s*-?\d/m, // numeric boundary checks: if (n < 0), if (arr[i] <= 0)
|
|
578
|
-
/^\+.*\bif\s*\(\s*-?\d+\s*[<>]=?\s*[\w.\[\]]+/m, // numeric boundary checks (reversed): if (0 > n)
|
|
579
|
-
/^\+.*\?\?/m, // Nullish coalescing
|
|
580
|
-
/^\+.*\?\./m, // Optional chaining
|
|
581
|
-
/^\+.*\|\|/m, // Default value patterns (when combined with guards)
|
|
582
|
-
];
|
|
583
|
-
// Check if this is a simplification refactor (using modern syntax to replace verbose code)
|
|
584
|
-
// When deletions > additions significantly, it's likely simplifying existing code, not adding new checks
|
|
585
|
-
const isSimplificationRefactor = removedLines > addedLines &&
|
|
586
|
-
(removedLines - addedLines) >= 3 && // At least 3 net lines removed (significant simplification)
|
|
587
|
-
(/^\+.*\?\./m.test(diff) || // Using optional chaining
|
|
588
|
-
/^\+.*\?\?/m.test(diff) // Using nullish coalescing
|
|
589
|
-
) &&
|
|
590
|
-
(/^-.*\bif\s*\(/m.test(diff) // Removing if statements
|
|
591
|
-
);
|
|
592
|
-
// If it's a simplification (replacing verbose ifs with optional chaining), it's refactor
|
|
593
|
-
if (isSimplificationRefactor) {
|
|
594
|
-
return 'refactor';
|
|
595
|
-
}
|
|
596
|
-
// If only modifying files (no new functions detected) and adding validation patterns, it's likely a fix
|
|
597
|
-
if (hasOnlyModifications && validationPatterns.some(p => p.test(diff))) {
|
|
598
|
-
return 'fix';
|
|
599
|
-
}
|
|
600
|
-
const fixPatterns = [
|
|
601
|
-
/\bfix(es|ed|ing)?\s*(the\s*)?(bug|issue|error|problem|crash)/i,
|
|
602
|
-
/\bfix(es|ed|ing)?\b/i, // Simple "fix" or "fixed" alone
|
|
603
|
-
/\bbug\s*fix/i,
|
|
604
|
-
/\bBUG:/i, // Bug comment markers
|
|
605
|
-
/\bhotfix\b/i,
|
|
606
|
-
/\bpatch(es|ed|ing)?\b/i,
|
|
607
|
-
/\bresolv(e|es|ed|ing)\s*(the\s*)?(issue|bug|error)/i,
|
|
608
|
-
/\bcorrect(s|ed|ing)?\s*(the\s*)?(bug|issue|error|problem)/i,
|
|
609
|
-
/\brepair(s|ed|ing)?\b/i,
|
|
610
|
-
/\bhandle\s*(error|exception|null|undefined)/i,
|
|
611
|
-
/\bnull\s*check/i,
|
|
612
|
-
/\bundefined\s*check/i,
|
|
613
|
-
/\btry\s*{\s*.*\s*}\s*catch/i,
|
|
614
|
-
/\bif\s*\(\s*!\s*\w+\s*\)/, // Null/undefined guards
|
|
615
|
-
/\bwas\s*broken\b/i, // "was broken" indicates fixing
|
|
616
|
-
/\bbroken\b.*\bfix/i, // broken...fix pattern
|
|
617
|
-
];
|
|
618
|
-
if (fixPatterns.some(p => p.test(cleanedDiff))) {
|
|
619
|
-
return 'fix';
|
|
620
|
-
}
|
|
621
|
-
// === REFACTOR DETECTION ===
|
|
622
|
-
const refactorPatterns = [
|
|
623
|
-
/\brefactor(s|ed|ing)?\b/i,
|
|
624
|
-
/\brestructur(e|es|ed|ing)\b/i,
|
|
625
|
-
/\bclean\s*up\b/i,
|
|
626
|
-
/\bsimplif(y|ies|ied|ying)\b/i,
|
|
627
|
-
/\brenam(e|es|ed|ing)\b/i,
|
|
628
|
-
/\bmov(e|es|ed|ing)\s*(to|from|into)\b/i,
|
|
629
|
-
/\bextract(s|ed|ing)?\s*(function|method|class|component)/i,
|
|
630
|
-
/\binline(s|d|ing)?\b/i,
|
|
631
|
-
/\bdedup(licate)?\b/i,
|
|
632
|
-
/\bDRY\b/,
|
|
633
|
-
/\breorganiz(e|es|ed|ing)\b/i,
|
|
634
|
-
];
|
|
635
|
-
if (refactorPatterns.some(p => p.test(cleanedDiff))) {
|
|
636
|
-
return 'refactor';
|
|
637
|
-
}
|
|
638
|
-
// If modifications with balanced adds/removes and no new functionality, likely refactor
|
|
639
|
-
// UNLESS the change modifies conditional logic (if/while/for/switch conditions),
|
|
640
|
-
// which typically alters behavior and should be treated as a fix, not a refactor
|
|
641
|
-
if (hasOnlyModifications && !hasNewFunctionality) {
|
|
642
|
-
// Check if the diff modifies conditions inside control flow statements.
|
|
643
|
-
// When both the removed and added lines contain the SAME control flow keyword
|
|
644
|
-
// but with different conditions, it's a behavioral change (fix), not a refactor.
|
|
645
|
-
const removedCondition = /^-\s*(if|while|for|switch)\s*\(/m.test(diff);
|
|
646
|
-
const addedCondition = /^\+\s*(if|while|for|switch)\s*\(/m.test(diff);
|
|
647
|
-
const isConditionChange = removedCondition && addedCondition;
|
|
648
|
-
if (isConditionChange) {
|
|
649
|
-
return 'fix';
|
|
650
|
-
}
|
|
651
|
-
// If roughly equal adds and removes, it's likely refactoring
|
|
652
|
-
if (addedLines > 0 && removedLines > 0) {
|
|
653
|
-
const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
|
|
654
|
-
if (ratio > 0.5) { // More strict ratio - must be very balanced
|
|
655
|
-
return 'refactor';
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
// === CHORE DETECTION ===
|
|
660
|
-
const chorePatterns = [
|
|
661
|
-
/\bdependenc(y|ies)\b/i,
|
|
662
|
-
/\bupgrade\b/i,
|
|
663
|
-
/\bupdate\s*(version|dep)/i,
|
|
664
|
-
/\bbump\b/i,
|
|
665
|
-
/\bpackage\.json\b/i,
|
|
666
|
-
/\bpackage-lock\.json\b/i,
|
|
667
|
-
/\byarn\.lock\b/i,
|
|
668
|
-
/\b\.gitignore\b/i,
|
|
669
|
-
/\bci\b.*\b(config|setup)\b/i,
|
|
670
|
-
/\blint(er|ing)?\b/i,
|
|
671
|
-
];
|
|
672
|
-
if (chorePatterns.some(p => p.test(cleanedDiff)) || chorePatterns.some(p => filePaths.some(f => p.test(f)))) {
|
|
673
|
-
return 'chore';
|
|
674
|
-
}
|
|
675
|
-
// === FALLBACK ===
|
|
676
|
-
// If we have more additions than removals, lean towards feat
|
|
677
|
-
if (filesAffected.source > 0) {
|
|
678
|
-
if (netNewLines > 10) {
|
|
679
|
-
return 'feat'; // Significant new code added
|
|
680
|
-
}
|
|
681
|
-
if (hasOnlyModifications) {
|
|
682
|
-
return 'refactor'; // Modifications without clear new functionality
|
|
683
|
-
}
|
|
684
|
-
return 'feat'; // Default for source changes with new files
|
|
685
|
-
}
|
|
686
|
-
return 'chore';
|
|
687
|
-
}
|
|
688
|
-
/**
|
|
689
|
-
* Check if diff contains actual logic changes (not just formatting)
|
|
690
|
-
*/
|
|
691
|
-
static hasLogicChanges(diff) {
|
|
692
|
-
// Remove formatting-only changes and check if there's real code
|
|
693
|
-
const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
|
|
694
|
-
!line.startsWith('+++') &&
|
|
695
|
-
!line.startsWith('---'));
|
|
696
|
-
for (const line of lines) {
|
|
697
|
-
const content = line.substring(1).trim();
|
|
698
|
-
// Skip empty lines, comments, and whitespace-only
|
|
699
|
-
if (content.length === 0 ||
|
|
700
|
-
content.startsWith('//') ||
|
|
701
|
-
content.startsWith('/*') ||
|
|
702
|
-
content.startsWith('*') ||
|
|
703
|
-
content === '{' ||
|
|
704
|
-
content === '}' ||
|
|
705
|
-
content === ';') {
|
|
706
|
-
continue;
|
|
707
|
-
}
|
|
708
|
-
// Has actual code change
|
|
709
|
-
return true;
|
|
710
|
-
}
|
|
711
|
-
return false;
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* Check if the diff contains ONLY formatting changes (no logic impact).
|
|
715
|
-
* Formatting changes include: semicolons, blank lines, indentation, trailing commas, braces.
|
|
716
|
-
* Per Conventional Commits, these should be classified as "style".
|
|
717
|
-
*/
|
|
718
|
-
static isFormattingOnlyChange(diff) {
|
|
719
|
-
const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
|
|
720
|
-
!line.startsWith('+++') &&
|
|
721
|
-
!line.startsWith('---'));
|
|
722
|
-
if (lines.length === 0) {
|
|
723
|
-
return false;
|
|
724
|
-
}
|
|
725
|
-
// Pair up removed and added lines to detect indentation-only changes
|
|
726
|
-
const removed = [];
|
|
727
|
-
const added = [];
|
|
728
|
-
for (const line of lines) {
|
|
729
|
-
const content = line.substring(1);
|
|
730
|
-
const trimmed = content.trim();
|
|
731
|
-
// Empty / whitespace-only lines are always formatting
|
|
732
|
-
if (trimmed.length === 0) {
|
|
733
|
-
continue;
|
|
734
|
-
}
|
|
735
|
-
// Standalone semicolons, braces, trailing commas are formatting
|
|
736
|
-
if (trimmed === ';' || trimmed === '{' || trimmed === '}' || trimmed === ',') {
|
|
737
|
-
continue;
|
|
738
|
-
}
|
|
739
|
-
// Collect lines for indentation-only and semicolon-only comparison
|
|
740
|
-
if (line.startsWith('-')) {
|
|
741
|
-
removed.push(trimmed);
|
|
742
|
-
}
|
|
743
|
-
else {
|
|
744
|
-
added.push(trimmed);
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
// If no added/removed content lines remain, it's purely blank line changes
|
|
748
|
-
if (removed.length === 0 && added.length === 0) {
|
|
749
|
-
return true;
|
|
750
|
-
}
|
|
751
|
-
// Check if each added line corresponds to a removed line that differs only by
|
|
752
|
-
// formatting (trailing semicolons, trailing commas, whitespace)
|
|
753
|
-
if (removed.length !== added.length) {
|
|
754
|
-
return false;
|
|
755
|
-
}
|
|
756
|
-
for (let i = 0; i < removed.length; i++) {
|
|
757
|
-
// Normalize: strip trailing semicolons, commas, and whitespace
|
|
758
|
-
const normalizeFormatting = (s) => s.replace(/[;\s,]+$/g, '').replace(/^\s+/, '');
|
|
759
|
-
const normRemoved = normalizeFormatting(removed[i]);
|
|
760
|
-
const normAdded = normalizeFormatting(added[i]);
|
|
761
|
-
if (normRemoved !== normAdded) {
|
|
762
|
-
return false;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
return true;
|
|
766
|
-
}
|
|
767
|
-
/**
|
|
768
|
-
* Generate a description for formatting-only changes
|
|
769
|
-
*/
|
|
770
|
-
static generateFormattingDescription(diff, stagedFiles) {
|
|
771
|
-
const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
|
|
772
|
-
!line.startsWith('+++') &&
|
|
773
|
-
!line.startsWith('---'));
|
|
774
|
-
let hasSemicolonChanges = false;
|
|
775
|
-
let hasBlankLineChanges = false;
|
|
776
|
-
let hasIndentationChanges = false;
|
|
777
|
-
let hasBraceChanges = false;
|
|
778
|
-
let hasTrailingCommaChanges = false;
|
|
779
|
-
const removed = [];
|
|
780
|
-
const added = [];
|
|
781
|
-
for (const line of lines) {
|
|
782
|
-
const trimmed = line.substring(1).trim();
|
|
783
|
-
if (trimmed.length === 0) {
|
|
784
|
-
hasBlankLineChanges = true;
|
|
785
|
-
continue;
|
|
786
|
-
}
|
|
787
|
-
if (trimmed === ';') {
|
|
788
|
-
hasSemicolonChanges = true;
|
|
789
|
-
continue;
|
|
790
|
-
}
|
|
791
|
-
if (trimmed === '{' || trimmed === '}') {
|
|
792
|
-
hasBraceChanges = true;
|
|
793
|
-
continue;
|
|
794
|
-
}
|
|
795
|
-
if (trimmed === ',') {
|
|
796
|
-
hasTrailingCommaChanges = true;
|
|
797
|
-
continue;
|
|
798
|
-
}
|
|
799
|
-
if (line.startsWith('-')) {
|
|
800
|
-
removed.push(trimmed);
|
|
801
|
-
}
|
|
802
|
-
else {
|
|
803
|
-
added.push(trimmed);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
// Check paired lines for semicolon-only or indentation-only diffs
|
|
807
|
-
for (let i = 0; i < removed.length && i < added.length; i++) {
|
|
808
|
-
const r = removed[i];
|
|
809
|
-
const a = added[i];
|
|
810
|
-
// If trimmed content is identical, the only difference was indentation
|
|
811
|
-
if (r === a) {
|
|
812
|
-
hasIndentationChanges = true;
|
|
813
|
-
}
|
|
814
|
-
// Semicolon difference (one has semicolon, the other doesn't)
|
|
815
|
-
else if (r + ';' === a || a + ';' === r) {
|
|
816
|
-
hasSemicolonChanges = true;
|
|
817
|
-
}
|
|
818
|
-
// Trailing comma difference
|
|
819
|
-
else if (r + ',' === a || a + ',' === r) {
|
|
820
|
-
hasTrailingCommaChanges = true;
|
|
821
|
-
}
|
|
822
|
-
else {
|
|
823
|
-
hasIndentationChanges = true;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
// Build description based on what changed
|
|
827
|
-
let description;
|
|
828
|
-
if (hasSemicolonChanges && !hasBlankLineChanges && !hasIndentationChanges && !hasBraceChanges && !hasTrailingCommaChanges) {
|
|
829
|
-
description = 'add missing semicolons';
|
|
830
|
-
}
|
|
831
|
-
else if (hasBlankLineChanges && !hasSemicolonChanges && !hasIndentationChanges && !hasBraceChanges && !hasTrailingCommaChanges) {
|
|
832
|
-
description = 'remove blank lines';
|
|
833
|
-
}
|
|
834
|
-
else if (hasIndentationChanges && !hasSemicolonChanges && !hasBlankLineChanges && !hasBraceChanges && !hasTrailingCommaChanges) {
|
|
835
|
-
description = 'fix indentation';
|
|
836
|
-
}
|
|
837
|
-
else if (hasBraceChanges && !hasSemicolonChanges && !hasBlankLineChanges && !hasIndentationChanges && !hasTrailingCommaChanges) {
|
|
838
|
-
description = 'fix brace formatting';
|
|
839
|
-
}
|
|
840
|
-
else if (hasTrailingCommaChanges && !hasSemicolonChanges && !hasBlankLineChanges && !hasIndentationChanges && !hasBraceChanges) {
|
|
841
|
-
description = 'fix trailing commas';
|
|
842
|
-
}
|
|
843
|
-
else {
|
|
844
|
-
description = 'fix code formatting';
|
|
845
|
-
}
|
|
846
|
-
// Append filename for single-file changes
|
|
847
|
-
if (stagedFiles.length === 1) {
|
|
848
|
-
const fileName = stagedFiles[0].path.split('/').pop() || stagedFiles[0].path;
|
|
849
|
-
description += ` in ${fileName}`;
|
|
850
|
-
}
|
|
851
|
-
return description;
|
|
852
|
-
}
|
|
853
|
-
/**
|
|
854
|
-
* Check if the changes are ONLY non-style JavaScript changes
|
|
855
|
-
* (console.log, debug statements, test prints, comments, etc.)
|
|
856
|
-
* Returns true if JS files have changes but NO actual styling changes
|
|
857
|
-
*/
|
|
858
|
-
static hasOnlyNonStyleJsChanges(diff, filePaths) {
|
|
859
|
-
// Check if all files are JavaScript/TypeScript (not CSS)
|
|
860
|
-
const hasOnlyJsFiles = filePaths.every(p => p.endsWith('.js') ||
|
|
861
|
-
p.endsWith('.jsx') ||
|
|
862
|
-
p.endsWith('.ts') ||
|
|
863
|
-
p.endsWith('.tsx') ||
|
|
864
|
-
p.endsWith('.mjs') ||
|
|
865
|
-
p.endsWith('.cjs'));
|
|
866
|
-
if (!hasOnlyJsFiles) {
|
|
867
|
-
return false;
|
|
868
|
-
}
|
|
869
|
-
// Patterns that indicate NON-style changes (debug, logging, test output)
|
|
870
|
-
const nonStylePatterns = [
|
|
871
|
-
/console\.(log|debug|info|warn|error|trace|dir|table)/,
|
|
872
|
-
/debugger\b/,
|
|
873
|
-
/logger\.\w+/i,
|
|
874
|
-
/debug\s*\(/,
|
|
875
|
-
/print\s*\(/,
|
|
876
|
-
/console\.assert/,
|
|
877
|
-
/console\.time/,
|
|
878
|
-
/console\.count/,
|
|
879
|
-
/console\.group/,
|
|
880
|
-
/process\.stdout/,
|
|
881
|
-
/process\.stderr/,
|
|
882
|
-
/\.toLog\(/,
|
|
883
|
-
/\.log\(/,
|
|
884
|
-
/winston\./,
|
|
885
|
-
/pino\./,
|
|
886
|
-
/bunyan\./,
|
|
887
|
-
/log4js\./,
|
|
888
|
-
];
|
|
889
|
-
// Get all added/changed lines
|
|
890
|
-
const changedLines = diff.split('\n').filter(line => line.startsWith('+') && !line.startsWith('+++'));
|
|
891
|
-
// If the changes match non-style patterns, return true
|
|
892
|
-
const hasNonStyleChanges = changedLines.some(line => nonStylePatterns.some(pattern => pattern.test(line)));
|
|
893
|
-
// Check if there are NO style-related patterns in the JS files
|
|
894
|
-
const stylePatterns = [
|
|
895
|
-
/styled\s*[.(]/,
|
|
896
|
-
/css\s*`/,
|
|
897
|
-
/\bstyle\s*=\s*\{\{/,
|
|
898
|
-
/className\s*=/,
|
|
899
|
-
/\bsx\s*=/,
|
|
900
|
-
/theme\./,
|
|
901
|
-
/colors\./,
|
|
902
|
-
/palette\./,
|
|
903
|
-
];
|
|
904
|
-
const hasStylePatterns = changedLines.some(line => stylePatterns.some(pattern => pattern.test(line)));
|
|
905
|
-
// Return true only if we have non-style changes AND no style patterns
|
|
906
|
-
return hasNonStyleChanges && !hasStylePatterns;
|
|
907
|
-
}
|
|
908
|
-
/**
|
|
909
|
-
* Extract non-comment changed lines from a diff for keyword analysis.
|
|
910
|
-
* Filters out comment lines to prevent false positives from descriptive text.
|
|
911
|
-
*/
|
|
912
|
-
static extractNonCommentChanges(diff) {
|
|
913
|
-
return diff.split('\n')
|
|
914
|
-
.filter(line => (line.startsWith('+') || line.startsWith('-')) &&
|
|
915
|
-
!line.startsWith('+++') &&
|
|
916
|
-
!line.startsWith('---'))
|
|
917
|
-
.map(line => line.substring(1))
|
|
918
|
-
.filter(line => {
|
|
919
|
-
const trimmed = line.trim();
|
|
920
|
-
if (trimmed.length === 0)
|
|
921
|
-
return false;
|
|
922
|
-
// Filter out comment lines
|
|
923
|
-
if (trimmed.startsWith('//') ||
|
|
924
|
-
trimmed.startsWith('/*') ||
|
|
925
|
-
trimmed.startsWith('*') ||
|
|
926
|
-
trimmed.startsWith('*/') ||
|
|
927
|
-
trimmed.startsWith('#') ||
|
|
928
|
-
trimmed.startsWith('<!--') ||
|
|
929
|
-
trimmed.startsWith('-->') ||
|
|
930
|
-
trimmed.startsWith('"""') ||
|
|
931
|
-
trimmed.startsWith("'''") ||
|
|
932
|
-
/^\/\*\*/.test(trimmed) ||
|
|
933
|
-
/^\*\s*@\w+/.test(trimmed)) {
|
|
934
|
-
return false;
|
|
935
|
-
}
|
|
936
|
-
// Filter out lines that are purely string literals
|
|
937
|
-
if (/^(?:return\s+)?(['"`]).*\1[;,]?\s*$/.test(trimmed)) {
|
|
938
|
-
return false;
|
|
939
|
-
}
|
|
940
|
-
return true;
|
|
941
|
-
})
|
|
942
|
-
.join('\n');
|
|
943
|
-
}
|
|
944
|
-
/**
|
|
945
|
-
* Check if the diff contains only comment changes (documentation)
|
|
946
|
-
* Returns true if ALL changes are comments (no code changes)
|
|
947
|
-
*/
|
|
948
|
-
static isCommentOnlyChange(diff) {
|
|
949
|
-
// Get all changed lines (additions and deletions)
|
|
950
|
-
const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
|
|
951
|
-
!line.startsWith('+++') &&
|
|
952
|
-
!line.startsWith('---'));
|
|
953
|
-
if (lines.length === 0) {
|
|
954
|
-
return false;
|
|
955
|
-
}
|
|
956
|
-
let hasCommentChanges = false;
|
|
957
|
-
for (const line of lines) {
|
|
958
|
-
const content = line.substring(1).trim();
|
|
959
|
-
// Skip empty lines
|
|
960
|
-
if (content.length === 0) {
|
|
961
|
-
continue;
|
|
962
|
-
}
|
|
963
|
-
// Check if line is a comment
|
|
964
|
-
const isComment = content.startsWith('//') || // Single-line comment
|
|
965
|
-
content.startsWith('/*') || // Multi-line comment start
|
|
966
|
-
content.startsWith('*') || // Multi-line comment body
|
|
967
|
-
content.startsWith('*/') || // Multi-line comment end
|
|
968
|
-
content.startsWith('#') || // Shell/Python/Ruby comments
|
|
969
|
-
content.startsWith('<!--') || // HTML comments
|
|
970
|
-
content.startsWith('-->') || // HTML comment end
|
|
971
|
-
content.startsWith('"""') || // Python docstring
|
|
972
|
-
content.startsWith("'''") || // Python docstring
|
|
973
|
-
/^\/\*\*/.test(content) || // JSDoc start
|
|
974
|
-
/^\*\s*@\w+/.test(content); // JSDoc tag
|
|
975
|
-
if (isComment) {
|
|
976
|
-
hasCommentChanges = true;
|
|
977
|
-
}
|
|
978
|
-
else {
|
|
979
|
-
// Found a non-comment change - not a comment-only diff
|
|
980
|
-
return false;
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
return hasCommentChanges;
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Perform semantic analysis on the diff to understand the nature of changes
|
|
987
|
-
* This provides intent-based understanding rather than line-count metrics
|
|
988
|
-
*/
|
|
989
|
-
static analyzeSemanticChanges(diff, stagedFiles) {
|
|
990
|
-
const roleChanges = this.detectRoleChanges(diff, stagedFiles);
|
|
991
|
-
const primaryRole = this.determinePrimaryRole(roleChanges);
|
|
992
|
-
let primaryIntent = this.determineIntent(diff, stagedFiles, roleChanges);
|
|
993
|
-
const affectedElements = this.extractAffectedElements(diff);
|
|
994
|
-
const hunkContext = this.extractHunkContext(diff);
|
|
995
|
-
// Detect renames - this takes priority for determining intent
|
|
996
|
-
const renames = this.detectRenames(diff);
|
|
997
|
-
if (renames.length > 0) {
|
|
998
|
-
primaryIntent = 'rename';
|
|
999
|
-
}
|
|
1000
|
-
// Generate human-readable descriptions
|
|
1001
|
-
const intentDescription = this.generateIntentDescription(primaryIntent, primaryRole, roleChanges);
|
|
1002
|
-
const whatChanged = renames.length > 0
|
|
1003
|
-
? `${renames[0].oldName} to ${renames[0].newName}`
|
|
1004
|
-
: this.generateWhatChanged(roleChanges, affectedElements, stagedFiles, hunkContext);
|
|
1005
|
-
return {
|
|
1006
|
-
primaryRole,
|
|
1007
|
-
primaryIntent,
|
|
1008
|
-
roleChanges,
|
|
1009
|
-
intentDescription,
|
|
1010
|
-
whatChanged,
|
|
1011
|
-
hasMultipleRoles: roleChanges.filter(r => r.significance > 20).length > 1,
|
|
1012
|
-
renames: renames.length > 0 ? renames : undefined,
|
|
1013
|
-
hunkContext: hunkContext.length > 0 ? hunkContext : undefined,
|
|
1014
|
-
};
|
|
1015
|
-
}
|
|
1016
|
-
/**
|
|
1017
|
-
* Detect which roles are affected by the changes and calculate semantic significance
|
|
1018
|
-
* Significance is NOT based on line count - it's based on the semantic weight of patterns
|
|
1019
|
-
*/
|
|
1020
|
-
static detectRoleChanges(diff, _stagedFiles) {
|
|
1021
|
-
const roleChanges = [];
|
|
1022
|
-
// Check each role for matches
|
|
1023
|
-
for (const [role, patterns] of Object.entries(ROLE_PATTERNS)) {
|
|
1024
|
-
if (role === 'unknown' || patterns.length === 0)
|
|
1025
|
-
continue;
|
|
1026
|
-
let matchCount = 0;
|
|
1027
|
-
let highValueMatches = 0;
|
|
1028
|
-
for (const pattern of patterns) {
|
|
1029
|
-
pattern.lastIndex = 0;
|
|
1030
|
-
const matches = diff.match(pattern);
|
|
1031
|
-
if (matches) {
|
|
1032
|
-
matchCount += matches.length;
|
|
1033
|
-
// Some patterns indicate more significant changes
|
|
1034
|
-
if (this.isHighValuePattern(pattern, role)) {
|
|
1035
|
-
highValueMatches += matches.length;
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
if (matchCount > 0) {
|
|
1040
|
-
// Calculate significance based on pattern matches, not line counts
|
|
1041
|
-
// High-value patterns contribute more to significance
|
|
1042
|
-
const baseSignificance = Math.min(matchCount * 10, 40);
|
|
1043
|
-
const highValueBonus = highValueMatches * 15;
|
|
1044
|
-
const significance = Math.min(baseSignificance + highValueBonus, 100);
|
|
1045
|
-
const intent = this.detectRoleIntent(diff, role);
|
|
1046
|
-
const summary = this.generateRoleSummary(role, intent, matchCount);
|
|
1047
|
-
roleChanges.push({
|
|
1048
|
-
role,
|
|
1049
|
-
intent,
|
|
1050
|
-
significance,
|
|
1051
|
-
summary,
|
|
1052
|
-
affectedElements: this.extractElementsForRole(diff, role),
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
// Sort by significance (highest first)
|
|
1057
|
-
return roleChanges.sort((a, b) => b.significance - a.significance);
|
|
1058
|
-
}
|
|
1059
|
-
/**
|
|
1060
|
-
* Determine if a pattern represents a high-value semantic change
|
|
1061
|
-
*/
|
|
1062
|
-
static isHighValuePattern(pattern, role) {
|
|
1063
|
-
const patternStr = pattern.source;
|
|
1064
|
-
// High-value patterns for each role
|
|
1065
|
-
const highValueIndicators = {
|
|
1066
|
-
ui: ['<[A-Z]', 'className', 'onClick', 'onSubmit', 'aria-'],
|
|
1067
|
-
logic: ['function', 'class', 'if\\s*\\(', 'switch', 'try\\s*\\{', 'throw'],
|
|
1068
|
-
data: ['useState', 'useReducer', 'interface', 'type\\s+\\w+'],
|
|
1069
|
-
style: ['styled\\.', 'css`', 'theme\\.'],
|
|
1070
|
-
api: ['fetch\\s*\\(', 'axios', 'useQuery', 'useMutation', 'graphql'],
|
|
1071
|
-
config: ['process\\.env', 'CONFIG\\.', 'export\\s+(const|let)\\s+[A-Z_]+'],
|
|
1072
|
-
test: ['describe\\s*\\(', 'it\\s*\\(', 'test\\s*\\(', 'expect\\s*\\('],
|
|
1073
|
-
unknown: [],
|
|
1074
|
-
};
|
|
1075
|
-
return highValueIndicators[role].some(indicator => patternStr.includes(indicator));
|
|
1076
|
-
}
|
|
1077
|
-
/**
|
|
1078
|
-
* Detect the intent for changes in a specific role
|
|
1079
|
-
*/
|
|
1080
|
-
static detectRoleIntent(diff, _role) {
|
|
1081
|
-
// Check for intent patterns (role context reserved for future use)
|
|
1082
|
-
const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
|
|
1083
|
-
const removedLines = (diff.match(/^-[^-]/gm) || []).length;
|
|
1084
|
-
// Check for add patterns in this role's context
|
|
1085
|
-
for (const pattern of INTENT_PATTERNS.add) {
|
|
1086
|
-
if (pattern.test(diff)) {
|
|
1087
|
-
// If adding new constructs, it's an 'add' intent
|
|
1088
|
-
return 'add';
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
// Check for remove patterns
|
|
1092
|
-
for (const pattern of INTENT_PATTERNS.remove) {
|
|
1093
|
-
if (pattern.test(diff)) {
|
|
1094
|
-
return 'remove';
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
// Check for fix patterns
|
|
1098
|
-
for (const pattern of INTENT_PATTERNS.fix) {
|
|
1099
|
-
if (pattern.test(diff)) {
|
|
1100
|
-
return 'fix';
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
// Check for enhance patterns
|
|
1104
|
-
for (const pattern of INTENT_PATTERNS.enhance) {
|
|
1105
|
-
if (pattern.test(diff)) {
|
|
1106
|
-
return 'enhance';
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
// Determine based on add/remove ratio
|
|
1110
|
-
if (addedLines > 0 && removedLines === 0) {
|
|
1111
|
-
return 'add';
|
|
1112
|
-
}
|
|
1113
|
-
else if (removedLines > 0 && addedLines === 0) {
|
|
1114
|
-
return 'remove';
|
|
1115
|
-
}
|
|
1116
|
-
else if (addedLines > 0 && removedLines > 0) {
|
|
1117
|
-
const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
|
|
1118
|
-
if (ratio > 0.5) {
|
|
1119
|
-
return 'refactor'; // Balanced changes suggest refactoring
|
|
1120
|
-
}
|
|
1121
|
-
return addedLines > removedLines ? 'add' : 'modify';
|
|
1122
|
-
}
|
|
1123
|
-
return 'modify';
|
|
1124
|
-
}
|
|
1125
|
-
/**
|
|
1126
|
-
* Determine the primary role from all detected role changes
|
|
1127
|
-
*/
|
|
1128
|
-
static determinePrimaryRole(roleChanges) {
|
|
1129
|
-
if (roleChanges.length === 0) {
|
|
1130
|
-
return 'unknown';
|
|
1131
|
-
}
|
|
1132
|
-
// The role with highest significance is primary
|
|
1133
|
-
// But if multiple roles have similar significance, prefer more specific ones
|
|
1134
|
-
const topRole = roleChanges[0];
|
|
1135
|
-
// If there's a close second that's more specific, consider it
|
|
1136
|
-
if (roleChanges.length > 1) {
|
|
1137
|
-
const secondRole = roleChanges[1];
|
|
1138
|
-
const significanceDiff = topRole.significance - secondRole.significance;
|
|
1139
|
-
// If within 15 points and second is more specific (api > logic > ui)
|
|
1140
|
-
if (significanceDiff <= 15) {
|
|
1141
|
-
const specificityOrder = ['api', 'data', 'style', 'logic', 'ui', 'config', 'test', 'unknown'];
|
|
1142
|
-
const topIndex = specificityOrder.indexOf(topRole.role);
|
|
1143
|
-
const secondIndex = specificityOrder.indexOf(secondRole.role);
|
|
1144
|
-
if (secondIndex < topIndex) {
|
|
1145
|
-
return secondRole.role;
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
return topRole.role;
|
|
1150
|
-
}
|
|
1151
|
-
/**
|
|
1152
|
-
* Determine the overall intent of the changes
|
|
1153
|
-
*/
|
|
1154
|
-
static determineIntent(diff, stagedFiles, roleChanges) {
|
|
1155
|
-
// Check file statuses first
|
|
1156
|
-
const hasOnlyAdded = stagedFiles.every(f => f.status === 'A');
|
|
1157
|
-
const hasOnlyDeleted = stagedFiles.every(f => f.status === 'D');
|
|
1158
|
-
const hasOnlyModified = stagedFiles.every(f => f.status === 'M');
|
|
1159
|
-
if (hasOnlyAdded) {
|
|
1160
|
-
return 'add';
|
|
1161
|
-
}
|
|
1162
|
-
if (hasOnlyDeleted) {
|
|
1163
|
-
return 'remove';
|
|
1164
|
-
}
|
|
1165
|
-
// If we have role changes, use the most significant role's intent
|
|
1166
|
-
if (roleChanges.length > 0) {
|
|
1167
|
-
const primaryRoleChange = roleChanges[0];
|
|
1168
|
-
// Special case: if the primary intent is 'fix' and we see validation patterns
|
|
1169
|
-
// even in non-modified files, treat it as a fix
|
|
1170
|
-
if (hasOnlyModified && primaryRoleChange.intent === 'fix') {
|
|
1171
|
-
return 'fix';
|
|
1172
|
-
}
|
|
1173
|
-
// If we're enhancing (useMemo, useCallback, etc.), that takes precedence
|
|
1174
|
-
if (roleChanges.some(r => r.intent === 'enhance')) {
|
|
1175
|
-
return 'enhance';
|
|
1176
|
-
}
|
|
1177
|
-
return primaryRoleChange.intent;
|
|
1178
|
-
}
|
|
1179
|
-
// Fallback to diff analysis
|
|
1180
|
-
const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
|
|
1181
|
-
const removedLines = (diff.match(/^-[^-]/gm) || []).length;
|
|
1182
|
-
if (addedLines > 0 && removedLines === 0) {
|
|
1183
|
-
return 'add';
|
|
1184
|
-
}
|
|
1185
|
-
if (removedLines > 0 && addedLines === 0) {
|
|
1186
|
-
return 'remove';
|
|
1187
|
-
}
|
|
1188
|
-
const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
|
|
1189
|
-
if (ratio > 0.6) {
|
|
1190
|
-
return 'refactor';
|
|
1191
|
-
}
|
|
1192
|
-
return 'modify';
|
|
1193
|
-
}
|
|
1194
|
-
/**
|
|
1195
|
-
* Extract affected element names (components, functions, etc.) from the diff
|
|
1196
|
-
*/
|
|
1197
|
-
static extractAffectedElements(diff) {
|
|
1198
|
-
const elements = [];
|
|
1199
|
-
// Helper to add unique element names
|
|
1200
|
-
const addUnique = (name) => {
|
|
1201
|
-
if (!elements.includes(name)) {
|
|
1202
|
-
elements.push(name);
|
|
1203
|
-
}
|
|
1204
|
-
};
|
|
1205
|
-
// Match both added (+) and modified (-) top-level declarations.
|
|
1206
|
-
// For fix/refactor commits, the declaration line often appears as a removed line
|
|
1207
|
-
// (the old version) or only in the - side of the diff. Using [+-] ensures we
|
|
1208
|
-
// capture the affected function/class/component name regardless of whether
|
|
1209
|
-
// the declaration itself was added, removed, or modified.
|
|
1210
|
-
// (?!\s) after [+-] rejects indented lines (local variables inside function bodies)
|
|
1211
|
-
// Extract component names (PascalCase function/const declarations)
|
|
1212
|
-
const componentFuncMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+([A-Z][a-zA-Z0-9]*)/gm);
|
|
1213
|
-
if (componentFuncMatches) {
|
|
1214
|
-
for (const match of componentFuncMatches) {
|
|
1215
|
-
const nameMatch = match.match(/function\s+([A-Z][a-zA-Z0-9]*)/);
|
|
1216
|
-
if (nameMatch)
|
|
1217
|
-
addUnique(nameMatch[1]);
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
const componentConstMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?const\s+([A-Z][a-zA-Z0-9]*)\s*=\s*(?:async\s+)?(?:function|\()/gm);
|
|
1221
|
-
if (componentConstMatches) {
|
|
1222
|
-
for (const match of componentConstMatches) {
|
|
1223
|
-
const nameMatch = match.match(/const\s+([A-Z][a-zA-Z0-9]*)/);
|
|
1224
|
-
if (nameMatch)
|
|
1225
|
-
addUnique(nameMatch[1]);
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
// Extract function names (camelCase declarations)
|
|
1229
|
-
const funcKeywordMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+([a-z][a-zA-Z0-9]*)\s*\(/gm);
|
|
1230
|
-
if (funcKeywordMatches) {
|
|
1231
|
-
for (const match of funcKeywordMatches) {
|
|
1232
|
-
const nameMatch = match.match(/function\s+([a-z][a-zA-Z0-9]*)/);
|
|
1233
|
-
if (nameMatch)
|
|
1234
|
-
addUnique(nameMatch[1]);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
// const/let arrow or function expression: const foo = (, export const foo = async (
|
|
1238
|
-
const funcConstMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?const\s+([a-z][a-zA-Z0-9]*)\s*=\s*(?:async\s+)?(?:function|\()/gm);
|
|
1239
|
-
if (funcConstMatches) {
|
|
1240
|
-
for (const match of funcConstMatches) {
|
|
1241
|
-
const nameMatch = match.match(/const\s+([a-z][a-zA-Z0-9]*)/);
|
|
1242
|
-
if (nameMatch)
|
|
1243
|
-
addUnique(nameMatch[1]);
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
// Extract interface/type names
|
|
1247
|
-
const typeMatches = diff.match(/^[+-](?!\s)(?:export\s+)?(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/gm);
|
|
1248
|
-
if (typeMatches) {
|
|
1249
|
-
for (const match of typeMatches) {
|
|
1250
|
-
const nameMatch = match.match(/(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/);
|
|
1251
|
-
if (nameMatch)
|
|
1252
|
-
addUnique(nameMatch[1]);
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
// Extract class names
|
|
1256
|
-
const classMatches = diff.match(/^[+-](?!\s)(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Z][a-zA-Z0-9]*)/gm);
|
|
1257
|
-
if (classMatches) {
|
|
1258
|
-
for (const match of classMatches) {
|
|
1259
|
-
const nameMatch = match.match(/class\s+([A-Z][a-zA-Z0-9]*)/);
|
|
1260
|
-
if (nameMatch)
|
|
1261
|
-
addUnique(nameMatch[1]);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
return elements.slice(0, 5); // Limit to top 5 elements
|
|
1265
|
-
}
|
|
1266
|
-
/**
|
|
1267
|
-
* Extract function/class/method names from git diff by analyzing both hunk headers
|
|
1268
|
-
* and the actual context lines around changes.
|
|
1269
|
-
*
|
|
1270
|
-
* This method prioritizes function declarations found in context lines near the
|
|
1271
|
-
* actual changes, which is more accurate than just using hunk headers (which may
|
|
1272
|
-
* show preceding functions instead of the one being modified).
|
|
1273
|
-
*/
|
|
1274
|
-
static extractHunkContext(diff) {
|
|
1275
|
-
const names = [];
|
|
1276
|
-
// Split diff into hunks
|
|
1277
|
-
const hunks = diff.split(/^@@/gm).slice(1); // Skip header before first @@
|
|
1278
|
-
for (const hunk of hunks) {
|
|
1279
|
-
const lines = hunk.split('\n');
|
|
1280
|
-
let lastFunctionName = null;
|
|
1281
|
-
let foundChange = false;
|
|
1282
|
-
// Skip first line (hunk header like " -425,7 +425,7 @@ function normalize...")
|
|
1283
|
-
for (let i = 1; i < lines.length; i++) {
|
|
1284
|
-
const line = lines[i];
|
|
1285
|
-
// Check if this is a context line (starts with space) or added line (starts with +)
|
|
1286
|
-
// Context lines show surrounding code that hasn't changed
|
|
1287
|
-
if (line.startsWith(' ') || (line.startsWith('+') && !line.startsWith('+++'))) {
|
|
1288
|
-
const extractedName = this.extractNameFromLine(line);
|
|
1289
|
-
if (extractedName) {
|
|
1290
|
-
lastFunctionName = extractedName;
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
// If we find an actual change (+/-) and we have a function name, record it
|
|
1294
|
-
if ((line.startsWith('+') || line.startsWith('-')) &&
|
|
1295
|
-
!line.startsWith('+++') && !line.startsWith('---') &&
|
|
1296
|
-
lastFunctionName &&
|
|
1297
|
-
!foundChange) {
|
|
1298
|
-
if (!names.includes(lastFunctionName)) {
|
|
1299
|
-
names.push(lastFunctionName);
|
|
1300
|
-
}
|
|
1301
|
-
foundChange = true; // Only record once per hunk
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
// Fallback: if no function found in context lines, try hunk header
|
|
1305
|
-
if (!foundChange && lines[0]) {
|
|
1306
|
-
const headerMatch = lines[0].match(/^@?\s+[^@]+@@\s+(.+)$/);
|
|
1307
|
-
if (headerMatch) {
|
|
1308
|
-
const name = this.extractNameFromLine(headerMatch[1]);
|
|
1309
|
-
if (name && !names.includes(name)) {
|
|
1310
|
-
names.push(name);
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
return names.slice(0, 5);
|
|
1316
|
-
}
|
|
1317
|
-
/**
|
|
1318
|
-
* Extract a function/class/method name from a single line of code
|
|
1319
|
-
*/
|
|
1320
|
-
static extractNameFromLine(line) {
|
|
1321
|
-
// Remove leading diff markers and whitespace
|
|
1322
|
-
const cleanLine = line.replace(/^[+\-\s@]*/, '').trim();
|
|
1323
|
-
// Skip lines that are clearly control flow (for, if, while, etc.)
|
|
1324
|
-
if (/^\s*(if|else|for|while|do|switch|catch|return|throw)\s*\(/.test(cleanLine)) {
|
|
1325
|
-
return null;
|
|
1326
|
-
}
|
|
1327
|
-
// function declarations: function foo(, async function foo(, export function foo(
|
|
1328
|
-
const funcMatch = cleanLine.match(/(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
1329
|
-
if (funcMatch)
|
|
1330
|
-
return funcMatch[1];
|
|
1331
|
-
// const/let/var arrow or function expression: const foo = (, export const foo = async (
|
|
1332
|
-
// Only match if it looks like a function (has = followed by function keyword, arrow, or paren)
|
|
1333
|
-
const constMatch = cleanLine.match(/(?:export\s+(?:default\s+)?)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\(|[a-zA-Z_$])/);
|
|
1334
|
-
if (constMatch)
|
|
1335
|
-
return constMatch[1];
|
|
1336
|
-
// class declarations: class Foo {, export class Foo extends Bar {
|
|
1337
|
-
const classMatch = cleanLine.match(/(?:export\s+(?:default\s+)?)?(?:abstract\s+)?class\s+([A-Z][a-zA-Z0-9]*)/);
|
|
1338
|
-
if (classMatch)
|
|
1339
|
-
return classMatch[1];
|
|
1340
|
-
// class method: methodName(, async methodName(, private methodName(, static async methodName(
|
|
1341
|
-
// Exclude control flow keywords (if, for, while, etc.) which share the `keyword(` shape
|
|
1342
|
-
const methodMatch = cleanLine.match(/(?:public|private|protected)?\s*(?:static\s+)?(?:async\s+)?(?!if|else|for|while|do|switch|catch|return|throw|new|typeof|instanceof|delete|void|yield|await\b)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/);
|
|
1343
|
-
if (methodMatch)
|
|
1344
|
-
return methodMatch[1];
|
|
1345
|
-
// interface/type declarations
|
|
1346
|
-
const typeMatch = cleanLine.match(/(?:export\s+)?(?:interface|type)\s+([A-Z][a-zA-Z0-9]*)/);
|
|
1347
|
-
if (typeMatch)
|
|
1348
|
-
return typeMatch[1];
|
|
1349
|
-
return null;
|
|
1350
|
-
}
|
|
1351
|
-
/**
|
|
1352
|
-
* Detect function/class/type renames by comparing removed and added names
|
|
1353
|
-
* Returns an array of { oldName, newName } objects
|
|
1354
|
-
*/
|
|
1355
|
-
static detectRenames(diff) {
|
|
1356
|
-
const renames = [];
|
|
1357
|
-
// Patterns for extracting function/class/type names from removed and added lines
|
|
1358
|
-
const patterns = [
|
|
1359
|
-
// Function declarations: function name( or const name = or const name: Type =
|
|
1360
|
-
{ regex: /^[-+].*(?:function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/gm, type: 'function' },
|
|
1361
|
-
{ regex: /^[-+].*(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*[=:]/gm, type: 'function' },
|
|
1362
|
-
// Class declarations
|
|
1363
|
-
{ regex: /^[-+].*class\s+([A-Z][a-zA-Z0-9]*)/gm, type: 'class' },
|
|
1364
|
-
// Interface declarations
|
|
1365
|
-
{ regex: /^[-+].*interface\s+([A-Z][a-zA-Z0-9]*)/gm, type: 'interface' },
|
|
1366
|
-
// Type declarations
|
|
1367
|
-
{ regex: /^[-+].*type\s+([A-Z][a-zA-Z0-9]*)\s*=/gm, type: 'type' },
|
|
1368
|
-
];
|
|
1369
|
-
for (const { regex, type } of patterns) {
|
|
1370
|
-
const removed = [];
|
|
1371
|
-
const added = [];
|
|
1372
|
-
let match;
|
|
1373
|
-
regex.lastIndex = 0;
|
|
1374
|
-
while ((match = regex.exec(diff)) !== null) {
|
|
1375
|
-
const line = match[0];
|
|
1376
|
-
const name = match[1];
|
|
1377
|
-
if (line.startsWith('-') && !line.startsWith('---')) {
|
|
1378
|
-
removed.push(name);
|
|
1379
|
-
}
|
|
1380
|
-
else if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
1381
|
-
added.push(name);
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
// If we have exactly one removed and one added of the same type,
|
|
1385
|
-
// and they're different names, it's likely a rename
|
|
1386
|
-
if (removed.length === 1 && added.length === 1 && removed[0] !== added[0]) {
|
|
1387
|
-
renames.push({ oldName: removed[0], newName: added[0], type });
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
return renames;
|
|
1391
|
-
}
|
|
1392
|
-
/**
|
|
1393
|
-
* Extract affected elements for a specific role
|
|
1394
|
-
*/
|
|
1395
|
-
static extractElementsForRole(diff, role) {
|
|
1396
|
-
const elements = [];
|
|
1397
|
-
switch (role) {
|
|
1398
|
-
case 'ui':
|
|
1399
|
-
// Extract JSX component names being used
|
|
1400
|
-
const jsxMatches = diff.match(/^\+.*<([A-Z][a-zA-Z0-9]*)/gm);
|
|
1401
|
-
if (jsxMatches) {
|
|
1402
|
-
for (const match of jsxMatches) {
|
|
1403
|
-
const nameMatch = match.match(/<([A-Z][a-zA-Z0-9]*)/);
|
|
1404
|
-
if (nameMatch && !elements.includes(nameMatch[1])) {
|
|
1405
|
-
elements.push(nameMatch[1]);
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
break;
|
|
1410
|
-
case 'data':
|
|
1411
|
-
// Extract state variable names
|
|
1412
|
-
const stateMatches = diff.match(/^\+.*const\s+\[([a-z][a-zA-Z0-9]*),/gm);
|
|
1413
|
-
if (stateMatches) {
|
|
1414
|
-
for (const match of stateMatches) {
|
|
1415
|
-
const nameMatch = match.match(/const\s+\[([a-z][a-zA-Z0-9]*)/);
|
|
1416
|
-
if (nameMatch) {
|
|
1417
|
-
elements.push(nameMatch[1]);
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
break;
|
|
1422
|
-
case 'api':
|
|
1423
|
-
// Extract API endpoints
|
|
1424
|
-
const apiMatches = diff.match(/['"`]\/api\/[^'"`]+['"`]/g);
|
|
1425
|
-
if (apiMatches) {
|
|
1426
|
-
elements.push(...apiMatches.map(m => m.replace(/['"`]/g, '')));
|
|
1427
|
-
}
|
|
1428
|
-
break;
|
|
1429
|
-
default:
|
|
1430
|
-
// Use generic extraction
|
|
1431
|
-
return this.extractAffectedElements(diff).slice(0, 3);
|
|
1432
|
-
}
|
|
1433
|
-
return elements.slice(0, 3);
|
|
1434
|
-
}
|
|
1435
|
-
/**
|
|
1436
|
-
* Generate a human-readable summary for a role change
|
|
1437
|
-
*/
|
|
1438
|
-
static generateRoleSummary(role, intent, matchCount) {
|
|
1439
|
-
const roleDesc = ROLE_DESCRIPTIONS[role];
|
|
1440
|
-
const intentVerb = INTENT_VERBS[intent].past;
|
|
1441
|
-
if (matchCount === 1) {
|
|
1442
|
-
return `${intentVerb} ${roleDesc}`;
|
|
1443
|
-
}
|
|
1444
|
-
return `${intentVerb} ${roleDesc} (${matchCount} changes)`;
|
|
1445
|
-
}
|
|
1446
|
-
/**
|
|
1447
|
-
* Generate the WHY description for the commit
|
|
1448
|
-
*/
|
|
1449
|
-
static generateIntentDescription(intent, role, roleChanges) {
|
|
1450
|
-
const roleDesc = ROLE_DESCRIPTIONS[role];
|
|
1451
|
-
switch (intent) {
|
|
1452
|
-
case 'add':
|
|
1453
|
-
return `to add new ${roleDesc}`;
|
|
1454
|
-
case 'fix':
|
|
1455
|
-
return `to fix ${roleDesc} issues`;
|
|
1456
|
-
case 'refactor':
|
|
1457
|
-
return `to improve ${roleDesc} structure`;
|
|
1458
|
-
case 'enhance':
|
|
1459
|
-
return `to optimize ${roleDesc} performance`;
|
|
1460
|
-
case 'remove':
|
|
1461
|
-
return `to remove unused ${roleDesc}`;
|
|
1462
|
-
case 'modify':
|
|
1463
|
-
default:
|
|
1464
|
-
if (roleChanges.length > 1) {
|
|
1465
|
-
return `to update ${roleDesc} and related code`;
|
|
1466
|
-
}
|
|
1467
|
-
return `to update ${roleDesc}`;
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
/**
|
|
1471
|
-
* Generate the WHAT changed description
|
|
1472
|
-
*/
|
|
1473
|
-
static generateWhatChanged(roleChanges, affectedElements, stagedFiles, hunkContext) {
|
|
1474
|
-
// If we have specific elements from declarations, use them
|
|
1475
|
-
if (affectedElements.length > 0) {
|
|
1476
|
-
if (affectedElements.length === 1) {
|
|
1477
|
-
return affectedElements[0];
|
|
1478
|
-
}
|
|
1479
|
-
if (affectedElements.length <= 3) {
|
|
1480
|
-
return affectedElements.join(', ');
|
|
1481
|
-
}
|
|
1482
|
-
return `${affectedElements.slice(0, 2).join(', ')} and ${affectedElements.length - 2} more`;
|
|
1483
|
-
}
|
|
1484
|
-
// Fall back to hunk context (function/class names from @@ headers)
|
|
1485
|
-
// This is especially useful for fix/refactor where changes are inside
|
|
1486
|
-
// function bodies and the declaration line itself isn't in the diff
|
|
1487
|
-
if (hunkContext && hunkContext.length > 0) {
|
|
1488
|
-
if (hunkContext.length === 1) {
|
|
1489
|
-
return hunkContext[0];
|
|
1490
|
-
}
|
|
1491
|
-
if (hunkContext.length <= 3) {
|
|
1492
|
-
return hunkContext.join(', ');
|
|
1493
|
-
}
|
|
1494
|
-
return `${hunkContext.slice(0, 2).join(', ')} and ${hunkContext.length - 2} more`;
|
|
1495
|
-
}
|
|
1496
|
-
// Fall back to role-based description
|
|
1497
|
-
if (roleChanges.length > 0) {
|
|
1498
|
-
const primaryRole = roleChanges[0];
|
|
1499
|
-
if (primaryRole.affectedElements.length > 0) {
|
|
1500
|
-
return primaryRole.affectedElements[0];
|
|
1501
|
-
}
|
|
1502
|
-
return ROLE_DESCRIPTIONS[primaryRole.role];
|
|
1503
|
-
}
|
|
1504
|
-
// Fall back to file names
|
|
1505
|
-
if (stagedFiles.length === 1) {
|
|
1506
|
-
const parts = stagedFiles[0].path.split('/');
|
|
1507
|
-
return parts[parts.length - 1].replace(/\.\w+$/, '');
|
|
1508
|
-
}
|
|
1509
|
-
return 'code';
|
|
1510
|
-
}
|
|
1511
|
-
/**
|
|
1512
|
-
* Generate a description from AST analysis results.
|
|
1513
|
-
* Produces specific, human-readable descriptions based on structural signals.
|
|
1514
|
-
* Returns empty string if AST data is insufficient for a good description.
|
|
1515
|
-
*/
|
|
1516
|
-
static generateASTDescription(astResult, commitType) {
|
|
1517
|
-
const { declarations, signals } = astResult;
|
|
1518
|
-
// Handle renames โ most specific and readable
|
|
1519
|
-
if (declarations.renamed.length > 0) {
|
|
1520
|
-
const r = declarations.renamed[0];
|
|
1521
|
-
if (declarations.renamed.length === 1) {
|
|
1522
|
-
return `rename ${r.oldName} to ${r.newName}`;
|
|
1523
|
-
}
|
|
1524
|
-
return `rename ${r.oldName} to ${r.newName} and ${declarations.renamed.length - 1} more`;
|
|
1525
|
-
}
|
|
1526
|
-
// Handle removed exported declarations (breaking refactor)
|
|
1527
|
-
const removedExported = declarations.removed.filter(d => d.exported);
|
|
1528
|
-
if (removedExported.length > 0) {
|
|
1529
|
-
if (removedExported.length === 1) {
|
|
1530
|
-
return `remove ${removedExported[0].name} export`;
|
|
1531
|
-
}
|
|
1532
|
-
return `remove ${removedExported[0].name} and ${removedExported.length - 1} more exports`;
|
|
1533
|
-
}
|
|
1534
|
-
// Handle breaking signature changes
|
|
1535
|
-
const sigChangeSignals = signals.filter(s => s.signal === 'changed-exported-sig');
|
|
1536
|
-
if (sigChangeSignals.length > 0) {
|
|
1537
|
-
const nameMatch = sigChangeSignals[0].detail.match(/on '([^']+)'/);
|
|
1538
|
-
if (nameMatch) {
|
|
1539
|
-
return `change ${nameMatch[1]} signature`;
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
// Handle new declarations (feat)
|
|
1543
|
-
if (commitType === 'feat' && declarations.added.length > 0) {
|
|
1544
|
-
const exported = declarations.added.filter(d => d.exported);
|
|
1545
|
-
const target = exported.length > 0 ? exported : declarations.added;
|
|
1546
|
-
if (target.length === 1) {
|
|
1547
|
-
return `add ${target[0].name} ${target[0].kind}`;
|
|
1548
|
-
}
|
|
1549
|
-
if (target.length === 2) {
|
|
1550
|
-
return `add ${target[0].name} and ${target[1].name}`;
|
|
1551
|
-
}
|
|
1552
|
-
return `add ${target[0].name}, ${target[1].name} and ${target.length - 2} more`;
|
|
1553
|
-
}
|
|
1554
|
-
// Handle fix signals (error handling, guards)
|
|
1555
|
-
if (commitType === 'fix') {
|
|
1556
|
-
const errorSignals = signals.filter(s => s.signal === 'error-handling-change');
|
|
1557
|
-
if (errorSignals.length > 0) {
|
|
1558
|
-
const nameMatch = errorSignals[0].detail.match(/in '([^']+)'/);
|
|
1559
|
-
if (nameMatch) {
|
|
1560
|
-
return `handle errors in ${nameMatch[1]}`;
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
const guardSignals = signals.filter(s => s.signal === 'guard-added');
|
|
1564
|
-
if (guardSignals.length > 0) {
|
|
1565
|
-
const nameMatch = guardSignals[0].detail.match(/in '([^']+)'/);
|
|
1566
|
-
if (nameMatch) {
|
|
1567
|
-
return `add validation to ${nameMatch[1]}`;
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
// Literal-only changes
|
|
1571
|
-
const literalSignals = signals.filter(s => s.signal === 'literal-only-change');
|
|
1572
|
-
if (literalSignals.length > 0) {
|
|
1573
|
-
const nameMatch = literalSignals[0].detail.match(/in '([^']+)'/);
|
|
1574
|
-
if (nameMatch) {
|
|
1575
|
-
return `fix values in ${nameMatch[1]}`;
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
// Handle perf signals
|
|
1580
|
-
if (commitType === 'perf') {
|
|
1581
|
-
const perfSignals = signals.filter(s => s.signal === 'perf-pattern');
|
|
1582
|
-
if (perfSignals.length > 0) {
|
|
1583
|
-
const nameMatch = perfSignals[0].detail.match(/in '([^']+)'/);
|
|
1584
|
-
if (nameMatch) {
|
|
1585
|
-
return `optimize ${nameMatch[1]} performance`;
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
// Handle refactor signals (control flow, modified)
|
|
1590
|
-
if (commitType === 'refactor') {
|
|
1591
|
-
const controlFlowSignals = signals.filter(s => s.signal === 'control-flow-restructured');
|
|
1592
|
-
if (controlFlowSignals.length > 0) {
|
|
1593
|
-
const nameMatch = controlFlowSignals[0].detail.match(/in '([^']+)'/);
|
|
1594
|
-
if (nameMatch) {
|
|
1595
|
-
return `simplify ${nameMatch[1]} control flow`;
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
if (declarations.modified.length > 0) {
|
|
1599
|
-
const names = declarations.modified.map(m => m.name);
|
|
1600
|
-
if (names.length === 1) {
|
|
1601
|
-
return `refactor ${names[0]}`;
|
|
1602
|
-
}
|
|
1603
|
-
if (names.length === 2) {
|
|
1604
|
-
return `refactor ${names[0]} and ${names[1]}`;
|
|
1605
|
-
}
|
|
1606
|
-
return `refactor ${names[0]}, ${names[1]} and ${names.length - 2} more`;
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
// Generic fallback using affectedElements
|
|
1610
|
-
if (astResult.affectedElements.length > 0) {
|
|
1611
|
-
const verb = commitType === 'feat' ? 'add' :
|
|
1612
|
-
commitType === 'fix' ? 'fix' :
|
|
1613
|
-
commitType === 'refactor' ? 'refactor' :
|
|
1614
|
-
commitType === 'perf' ? 'optimize' : 'update';
|
|
1615
|
-
const els = astResult.affectedElements;
|
|
1616
|
-
if (els.length === 1) {
|
|
1617
|
-
return `${verb} ${els[0]}`;
|
|
1618
|
-
}
|
|
1619
|
-
if (els.length === 2) {
|
|
1620
|
-
return `${verb} ${els[0]} and ${els[1]}`;
|
|
1621
|
-
}
|
|
1622
|
-
return `${verb} ${els[0]}, ${els[1]} and ${els.length - 2} more`;
|
|
1623
|
-
}
|
|
1624
|
-
return '';
|
|
1625
|
-
}
|
|
1626
|
-
/**
|
|
1627
|
-
* Detect test+implementation file pairs.
|
|
1628
|
-
* Returns the pair info if files match naming conventions, null otherwise.
|
|
1629
|
-
*/
|
|
1630
|
-
static detectTestImplPair(stagedFiles) {
|
|
1631
|
-
if (stagedFiles.length < 2)
|
|
1632
|
-
return null;
|
|
1633
|
-
for (const file of stagedFiles) {
|
|
1634
|
-
const path = file.path;
|
|
1635
|
-
// Check if this is a test file
|
|
1636
|
-
const testMatch = path.match(/^(.+)\.(test|spec)\.(ts|tsx|js|jsx|mts|cts)$/);
|
|
1637
|
-
const dirTestMatch = path.match(/^(.+\/)__tests__\/(.+)\.(ts|tsx|js|jsx|mts|cts)$/);
|
|
1638
|
-
if (testMatch) {
|
|
1639
|
-
const implPath = `${testMatch[1]}.${testMatch[3]}`;
|
|
1640
|
-
const implFile = stagedFiles.find(f => f.path === implPath);
|
|
1641
|
-
if (implFile) {
|
|
1642
|
-
return {
|
|
1643
|
-
implFile: this.getFileName(implFile.path),
|
|
1644
|
-
testFile: this.getFileName(file.path),
|
|
1645
|
-
implStatus: implFile.status,
|
|
1646
|
-
testStatus: file.status,
|
|
1647
|
-
};
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
if (dirTestMatch) {
|
|
1651
|
-
const implPath = `${dirTestMatch[1]}${dirTestMatch[2]}.${dirTestMatch[3]}`;
|
|
1652
|
-
const implFile = stagedFiles.find(f => f.path === implPath);
|
|
1653
|
-
if (implFile) {
|
|
1654
|
-
return {
|
|
1655
|
-
implFile: this.getFileName(implFile.path),
|
|
1656
|
-
testFile: this.getFileName(file.path),
|
|
1657
|
-
implStatus: implFile.status,
|
|
1658
|
-
testStatus: file.status,
|
|
1659
|
-
};
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
return null;
|
|
1664
|
-
}
|
|
1665
|
-
/**
|
|
1666
|
-
* Generate description using lightweight file content analysis (JSON, CSS, YAML, MD).
|
|
1667
|
-
* Returns null if no structural info is available.
|
|
1668
|
-
*/
|
|
1669
|
-
static generateFileContentDescription(stagedFiles, diff) {
|
|
1670
|
-
if (stagedFiles.length !== 1)
|
|
1671
|
-
return null;
|
|
1672
|
-
const file = stagedFiles[0];
|
|
1673
|
-
const analysis = (0, fileContentAnalyzer_1.analyzeFileContent)(file.path, diff);
|
|
1674
|
-
if (!analysis || analysis.changedElements.length === 0)
|
|
1675
|
-
return null;
|
|
1676
|
-
const fileName = this.getFileName(file.path);
|
|
1677
|
-
const elements = analysis.changedElements;
|
|
1678
|
-
const elementStr = elements.length <= 2
|
|
1679
|
-
? elements.join(' and ')
|
|
1680
|
-
: `${elements[0]} and ${elements.length - 1} more`;
|
|
1681
|
-
const verb = analysis.changeKind === 'added' ? 'add'
|
|
1682
|
-
: analysis.changeKind === 'removed' ? 'remove'
|
|
1683
|
-
: 'update';
|
|
1684
|
-
return `${verb} ${elementStr} in ${fileName}`;
|
|
1685
|
-
}
|
|
1686
|
-
/**
|
|
1687
|
-
* Generate description using hunk header context (function/class names).
|
|
1688
|
-
* Works for any language that git can identify scope for.
|
|
1689
|
-
* Returns null if no hunk context is available.
|
|
1690
|
-
*/
|
|
1691
|
-
static generateHunkDescription(stagedFiles, diff, commitType) {
|
|
1692
|
-
const hunkNames = this.extractHunkContext(diff);
|
|
1693
|
-
if (hunkNames.length === 0)
|
|
1694
|
-
return null;
|
|
1695
|
-
const verb = commitType === 'feat' ? 'add' :
|
|
1696
|
-
commitType === 'fix' ? 'fix' :
|
|
1697
|
-
commitType === 'refactor' ? 'refactor' :
|
|
1698
|
-
commitType === 'perf' ? 'optimize' : 'update';
|
|
1699
|
-
const nameStr = hunkNames.length <= 2
|
|
1700
|
-
? hunkNames.join(' and ')
|
|
1701
|
-
: `${hunkNames[0]} and ${hunkNames.length - 1} more`;
|
|
1702
|
-
if (stagedFiles.length === 1) {
|
|
1703
|
-
const fileName = this.getFileName(stagedFiles[0].path);
|
|
1704
|
-
return `${verb} ${nameStr} in ${fileName}`;
|
|
1705
|
-
}
|
|
1706
|
-
return `${verb} ${nameStr}`;
|
|
1707
|
-
}
|
|
1708
|
-
/**
|
|
1709
|
-
* Generate a descriptive commit message
|
|
1710
|
-
* Uses AST analysis for specific descriptions when available,
|
|
1711
|
-
* falls back to semantic analysis and regex-based descriptions.
|
|
1712
|
-
*/
|
|
1713
|
-
static generateDescription(filesAffected, fileStatuses, stagedFiles, diff, semanticAnalysis, astResult, commitType) {
|
|
1714
|
-
// Check for comment-only changes (documentation in source files)
|
|
1715
|
-
if (this.isCommentOnlyChange(diff)) {
|
|
1716
|
-
const fileName = stagedFiles.length === 1 ? this.getFileName(stagedFiles[0].path) : null;
|
|
1717
|
-
// Detect type of comments being added
|
|
1718
|
-
const hasJSDoc = /^\+\s*\/\*\*/.test(diff) || /^\+\s*\*\s*@\w+/.test(diff);
|
|
1719
|
-
const hasFunctionComments = /^\+\s*\/\/.*\b(function|method|param|return|arg)/i.test(diff);
|
|
1720
|
-
if (hasJSDoc) {
|
|
1721
|
-
return fileName ? `add JSDoc comments to ${fileName}` : 'add JSDoc documentation';
|
|
1722
|
-
}
|
|
1723
|
-
else if (hasFunctionComments) {
|
|
1724
|
-
return fileName ? `add function documentation to ${fileName}` : 'add function documentation';
|
|
1725
|
-
}
|
|
1726
|
-
else {
|
|
1727
|
-
return fileName ? `add comments to ${fileName}` : 'add code comments';
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
// Check for formatting-only changes (semicolons, whitespace, etc.)
|
|
1731
|
-
if (this.isFormattingOnlyChange(diff)) {
|
|
1732
|
-
return this.generateFormattingDescription(diff, stagedFiles);
|
|
1733
|
-
}
|
|
1734
|
-
// Use AST analysis for specific, human-readable descriptions when available
|
|
1735
|
-
// AST descriptions are preferred because they name exact functions/classes
|
|
1736
|
-
if (astResult && commitType) {
|
|
1737
|
-
const astMaxWeight = astResult.signals.length > 0
|
|
1738
|
-
? Math.max(...astResult.signals.map(s => s.weight), 0)
|
|
1739
|
-
: 0;
|
|
1740
|
-
if (astMaxWeight > 1 || astResult.declarations.renamed.length > 0) {
|
|
1741
|
-
const astDesc = this.generateASTDescription(astResult, commitType);
|
|
1742
|
-
if (astDesc) {
|
|
1743
|
-
return astDesc;
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
// Use semantic analysis for intent-based descriptions when available
|
|
1748
|
-
if (semanticAnalysis && semanticAnalysis.roleChanges.length > 0) {
|
|
1749
|
-
return this.generateSemanticDescription(semanticAnalysis, stagedFiles);
|
|
1750
|
-
}
|
|
1751
|
-
// Detect test + implementation file pairs
|
|
1752
|
-
const pair = this.detectTestImplPair(stagedFiles);
|
|
1753
|
-
if (pair) {
|
|
1754
|
-
const implName = pair.implFile.replace(/\.\w+$/, '');
|
|
1755
|
-
if (pair.implStatus === 'A' && pair.testStatus === 'A') {
|
|
1756
|
-
return `add ${implName} with tests`;
|
|
1757
|
-
}
|
|
1758
|
-
else if (pair.implStatus === 'M' && pair.testStatus === 'A') {
|
|
1759
|
-
return `update ${implName} and add tests`;
|
|
1760
|
-
}
|
|
1761
|
-
else if (pair.implStatus === 'A' && pair.testStatus === 'M') {
|
|
1762
|
-
return `add ${implName} and update tests`;
|
|
1763
|
-
}
|
|
1764
|
-
else {
|
|
1765
|
-
return `update ${implName} and tests`;
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
// Use lightweight file content analysis (JSON keys, CSS selectors, etc.)
|
|
1769
|
-
const fileContentDesc = this.generateFileContentDescription(stagedFiles, diff);
|
|
1770
|
-
if (fileContentDesc) {
|
|
1771
|
-
return fileContentDesc;
|
|
1772
|
-
}
|
|
1773
|
-
// Use hunk header context for function/class names (works for any language)
|
|
1774
|
-
if (commitType) {
|
|
1775
|
-
const hunkDesc = this.generateHunkDescription(stagedFiles, diff, commitType);
|
|
1776
|
-
if (hunkDesc) {
|
|
1777
|
-
return hunkDesc;
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
// Single file changes (fallback)
|
|
1781
|
-
if (stagedFiles.length === 1) {
|
|
1782
|
-
const file = stagedFiles[0];
|
|
1783
|
-
const fileName = this.getFileName(file.path);
|
|
1784
|
-
if (file.status === 'A') {
|
|
1785
|
-
return `add ${fileName}`;
|
|
1786
|
-
}
|
|
1787
|
-
else if (file.status === 'D') {
|
|
1788
|
-
return `remove ${fileName}`;
|
|
1789
|
-
}
|
|
1790
|
-
else if (file.status === 'M') {
|
|
1791
|
-
return `update ${fileName}`;
|
|
1792
|
-
}
|
|
1793
|
-
else if (file.status === 'R') {
|
|
1794
|
-
return `rename ${fileName}`;
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
// Multiple files of the same type
|
|
1798
|
-
if (filesAffected.test > 0 && filesAffected.source === 0) {
|
|
1799
|
-
return `update test files`;
|
|
1800
|
-
}
|
|
1801
|
-
if (filesAffected.docs > 0 && filesAffected.source === 0) {
|
|
1802
|
-
return `update documentation`;
|
|
1803
|
-
}
|
|
1804
|
-
if (filesAffected.config > 0 && filesAffected.source === 0) {
|
|
1805
|
-
return `update configuration`;
|
|
1806
|
-
}
|
|
1807
|
-
// Mixed changes - try to be descriptive
|
|
1808
|
-
const parts = [];
|
|
1809
|
-
if (fileStatuses.added > 0) {
|
|
1810
|
-
parts.push(`add ${fileStatuses.added} file${fileStatuses.added > 1 ? 's' : ''}`);
|
|
1811
|
-
}
|
|
1812
|
-
if (fileStatuses.modified > 0) {
|
|
1813
|
-
if (parts.length === 0) {
|
|
1814
|
-
parts.push(`update ${fileStatuses.modified} file${fileStatuses.modified > 1 ? 's' : ''}`);
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
if (fileStatuses.deleted > 0) {
|
|
1818
|
-
parts.push(`remove ${fileStatuses.deleted} file${fileStatuses.deleted > 1 ? 's' : ''}`);
|
|
1819
|
-
}
|
|
1820
|
-
if (parts.length > 0) {
|
|
1821
|
-
return parts.join(' and ');
|
|
1822
|
-
}
|
|
1823
|
-
// Fallback
|
|
1824
|
-
return `update ${stagedFiles.length} file${stagedFiles.length > 1 ? 's' : ''}`;
|
|
1825
|
-
}
|
|
1826
|
-
/**
|
|
1827
|
-
* Generate description based on semantic analysis
|
|
1828
|
-
* Creates intent-based messages like "update UserProfile validation logic"
|
|
1829
|
-
*/
|
|
1830
|
-
static generateSemanticDescription(semantic, stagedFiles) {
|
|
1831
|
-
const intent = semantic.primaryIntent;
|
|
1832
|
-
const role = semantic.primaryRole;
|
|
1833
|
-
const verb = INTENT_VERBS[intent].present;
|
|
1834
|
-
const roleDesc = ROLE_DESCRIPTIONS[role];
|
|
1835
|
-
const hunkContext = semantic.hunkContext;
|
|
1836
|
-
// Handle renames specially - use "rename X to Y" format
|
|
1837
|
-
if (intent === 'rename' && semantic.renames && semantic.renames.length > 0) {
|
|
1838
|
-
const rename = semantic.renames[0];
|
|
1839
|
-
return `${verb} ${rename.oldName} to ${rename.newName}`;
|
|
1840
|
-
}
|
|
1841
|
-
// If we have specific elements affected, include them
|
|
1842
|
-
const whatChanged = semantic.whatChanged;
|
|
1843
|
-
// For single file with identified elements
|
|
1844
|
-
if (stagedFiles.length === 1 && whatChanged && whatChanged !== 'code') {
|
|
1845
|
-
// Check if whatChanged is a component/function name
|
|
1846
|
-
if (/^[A-Z][a-zA-Z0-9]*$/.test(whatChanged)) {
|
|
1847
|
-
// It's a component name
|
|
1848
|
-
return `${verb} ${whatChanged} ${roleDesc}`;
|
|
1849
|
-
}
|
|
1850
|
-
if (/^[a-z][a-zA-Z0-9]*$/.test(whatChanged)) {
|
|
1851
|
-
// It's a function name
|
|
1852
|
-
return `${verb} ${whatChanged} ${roleDesc}`;
|
|
1853
|
-
}
|
|
1854
|
-
// Multiple elements or descriptive text
|
|
1855
|
-
return `${verb} ${whatChanged}`;
|
|
1856
|
-
}
|
|
1857
|
-
// For multiple files with identified elements
|
|
1858
|
-
if (stagedFiles.length > 1 && whatChanged && whatChanged !== 'code') {
|
|
1859
|
-
// whatChanged contains specific element names (from extractAffectedElements or hunkContext)
|
|
1860
|
-
if (/^[a-zA-Z_$][a-zA-Z0-9_$,\s]*$/.test(whatChanged) && !Object.values(ROLE_DESCRIPTIONS).includes(whatChanged)) {
|
|
1861
|
-
return `${verb} ${whatChanged} ${roleDesc}`;
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
// When whatChanged is generic but hunk context provides specific function/class names
|
|
1865
|
-
if (hunkContext && hunkContext.length > 0) {
|
|
1866
|
-
const contextNames = hunkContext.length <= 2
|
|
1867
|
-
? hunkContext.join(' and ')
|
|
1868
|
-
: `${hunkContext[0]} and ${hunkContext.length - 1} more`;
|
|
1869
|
-
return `${verb} ${contextNames} ${roleDesc}`;
|
|
1870
|
-
}
|
|
1871
|
-
// For multiple files or when we only have role info
|
|
1872
|
-
if (semantic.hasMultipleRoles) {
|
|
1873
|
-
// Changes span multiple concerns
|
|
1874
|
-
const roles = semantic.roleChanges
|
|
1875
|
-
.filter(r => r.significance > 20)
|
|
1876
|
-
.slice(0, 2)
|
|
1877
|
-
.map(r => ROLE_DESCRIPTIONS[r.role]);
|
|
1878
|
-
if (roles.length > 1) {
|
|
1879
|
-
return `${verb} ${roles.join(' and ')}`;
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
// Single role with file context
|
|
1883
|
-
if (stagedFiles.length === 1) {
|
|
1884
|
-
const fileName = this.getFileName(stagedFiles[0].path).replace(/\.\w+$/, '');
|
|
1885
|
-
return `${verb} ${fileName} ${roleDesc}`;
|
|
1886
|
-
}
|
|
1887
|
-
// Multiple files, same role
|
|
1888
|
-
return `${verb} ${roleDesc}`;
|
|
1889
|
-
}
|
|
1890
|
-
/**
|
|
1891
|
-
* Determine scope from file paths
|
|
1892
|
-
*/
|
|
1893
|
-
static determineScope(stagedFiles) {
|
|
1894
|
-
if (stagedFiles.length === 0)
|
|
1895
|
-
return undefined;
|
|
1896
|
-
const config = configService_1.ConfigService.getConfig();
|
|
1897
|
-
const paths = stagedFiles.map((f) => f.path);
|
|
1898
|
-
// Check config-based scope mappings first
|
|
1899
|
-
if (config.scopes && config.scopes.length > 0) {
|
|
1900
|
-
for (const mapping of config.scopes) {
|
|
1901
|
-
const matchingFiles = paths.filter((p) => p.includes(mapping.pattern));
|
|
1902
|
-
if (matchingFiles.length === paths.length) {
|
|
1903
|
-
return mapping.scope;
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
// If most files match a pattern, use that scope
|
|
1907
|
-
for (const mapping of config.scopes) {
|
|
1908
|
-
const matchingFiles = paths.filter((p) => p.includes(mapping.pattern));
|
|
1909
|
-
if (matchingFiles.length > paths.length / 2) {
|
|
1910
|
-
return mapping.scope;
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
// Fallback to default heuristic
|
|
1915
|
-
const firstPath = paths[0];
|
|
1916
|
-
const parts = firstPath.split('/');
|
|
1917
|
-
if (parts.length > 1) {
|
|
1918
|
-
const potentialScope = parts[0];
|
|
1919
|
-
// Common scope names to look for
|
|
1920
|
-
const validScopes = [
|
|
1921
|
-
'api',
|
|
1922
|
-
'ui',
|
|
1923
|
-
'auth',
|
|
1924
|
-
'db',
|
|
1925
|
-
'core',
|
|
1926
|
-
'utils',
|
|
1927
|
-
'components',
|
|
1928
|
-
'services',
|
|
1929
|
-
];
|
|
1930
|
-
if (validScopes.includes(potentialScope.toLowerCase())) {
|
|
1931
|
-
return potentialScope;
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
return undefined;
|
|
1935
|
-
}
|
|
1936
194
|
/**
|
|
1937
195
|
* Extract file name from path
|
|
1938
196
|
*/
|
|
@@ -1940,123 +198,6 @@ class AnalyzerService {
|
|
|
1940
198
|
const parts = path.split('/');
|
|
1941
199
|
return parts[parts.length - 1];
|
|
1942
200
|
}
|
|
1943
|
-
/**
|
|
1944
|
-
* Generate commit body for larger changes
|
|
1945
|
-
* Includes semantic role context when available
|
|
1946
|
-
*/
|
|
1947
|
-
static generateBody(analysis) {
|
|
1948
|
-
const lines = [];
|
|
1949
|
-
const semantic = analysis.semanticAnalysis;
|
|
1950
|
-
// Include semantic context (WHY the change was made) for multi-role changes
|
|
1951
|
-
if (semantic && semantic.hasMultipleRoles) {
|
|
1952
|
-
lines.push('Changes:');
|
|
1953
|
-
for (const roleChange of semantic.roleChanges.filter(r => r.significance > 15).slice(0, 4)) {
|
|
1954
|
-
const elements = roleChange.affectedElements.length > 0
|
|
1955
|
-
? ` (${roleChange.affectedElements.slice(0, 2).join(', ')})`
|
|
1956
|
-
: '';
|
|
1957
|
-
lines.push(`- ${roleChange.summary}${elements}`);
|
|
1958
|
-
}
|
|
1959
|
-
lines.push('');
|
|
1960
|
-
}
|
|
1961
|
-
// Only add file details for truly large changes
|
|
1962
|
-
if (!analysis.isLargeChange && lines.length === 0) {
|
|
1963
|
-
return undefined;
|
|
1964
|
-
}
|
|
1965
|
-
// Add file change details for large changes
|
|
1966
|
-
if (analysis.isLargeChange) {
|
|
1967
|
-
if (lines.length > 0) {
|
|
1968
|
-
lines.push('Files:');
|
|
1969
|
-
}
|
|
1970
|
-
if (analysis.fileChanges.added.length > 0) {
|
|
1971
|
-
lines.push(`- Add ${analysis.fileChanges.added.join(', ')}`);
|
|
1972
|
-
}
|
|
1973
|
-
if (analysis.fileChanges.modified.length > 0) {
|
|
1974
|
-
const files = analysis.fileChanges.modified.slice(0, 5);
|
|
1975
|
-
const suffix = analysis.fileChanges.modified.length > 5
|
|
1976
|
-
? ` and ${analysis.fileChanges.modified.length - 5} more`
|
|
1977
|
-
: '';
|
|
1978
|
-
lines.push(`- Update ${files.join(', ')}${suffix}`);
|
|
1979
|
-
}
|
|
1980
|
-
if (analysis.fileChanges.deleted.length > 0) {
|
|
1981
|
-
lines.push(`- Remove ${analysis.fileChanges.deleted.join(', ')}`);
|
|
1982
|
-
}
|
|
1983
|
-
if (analysis.fileChanges.renamed.length > 0) {
|
|
1984
|
-
lines.push(`- Rename ${analysis.fileChanges.renamed.join(', ')}`);
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
return lines.length > 0 ? lines.join('\n') : undefined;
|
|
1988
|
-
}
|
|
1989
|
-
/**
|
|
1990
|
-
* Apply a template to build the commit message subject line
|
|
1991
|
-
*/
|
|
1992
|
-
static applyTemplate(template, type, scope, description, includeEmoji, isBreaking) {
|
|
1993
|
-
const emoji = includeEmoji ? COMMIT_EMOJIS[type] : '';
|
|
1994
|
-
const breakingIndicator = isBreaking ? '!' : '';
|
|
1995
|
-
let result = template
|
|
1996
|
-
.replace('{emoji}', emoji)
|
|
1997
|
-
.replace('{type}', type + breakingIndicator)
|
|
1998
|
-
.replace('{description}', description);
|
|
1999
|
-
// Handle scope - if no scope, use noScope template or remove scope placeholder
|
|
2000
|
-
if (scope) {
|
|
2001
|
-
result = result.replace('{scope}', scope);
|
|
2002
|
-
}
|
|
2003
|
-
else {
|
|
2004
|
-
// Remove scope and parentheses if no scope
|
|
2005
|
-
result = result.replace('({scope})', '').replace('{scope}', '');
|
|
2006
|
-
}
|
|
2007
|
-
// Clean up extra spaces
|
|
2008
|
-
result = result.replace(/\s+/g, ' ').trim();
|
|
2009
|
-
return result;
|
|
2010
|
-
}
|
|
2011
|
-
/**
|
|
2012
|
-
* Build full commit message string
|
|
2013
|
-
*/
|
|
2014
|
-
static buildFullMessage(type, scope, description, body, includeEmoji, ticketInfo, isBreaking, breakingReasons) {
|
|
2015
|
-
const config = configService_1.ConfigService.getConfig();
|
|
2016
|
-
const includeBreakingFooter = config.breakingChangeDetection?.includeFooter !== false;
|
|
2017
|
-
const templates = config.templates;
|
|
2018
|
-
let full = '';
|
|
2019
|
-
// Use template if available
|
|
2020
|
-
if (templates) {
|
|
2021
|
-
const template = scope
|
|
2022
|
-
? (templates.default || '{emoji} {type}({scope}): {description}')
|
|
2023
|
-
: (templates.noScope || '{emoji} {type}: {description}');
|
|
2024
|
-
full = this.applyTemplate(template, type, scope, description, includeEmoji, isBreaking);
|
|
2025
|
-
}
|
|
2026
|
-
else {
|
|
2027
|
-
// Fallback to original logic
|
|
2028
|
-
if (includeEmoji) {
|
|
2029
|
-
full += `${COMMIT_EMOJIS[type]} `;
|
|
2030
|
-
}
|
|
2031
|
-
full += type;
|
|
2032
|
-
if (scope) {
|
|
2033
|
-
full += `(${scope})`;
|
|
2034
|
-
}
|
|
2035
|
-
// Add breaking change indicator
|
|
2036
|
-
if (isBreaking) {
|
|
2037
|
-
full += '!';
|
|
2038
|
-
}
|
|
2039
|
-
full += `: ${description}`;
|
|
2040
|
-
}
|
|
2041
|
-
if (body) {
|
|
2042
|
-
full += `\n\n${body}`;
|
|
2043
|
-
}
|
|
2044
|
-
// Add BREAKING CHANGE footer if enabled and breaking
|
|
2045
|
-
if (isBreaking && includeBreakingFooter && breakingReasons && breakingReasons.length > 0) {
|
|
2046
|
-
full += '\n\nBREAKING CHANGE: ' + breakingReasons[0];
|
|
2047
|
-
if (breakingReasons.length > 1) {
|
|
2048
|
-
for (let i = 1; i < breakingReasons.length; i++) {
|
|
2049
|
-
full += `\n- ${breakingReasons[i]}`;
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
// Add ticket reference as footer
|
|
2054
|
-
if (ticketInfo) {
|
|
2055
|
-
const prefix = ticketInfo.prefix || 'Refs:';
|
|
2056
|
-
full += `\n\n${prefix} ${ticketInfo.id}`;
|
|
2057
|
-
}
|
|
2058
|
-
return full;
|
|
2059
|
-
}
|
|
2060
201
|
/**
|
|
2061
202
|
* Generate the final commit message
|
|
2062
203
|
*/
|
|
@@ -2068,9 +209,9 @@ class AnalyzerService {
|
|
|
2068
209
|
if (includeEmoji === undefined) {
|
|
2069
210
|
includeEmoji = historyService_1.HistoryService.projectUsesEmojis();
|
|
2070
211
|
}
|
|
2071
|
-
const body =
|
|
212
|
+
const body = (0, messageBuilder_1.generateBody)(analysis);
|
|
2072
213
|
const ticketInfo = historyService_1.HistoryService.detectTicketFromBranch();
|
|
2073
|
-
const full =
|
|
214
|
+
const full = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, analysis.scope, analysis.description, body, includeEmoji, ticketInfo, analysis.isBreakingChange, analysis.breakingChangeReasons);
|
|
2074
215
|
return {
|
|
2075
216
|
type: analysis.commitType,
|
|
2076
217
|
scope: analysis.scope,
|
|
@@ -2078,6 +219,8 @@ class AnalyzerService {
|
|
|
2078
219
|
body,
|
|
2079
220
|
full,
|
|
2080
221
|
isBreaking: analysis.isBreakingChange,
|
|
222
|
+
confidence: analysis.confidence,
|
|
223
|
+
classificationSource: analysis.classificationSource,
|
|
2081
224
|
};
|
|
2082
225
|
}
|
|
2083
226
|
/**
|
|
@@ -2092,7 +235,7 @@ class AnalyzerService {
|
|
|
2092
235
|
includeEmoji = historyService_1.HistoryService.projectUsesEmojis();
|
|
2093
236
|
}
|
|
2094
237
|
const suggestions = [];
|
|
2095
|
-
const body =
|
|
238
|
+
const body = (0, messageBuilder_1.generateBody)(analysis);
|
|
2096
239
|
const ticketInfo = historyService_1.HistoryService.detectTicketFromBranch();
|
|
2097
240
|
const { isBreakingChange, breakingChangeReasons } = analysis;
|
|
2098
241
|
// Try to get a better scope from history if none detected
|
|
@@ -2103,7 +246,7 @@ class AnalyzerService {
|
|
|
2103
246
|
scope = historyService_1.HistoryService.getSuggestedScope(filePaths);
|
|
2104
247
|
}
|
|
2105
248
|
// Suggestion 1: Default (with scope if detected, with ticket, with breaking change)
|
|
2106
|
-
const defaultFull =
|
|
249
|
+
const defaultFull = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, scope, analysis.description, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
|
|
2107
250
|
suggestions.push({
|
|
2108
251
|
id: 1,
|
|
2109
252
|
label: isBreakingChange ? 'Breaking Change' : 'Recommended',
|
|
@@ -2114,11 +257,13 @@ class AnalyzerService {
|
|
|
2114
257
|
body,
|
|
2115
258
|
full: defaultFull,
|
|
2116
259
|
isBreaking: isBreakingChange,
|
|
260
|
+
confidence: analysis.confidence,
|
|
261
|
+
classificationSource: analysis.classificationSource,
|
|
2117
262
|
},
|
|
2118
263
|
});
|
|
2119
264
|
// Suggestion 2: Without scope (more concise)
|
|
2120
265
|
if (scope) {
|
|
2121
|
-
const noScopeFull =
|
|
266
|
+
const noScopeFull = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, undefined, analysis.description, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
|
|
2122
267
|
suggestions.push({
|
|
2123
268
|
id: 2,
|
|
2124
269
|
label: 'Concise',
|
|
@@ -2129,13 +274,15 @@ class AnalyzerService {
|
|
|
2129
274
|
body,
|
|
2130
275
|
full: noScopeFull,
|
|
2131
276
|
isBreaking: isBreakingChange,
|
|
277
|
+
confidence: analysis.confidence,
|
|
278
|
+
classificationSource: analysis.classificationSource,
|
|
2132
279
|
},
|
|
2133
280
|
});
|
|
2134
281
|
}
|
|
2135
282
|
// Suggestion 3: Alternative description style
|
|
2136
|
-
const altDescription =
|
|
283
|
+
const altDescription = (0, descriptionGenerator_1.generateAlternativeDescription)(analysis, gitService_1.GitService.getDiff());
|
|
2137
284
|
if (altDescription && altDescription !== analysis.description) {
|
|
2138
|
-
const altFull =
|
|
285
|
+
const altFull = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, scope, altDescription, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
|
|
2139
286
|
suggestions.push({
|
|
2140
287
|
id: suggestions.length + 1,
|
|
2141
288
|
label: 'Detailed',
|
|
@@ -2146,12 +293,14 @@ class AnalyzerService {
|
|
|
2146
293
|
body,
|
|
2147
294
|
full: altFull,
|
|
2148
295
|
isBreaking: isBreakingChange,
|
|
296
|
+
confidence: analysis.confidence,
|
|
297
|
+
classificationSource: analysis.classificationSource,
|
|
2149
298
|
},
|
|
2150
299
|
});
|
|
2151
300
|
}
|
|
2152
301
|
// Suggestion 4: Without body (compact) - only if body exists
|
|
2153
302
|
if (body) {
|
|
2154
|
-
const compactFull =
|
|
303
|
+
const compactFull = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, scope, analysis.description, undefined, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
|
|
2155
304
|
suggestions.push({
|
|
2156
305
|
id: suggestions.length + 1,
|
|
2157
306
|
label: 'Compact',
|
|
@@ -2162,12 +311,14 @@ class AnalyzerService {
|
|
|
2162
311
|
body: undefined,
|
|
2163
312
|
full: compactFull,
|
|
2164
313
|
isBreaking: isBreakingChange,
|
|
314
|
+
confidence: analysis.confidence,
|
|
315
|
+
classificationSource: analysis.classificationSource,
|
|
2165
316
|
},
|
|
2166
317
|
});
|
|
2167
318
|
}
|
|
2168
319
|
// Suggestion 5: Without ticket reference (if ticket was detected)
|
|
2169
320
|
if (ticketInfo) {
|
|
2170
|
-
const noTicketFull =
|
|
321
|
+
const noTicketFull = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, scope, analysis.description, body, includeEmoji, null, isBreakingChange, breakingChangeReasons);
|
|
2171
322
|
suggestions.push({
|
|
2172
323
|
id: suggestions.length + 1,
|
|
2173
324
|
label: 'No Ticket',
|
|
@@ -2178,12 +329,14 @@ class AnalyzerService {
|
|
|
2178
329
|
body,
|
|
2179
330
|
full: noTicketFull,
|
|
2180
331
|
isBreaking: isBreakingChange,
|
|
332
|
+
confidence: analysis.confidence,
|
|
333
|
+
classificationSource: analysis.classificationSource,
|
|
2181
334
|
},
|
|
2182
335
|
});
|
|
2183
336
|
}
|
|
2184
337
|
// Suggestion 6: Without breaking change indicator (if breaking change detected)
|
|
2185
338
|
if (isBreakingChange) {
|
|
2186
|
-
const noBreakingFull =
|
|
339
|
+
const noBreakingFull = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, scope, analysis.description, body, includeEmoji, ticketInfo, false, []);
|
|
2187
340
|
suggestions.push({
|
|
2188
341
|
id: suggestions.length + 1,
|
|
2189
342
|
label: 'No Breaking Flag',
|
|
@@ -2194,6 +347,8 @@ class AnalyzerService {
|
|
|
2194
347
|
body,
|
|
2195
348
|
full: noBreakingFull,
|
|
2196
349
|
isBreaking: false,
|
|
350
|
+
confidence: analysis.confidence,
|
|
351
|
+
classificationSource: analysis.classificationSource,
|
|
2197
352
|
},
|
|
2198
353
|
});
|
|
2199
354
|
}
|
|
@@ -2217,7 +372,7 @@ class AnalyzerService {
|
|
|
2217
372
|
if (includeEmoji === undefined) {
|
|
2218
373
|
includeEmoji = historyService_1.HistoryService.projectUsesEmojis();
|
|
2219
374
|
}
|
|
2220
|
-
const body =
|
|
375
|
+
const body = (0, messageBuilder_1.generateBody)(analysis);
|
|
2221
376
|
const ticketInfo = historyService_1.HistoryService.detectTicketFromBranch();
|
|
2222
377
|
const { isBreakingChange, breakingChangeReasons } = analysis;
|
|
2223
378
|
// Get scope
|
|
@@ -2231,7 +386,7 @@ class AnalyzerService {
|
|
|
2231
386
|
const aiResponse = await aiService_1.AIService.generateDescription(analysis, diff);
|
|
2232
387
|
if (aiResponse && aiResponse.description) {
|
|
2233
388
|
const aiDescription = aiResponse.description;
|
|
2234
|
-
const aiFull =
|
|
389
|
+
const aiFull = (0, messageBuilder_1.buildFullMessage)(analysis.commitType, scope, aiDescription, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
|
|
2235
390
|
// Insert AI suggestion at the beginning
|
|
2236
391
|
suggestions.unshift({
|
|
2237
392
|
id: 0,
|
|
@@ -2257,96 +412,6 @@ class AnalyzerService {
|
|
|
2257
412
|
}
|
|
2258
413
|
return suggestions;
|
|
2259
414
|
}
|
|
2260
|
-
/**
|
|
2261
|
-
* Generate an alternative description style
|
|
2262
|
-
* Uses AST data when available, otherwise falls back to regex extraction.
|
|
2263
|
-
*/
|
|
2264
|
-
static generateAlternativeDescription(analysis, diff) {
|
|
2265
|
-
const { fileChanges, filesAffected } = analysis;
|
|
2266
|
-
const totalFiles = fileChanges.added.length + fileChanges.modified.length +
|
|
2267
|
-
fileChanges.deleted.length + fileChanges.renamed.length;
|
|
2268
|
-
// Use AST data to build a detailed alternative with file context
|
|
2269
|
-
if (analysis.astResult && analysis.astResult.affectedElements.length > 0) {
|
|
2270
|
-
const elements = analysis.astResult.affectedElements;
|
|
2271
|
-
const verb = analysis.commitType === 'feat' ? 'add' :
|
|
2272
|
-
analysis.commitType === 'fix' ? 'fix' :
|
|
2273
|
-
analysis.commitType === 'refactor' ? 'refactor' : 'update';
|
|
2274
|
-
// For single file, include file name for extra context
|
|
2275
|
-
if (totalFiles === 1) {
|
|
2276
|
-
const fileName = fileChanges.added[0] || fileChanges.modified[0] || fileChanges.deleted[0] || fileChanges.renamed[0];
|
|
2277
|
-
if (elements.length === 1) {
|
|
2278
|
-
return `${verb} ${elements[0]} in ${fileName}`;
|
|
2279
|
-
}
|
|
2280
|
-
if (elements.length === 2) {
|
|
2281
|
-
return `${verb} ${elements[0]} and ${elements[1]} in ${fileName}`;
|
|
2282
|
-
}
|
|
2283
|
-
return `${verb} ${elements[0]}, ${elements[1]} and ${elements.length - 2} more in ${fileName}`;
|
|
2284
|
-
}
|
|
2285
|
-
// For multiple files, list elements across files
|
|
2286
|
-
if (elements.length === 1) {
|
|
2287
|
-
return `${verb} ${elements[0]}`;
|
|
2288
|
-
}
|
|
2289
|
-
if (elements.length === 2) {
|
|
2290
|
-
return `${verb} ${elements[0]} and ${elements[1]}`;
|
|
2291
|
-
}
|
|
2292
|
-
return `${verb} ${elements[0]}, ${elements[1]} and ${elements.length - 2} more`;
|
|
2293
|
-
}
|
|
2294
|
-
// For single file, provide more detail using extracted elements when available
|
|
2295
|
-
if (totalFiles === 1) {
|
|
2296
|
-
const elements = this.extractAffectedElements(diff);
|
|
2297
|
-
const fileName = fileChanges.added[0] || fileChanges.modified[0] || fileChanges.deleted[0] || fileChanges.renamed[0];
|
|
2298
|
-
if (elements.length > 0) {
|
|
2299
|
-
const verb = analysis.commitType === 'feat' ? 'add' :
|
|
2300
|
-
analysis.commitType === 'fix' ? 'fix' :
|
|
2301
|
-
analysis.commitType === 'refactor' ? 'refactor' : 'update';
|
|
2302
|
-
if (elements.length === 1) {
|
|
2303
|
-
return `${verb} ${elements[0]} in ${fileName}`;
|
|
2304
|
-
}
|
|
2305
|
-
if (elements.length === 2) {
|
|
2306
|
-
return `${verb} ${elements[0]} and ${elements[1]} in ${fileName}`;
|
|
2307
|
-
}
|
|
2308
|
-
return `${verb} ${elements[0]}, ${elements[1]} and ${elements.length - 2} more in ${fileName}`;
|
|
2309
|
-
}
|
|
2310
|
-
// Fall back to hunk context for modified files where declarations aren't in the diff
|
|
2311
|
-
const hunkNames = this.extractHunkContext(diff);
|
|
2312
|
-
if (hunkNames.length > 0) {
|
|
2313
|
-
const verb = analysis.commitType === 'feat' ? 'add' :
|
|
2314
|
-
analysis.commitType === 'fix' ? 'fix' :
|
|
2315
|
-
analysis.commitType === 'refactor' ? 'refactor' : 'update';
|
|
2316
|
-
if (hunkNames.length === 1) {
|
|
2317
|
-
return `${verb} ${hunkNames[0]} in ${fileName}`;
|
|
2318
|
-
}
|
|
2319
|
-
if (hunkNames.length === 2) {
|
|
2320
|
-
return `${verb} ${hunkNames[0]} and ${hunkNames[1]} in ${fileName}`;
|
|
2321
|
-
}
|
|
2322
|
-
return `${verb} ${hunkNames[0]}, ${hunkNames[1]} and ${hunkNames.length - 2} more in ${fileName}`;
|
|
2323
|
-
}
|
|
2324
|
-
if (fileChanges.added.length === 1) {
|
|
2325
|
-
return `implement ${fileChanges.added[0]}`;
|
|
2326
|
-
}
|
|
2327
|
-
if (fileChanges.modified.length === 1) {
|
|
2328
|
-
return `update ${fileChanges.modified[0]}`;
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
|
-
// For multiple files, be more descriptive about categories
|
|
2332
|
-
const parts = [];
|
|
2333
|
-
if (filesAffected.source > 0) {
|
|
2334
|
-
parts.push(`${filesAffected.source} source file${filesAffected.source > 1 ? 's' : ''}`);
|
|
2335
|
-
}
|
|
2336
|
-
if (filesAffected.test > 0) {
|
|
2337
|
-
parts.push(`${filesAffected.test} test${filesAffected.test > 1 ? 's' : ''}`);
|
|
2338
|
-
}
|
|
2339
|
-
if (filesAffected.docs > 0) {
|
|
2340
|
-
parts.push(`${filesAffected.docs} doc${filesAffected.docs > 1 ? 's' : ''}`);
|
|
2341
|
-
}
|
|
2342
|
-
if (filesAffected.config > 0) {
|
|
2343
|
-
parts.push(`${filesAffected.config} config${filesAffected.config > 1 ? 's' : ''}`);
|
|
2344
|
-
}
|
|
2345
|
-
if (parts.length > 0) {
|
|
2346
|
-
return `update ${parts.join(', ')}`;
|
|
2347
|
-
}
|
|
2348
|
-
return analysis.description;
|
|
2349
|
-
}
|
|
2350
415
|
}
|
|
2351
416
|
exports.AnalyzerService = AnalyzerService;
|
|
2352
417
|
//# sourceMappingURL=analyzerService.js.map
|