@sun-asterisk/sunlint 1.3.35 → 1.3.36
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/core/cli-action-handler.js +10 -0
- package/core/cli-program.js +9 -3
- package/core/impact-integration.js +294 -176
- package/package.json +1 -1
|
@@ -591,7 +591,9 @@ class CliActionHandler {
|
|
|
591
591
|
const integration = new ImpactIntegration({
|
|
592
592
|
...this.options,
|
|
593
593
|
impactBase: this.options.impactBase || 'HEAD~1',
|
|
594
|
+
impactHead: this.options.impactHead,
|
|
594
595
|
impactReport: this.options.impactReport,
|
|
596
|
+
impactMaxDepth: this.options.impactMaxDepth ? parseInt(this.options.impactMaxDepth, 10) : 3,
|
|
595
597
|
});
|
|
596
598
|
|
|
597
599
|
const projectPath = this.getProjectPath();
|
|
@@ -635,6 +637,14 @@ class CliActionHandler {
|
|
|
635
637
|
}
|
|
636
638
|
}
|
|
637
639
|
|
|
640
|
+
// Save JSON report if requested
|
|
641
|
+
if (this.options.impactJson) {
|
|
642
|
+
const jsonPath = await integration.saveJsonReport(results, this.options.impactJson);
|
|
643
|
+
if (!this.options.quiet) {
|
|
644
|
+
console.log(chalk.green(`📊 Impact JSON saved: ${jsonPath}`));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
638
648
|
return results;
|
|
639
649
|
} catch (error) {
|
|
640
650
|
console.error(chalk.yellow(`⚠️ Impact analysis failed: ${error.message}`));
|
package/core/cli-program.js
CHANGED
|
@@ -71,7 +71,10 @@ function createCliProgram() {
|
|
|
71
71
|
.option('--arch-report', 'Generate architecture markdown report')
|
|
72
72
|
.option('--impact', 'Enable impact analysis (analyze code changes)')
|
|
73
73
|
.option('--impact-base <ref>', 'Base git ref for impact analysis (default: HEAD~1)')
|
|
74
|
+
.option('--impact-head <ref>', 'Head git ref for impact analysis (default: working directory)')
|
|
74
75
|
.option('--impact-report <file>', 'Output impact report file (default: impact-report.md)')
|
|
76
|
+
.option('--impact-json <file>', 'Output impact analysis as JSON file')
|
|
77
|
+
.option('--impact-max-depth <n>', 'Maximum call graph depth for impact analysis (default: 3)')
|
|
75
78
|
.option('--engine <engine>', 'Analysis engine: auto, eslint, heuristic', 'auto')
|
|
76
79
|
.option('--eslint-integration', 'Merge with existing ESLint config')
|
|
77
80
|
.option('--no-eslint-integration', 'Disable ESLint integration');
|
|
@@ -123,9 +126,12 @@ Architecture:
|
|
|
123
126
|
sunlint --architecture --arch-report # Generate MD report
|
|
124
127
|
|
|
125
128
|
Impact Analysis:
|
|
126
|
-
sunlint --impact --input=src
|
|
127
|
-
sunlint --impact --impact-base=origin/main
|
|
128
|
-
sunlint --impact --impact-
|
|
129
|
+
sunlint --impact --input=src # Analyze code changes
|
|
130
|
+
sunlint --impact --impact-base=origin/main # Compare with main
|
|
131
|
+
sunlint --impact --impact-head=HEAD # Specify head ref
|
|
132
|
+
sunlint --impact --impact-report=report.md # Markdown report
|
|
133
|
+
sunlint --impact --impact-json=report.json # JSON report
|
|
134
|
+
sunlint --impact --impact-max-depth=5 # Call graph depth
|
|
129
135
|
|
|
130
136
|
Performance:
|
|
131
137
|
sunlint --all --performance=fast --input=. # Quick scan
|
|
@@ -12,7 +12,10 @@ const { execSync } = require('child_process');
|
|
|
12
12
|
class ImpactIntegration {
|
|
13
13
|
constructor(options = {}) {
|
|
14
14
|
this.options = options;
|
|
15
|
-
this.
|
|
15
|
+
this.impactModulePath = null;
|
|
16
|
+
this.ChangeDetector = null;
|
|
17
|
+
this.ImpactAnalyzer = null;
|
|
18
|
+
this.ReportGenerator = null;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/**
|
|
@@ -20,26 +23,20 @@ class ImpactIntegration {
|
|
|
20
23
|
* Note: impact-analyzer is ESM, so we use dynamic import
|
|
21
24
|
*/
|
|
22
25
|
async loadImpactModule() {
|
|
23
|
-
if (this.
|
|
24
|
-
return
|
|
26
|
+
if (this.ChangeDetector && this.ImpactAnalyzer) {
|
|
27
|
+
return;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
// Priority 1: Try bundled version (engines/impact)
|
|
28
31
|
const bundledPath = path.join(__dirname, '..', 'engines', 'impact');
|
|
29
32
|
if (fs.existsSync(path.join(bundledPath, 'index.js'))) {
|
|
30
33
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
bundledPath,
|
|
36
|
-
type: 'bundled'
|
|
37
|
-
};
|
|
38
|
-
if (this.options.verbose) {
|
|
39
|
-
console.log(chalk.gray('📦 Found bundled impact-analyzer'));
|
|
40
|
-
}
|
|
41
|
-
return this.impactModule;
|
|
34
|
+
this.impactModulePath = bundledPath;
|
|
35
|
+
await this.loadModules(bundledPath);
|
|
36
|
+
if (this.options.verbose) {
|
|
37
|
+
console.log(chalk.gray('📦 Loaded bundled impact-analyzer'));
|
|
42
38
|
}
|
|
39
|
+
return;
|
|
43
40
|
} catch (error) {
|
|
44
41
|
if (this.options.verbose) {
|
|
45
42
|
console.log(chalk.yellow(`⚠️ Failed to load bundled: ${error.message}`));
|
|
@@ -55,22 +52,44 @@ class ImpactIntegration {
|
|
|
55
52
|
|
|
56
53
|
for (const devPath of devPaths) {
|
|
57
54
|
if (fs.existsSync(path.join(devPath, 'index.js'))) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
try {
|
|
56
|
+
this.impactModulePath = devPath;
|
|
57
|
+
await this.loadModules(devPath);
|
|
58
|
+
if (this.options.verbose) {
|
|
59
|
+
console.log(chalk.gray(`📦 Loaded impact-analyzer from: ${devPath}`));
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (this.options.verbose) {
|
|
64
|
+
console.log(chalk.yellow(`⚠️ Failed to load from ${devPath}: ${error.message}`));
|
|
65
|
+
}
|
|
64
66
|
}
|
|
65
|
-
return this.impactModule;
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
throw new Error(
|
|
70
|
-
'Impact analyzer module not found.
|
|
71
|
+
'Impact analyzer module not found. Ensure impact-analyzer exists in the monorepo.'
|
|
71
72
|
);
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Load ESM modules using dynamic import
|
|
77
|
+
*/
|
|
78
|
+
async loadModules(basePath) {
|
|
79
|
+
const changeDetectorPath = path.join(basePath, 'core', 'change-detector.js');
|
|
80
|
+
const impactAnalyzerPath = path.join(basePath, 'core', 'impact-analyzer.js');
|
|
81
|
+
const reportGeneratorPath = path.join(basePath, 'core', 'report-generator.js');
|
|
82
|
+
|
|
83
|
+
// Use dynamic import for ESM modules
|
|
84
|
+
const changeDetectorModule = await import(`file://${changeDetectorPath}`);
|
|
85
|
+
const impactAnalyzerModule = await import(`file://${impactAnalyzerPath}`);
|
|
86
|
+
const reportGeneratorModule = await import(`file://${reportGeneratorPath}`);
|
|
87
|
+
|
|
88
|
+
this.ChangeDetector = changeDetectorModule.ChangeDetector;
|
|
89
|
+
this.ImpactAnalyzer = impactAnalyzerModule.ImpactAnalyzer;
|
|
90
|
+
this.ReportGenerator = reportGeneratorModule.ReportGenerator;
|
|
91
|
+
}
|
|
92
|
+
|
|
74
93
|
/**
|
|
75
94
|
* Check if git repository
|
|
76
95
|
*/
|
|
@@ -102,18 +121,20 @@ class ImpactIntegration {
|
|
|
102
121
|
}
|
|
103
122
|
|
|
104
123
|
/**
|
|
105
|
-
*
|
|
124
|
+
* Build config object for impact analyzer
|
|
106
125
|
*/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
buildConfig(projectPath) {
|
|
127
|
+
return {
|
|
128
|
+
sourceDir: projectPath,
|
|
129
|
+
baseRef: this.options.impactBase || 'HEAD~1',
|
|
130
|
+
headRef: this.options.impactHead || '', // Empty means current working directory
|
|
131
|
+
excludePaths: this.options.exclude
|
|
132
|
+
? this.options.exclude.split(',').map(p => p.trim())
|
|
133
|
+
: ['node_modules', 'dist', 'build', 'specs', 'coverage'],
|
|
134
|
+
maxDepth: this.options.impactMaxDepth || 3,
|
|
135
|
+
includeTests: this.options.impactIncludeTests || false,
|
|
136
|
+
verbose: this.options.verbose || false,
|
|
137
|
+
};
|
|
117
138
|
}
|
|
118
139
|
|
|
119
140
|
/**
|
|
@@ -125,7 +146,7 @@ class ImpactIntegration {
|
|
|
125
146
|
await this.loadImpactModule();
|
|
126
147
|
|
|
127
148
|
const absolutePath = path.resolve(projectPath);
|
|
128
|
-
const
|
|
149
|
+
const config = this.buildConfig(absolutePath);
|
|
129
150
|
|
|
130
151
|
// Validation
|
|
131
152
|
if (!fs.existsSync(absolutePath)) {
|
|
@@ -136,64 +157,199 @@ class ImpactIntegration {
|
|
|
136
157
|
throw new Error(`Not a git repository: ${absolutePath}`);
|
|
137
158
|
}
|
|
138
159
|
|
|
139
|
-
if (!this.refExists(baseRef, absolutePath)) {
|
|
140
|
-
throw new Error(`Git ref does not exist: ${baseRef}`);
|
|
160
|
+
if (!this.refExists(config.baseRef, absolutePath)) {
|
|
161
|
+
throw new Error(`Git ref does not exist: ${config.baseRef}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (config.headRef && !this.refExists(config.headRef, absolutePath)) {
|
|
165
|
+
throw new Error(`Head git ref does not exist: ${config.headRef}`);
|
|
141
166
|
}
|
|
142
167
|
|
|
168
|
+
// Ensure tsconfig.json exists (required by ts-morph)
|
|
169
|
+
await this.ensureTsConfig(absolutePath);
|
|
170
|
+
|
|
143
171
|
if (this.options.verbose) {
|
|
144
|
-
console.log(chalk.blue(`\n🔍 Impact Analysis`));
|
|
172
|
+
console.log(chalk.blue(`\n🔍 Impact Analysis Configuration`));
|
|
145
173
|
console.log(chalk.gray(` Path: ${absolutePath}`));
|
|
146
|
-
console.log(chalk.gray(` Base: ${baseRef}`));
|
|
174
|
+
console.log(chalk.gray(` Base: ${config.baseRef}`));
|
|
175
|
+
console.log(chalk.gray(` Head: ${config.headRef || 'working directory'}`));
|
|
176
|
+
console.log(chalk.gray(` Max Depth: ${config.maxDepth}`));
|
|
177
|
+
console.log(chalk.gray(` Include Tests: ${config.includeTests}`));
|
|
147
178
|
}
|
|
148
179
|
|
|
149
|
-
|
|
150
|
-
|
|
180
|
+
try {
|
|
181
|
+
// ============================================
|
|
182
|
+
// Step 1: Detect Changes
|
|
183
|
+
// ============================================
|
|
184
|
+
if (this.options.verbose) {
|
|
185
|
+
console.log(chalk.blue('\n📝 Step 1: Detecting changes...'));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const detector = new this.ChangeDetector(config);
|
|
189
|
+
const changedFiles = detector.detectChangedFiles();
|
|
190
|
+
|
|
191
|
+
if (this.options.verbose) {
|
|
192
|
+
console.log(chalk.gray(` ✓ Found ${changedFiles.length} changed files`));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const changedSymbols = detector.detectChangedSymbols(changedFiles);
|
|
196
|
+
|
|
197
|
+
if (this.options.verbose) {
|
|
198
|
+
console.log(chalk.gray(` ✓ Found ${changedSymbols.length} changed symbols`));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const changes = {
|
|
202
|
+
changedFiles,
|
|
203
|
+
changedSymbols,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// ============================================
|
|
207
|
+
// Step 2: Analyze Impact
|
|
208
|
+
// ============================================
|
|
209
|
+
if (this.options.verbose) {
|
|
210
|
+
console.log(chalk.blue('\n🔍 Step 2: Analyzing impact...'));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const analyzer = new this.ImpactAnalyzer(config);
|
|
214
|
+
|
|
215
|
+
// Initialize method-level call graph for precise tracking
|
|
216
|
+
await analyzer.initializeMethodCallGraph();
|
|
217
|
+
|
|
218
|
+
const impact = await analyzer.analyzeImpact(changes);
|
|
219
|
+
|
|
220
|
+
if (this.options.verbose) {
|
|
221
|
+
console.log(chalk.gray(` ✓ Impact score: ${impact.impactScore}`));
|
|
222
|
+
console.log(chalk.gray(` ✓ Severity: ${impact.severity.toUpperCase()}`));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ============================================
|
|
226
|
+
// Step 3: Build Result
|
|
227
|
+
// ============================================
|
|
228
|
+
const result = this.buildResult(changes, impact, config);
|
|
229
|
+
|
|
230
|
+
// Generate markdown report if requested
|
|
231
|
+
if (this.options.impactReport) {
|
|
232
|
+
const reporter = new this.ReportGenerator();
|
|
233
|
+
result.markdownReport = reporter.generateMarkdownReport(changes, impact);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return result;
|
|
151
237
|
|
|
152
|
-
|
|
153
|
-
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// Re-throw with more context
|
|
240
|
+
throw new Error(`Impact analysis failed: ${error.message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Ensure tsconfig.json exists (required by ts-morph)
|
|
246
|
+
*/
|
|
247
|
+
async ensureTsConfig(projectPath) {
|
|
248
|
+
const tsconfigPath = path.join(projectPath, '..', 'tsconfig.json');
|
|
249
|
+
|
|
250
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
251
|
+
return; // Already exists
|
|
154
252
|
}
|
|
155
253
|
|
|
156
|
-
//
|
|
157
|
-
const
|
|
254
|
+
// Create minimal tsconfig.json for analysis
|
|
255
|
+
const minimalTsConfig = {
|
|
256
|
+
compilerOptions: {
|
|
257
|
+
target: "ES2020",
|
|
258
|
+
module: "commonjs",
|
|
259
|
+
lib: ["ES2020"],
|
|
260
|
+
moduleResolution: "node",
|
|
261
|
+
esModuleInterop: true,
|
|
262
|
+
skipLibCheck: true,
|
|
263
|
+
strict: false,
|
|
264
|
+
resolveJsonModule: true,
|
|
265
|
+
allowSyntheticDefaultImports: true,
|
|
266
|
+
},
|
|
267
|
+
include: ["**/*"],
|
|
268
|
+
exclude: ["node_modules", "dist", "build"]
|
|
269
|
+
};
|
|
158
270
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
271
|
+
if (this.options.verbose) {
|
|
272
|
+
console.log(chalk.gray(` Creating minimal tsconfig.json at: ${tsconfigPath}`));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fs.writeFileSync(tsconfigPath, JSON.stringify(minimalTsConfig, null, 2));
|
|
276
|
+
|
|
277
|
+
// Mark for cleanup
|
|
278
|
+
this.tempTsConfig = tsconfigPath;
|
|
279
|
+
}
|
|
162
280
|
|
|
163
|
-
|
|
164
|
-
|
|
281
|
+
/**
|
|
282
|
+
* Clean up temporary files
|
|
283
|
+
*/
|
|
284
|
+
cleanup() {
|
|
285
|
+
if (this.tempTsConfig && fs.existsSync(this.tempTsConfig)) {
|
|
286
|
+
try {
|
|
287
|
+
fs.unlinkSync(this.tempTsConfig);
|
|
288
|
+
if (this.options.verbose) {
|
|
289
|
+
console.log(chalk.gray(` Cleaned up temporary tsconfig.json`));
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
// Ignore cleanup errors
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Build comprehensive result from impact analysis
|
|
299
|
+
*/
|
|
300
|
+
buildResult(changes, impact, config) {
|
|
301
|
+
// Extract data from impact analysis
|
|
302
|
+
const { affectedEndpoints = [], databaseImpact = {}, logicImpact = {} } = impact;
|
|
303
|
+
|
|
304
|
+
// Calculate category counts from file categorization
|
|
305
|
+
const fileCategories = this.categorizeFiles(changes.changedFiles);
|
|
306
|
+
|
|
307
|
+
return {
|
|
165
308
|
summary: {
|
|
166
|
-
baseRef,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
309
|
+
baseRef: config.baseRef,
|
|
310
|
+
headRef: config.headRef || 'working directory',
|
|
311
|
+
totalChanges: changes.changedFiles.length,
|
|
312
|
+
changedSymbols: changes.changedSymbols.length,
|
|
313
|
+
impactScore: impact.impactScore || 0,
|
|
314
|
+
severity: impact.severity || 'none',
|
|
170
315
|
categories: {
|
|
171
|
-
|
|
172
|
-
database:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
316
|
+
endpoints: affectedEndpoints.length,
|
|
317
|
+
database: databaseImpact.migrations?.length || 0,
|
|
318
|
+
logic: logicImpact.directCallers?.length || 0,
|
|
319
|
+
files: changes.changedFiles.length,
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
changes: {
|
|
323
|
+
changedFiles: changes.changedFiles,
|
|
324
|
+
changedSymbols: changes.changedSymbols,
|
|
325
|
+
},
|
|
326
|
+
impact: {
|
|
327
|
+
affectedEndpoints: affectedEndpoints.map(ep => ({
|
|
328
|
+
path: ep.path,
|
|
329
|
+
method: ep.method,
|
|
330
|
+
handler: ep.handler,
|
|
331
|
+
impactReason: ep.impactReason || 'Direct or indirect change',
|
|
332
|
+
})),
|
|
333
|
+
databaseImpact: {
|
|
334
|
+
migrations: databaseImpact.migrations || [],
|
|
335
|
+
schemaChanges: databaseImpact.schemaChanges || [],
|
|
336
|
+
riskLevel: databaseImpact.riskLevel || 'none',
|
|
337
|
+
},
|
|
338
|
+
logicImpact: {
|
|
339
|
+
directCallers: logicImpact.directCallers || [],
|
|
340
|
+
indirectCallers: logicImpact.indirectCallers || [],
|
|
341
|
+
riskLevel: logicImpact.riskLevel || 'none',
|
|
178
342
|
}
|
|
179
343
|
},
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
violations: this.generateViolations(categorizedChanges, impactScore),
|
|
344
|
+
violations: this.generateViolationsFromImpact(impact),
|
|
345
|
+
fileCategories,
|
|
183
346
|
};
|
|
184
|
-
|
|
185
|
-
// Generate markdown report if requested
|
|
186
|
-
if (this.options.impactReport) {
|
|
187
|
-
result.markdownReport = this.generateMarkdownReport(result);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return result;
|
|
191
347
|
}
|
|
192
348
|
|
|
193
349
|
/**
|
|
194
|
-
* Categorize changed files
|
|
350
|
+
* Categorize changed files for reporting
|
|
195
351
|
*/
|
|
196
|
-
|
|
352
|
+
categorizeFiles(changedFiles) {
|
|
197
353
|
const categories = {
|
|
198
354
|
api: [],
|
|
199
355
|
database: [],
|
|
@@ -213,19 +369,20 @@ class ImpactIntegration {
|
|
|
213
369
|
config: [/config/i, /\.json$/i, /\.ya?ml$/i, /\.env/i, /dockerfile/i],
|
|
214
370
|
};
|
|
215
371
|
|
|
216
|
-
for (const file of
|
|
372
|
+
for (const file of changedFiles) {
|
|
373
|
+
const filePath = file.path || file;
|
|
217
374
|
let categorized = false;
|
|
218
375
|
|
|
219
376
|
for (const [category, regexes] of Object.entries(patterns)) {
|
|
220
|
-
if (regexes.some(regex => regex.test(
|
|
221
|
-
categories[category].push(
|
|
377
|
+
if (regexes.some(regex => regex.test(filePath))) {
|
|
378
|
+
categories[category].push(filePath);
|
|
222
379
|
categorized = true;
|
|
223
380
|
break;
|
|
224
381
|
}
|
|
225
382
|
}
|
|
226
383
|
|
|
227
384
|
if (!categorized) {
|
|
228
|
-
categories.other.push(
|
|
385
|
+
categories.other.push(filePath);
|
|
229
386
|
}
|
|
230
387
|
}
|
|
231
388
|
|
|
@@ -233,146 +390,97 @@ class ImpactIntegration {
|
|
|
233
390
|
}
|
|
234
391
|
|
|
235
392
|
/**
|
|
236
|
-
*
|
|
237
|
-
*/
|
|
238
|
-
calculateImpactScore(categories) {
|
|
239
|
-
const weights = {
|
|
240
|
-
api: 30,
|
|
241
|
-
database: 35,
|
|
242
|
-
core: 25,
|
|
243
|
-
ui: 10,
|
|
244
|
-
tests: 5,
|
|
245
|
-
config: 15,
|
|
246
|
-
other: 5,
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
let score = 0;
|
|
250
|
-
let maxScore = 0;
|
|
251
|
-
|
|
252
|
-
for (const [category, files] of Object.entries(categories)) {
|
|
253
|
-
const weight = weights[category] || 5;
|
|
254
|
-
const fileCount = files.length;
|
|
255
|
-
|
|
256
|
-
// Diminishing returns for many files in same category
|
|
257
|
-
const categoryScore = Math.min(fileCount * weight, weight * 3);
|
|
258
|
-
score += categoryScore;
|
|
259
|
-
maxScore += weight * 3;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Normalize to 0-100
|
|
263
|
-
return Math.min(Math.round((score / maxScore) * 100), 100);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Get severity based on impact score
|
|
268
|
-
*/
|
|
269
|
-
getSeverity(score) {
|
|
270
|
-
if (score >= 70) return 'critical';
|
|
271
|
-
if (score >= 50) return 'high';
|
|
272
|
-
if (score >= 30) return 'medium';
|
|
273
|
-
return 'low';
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Generate violations for SunLint format
|
|
393
|
+
* Generate violations from impact analysis results
|
|
278
394
|
*/
|
|
279
|
-
|
|
395
|
+
generateViolationsFromImpact(impact) {
|
|
280
396
|
const violations = [];
|
|
281
397
|
|
|
282
|
-
//
|
|
283
|
-
if (
|
|
398
|
+
// Endpoint impact violations
|
|
399
|
+
if (impact.affectedEndpoints && impact.affectedEndpoints.length > 0) {
|
|
284
400
|
violations.push({
|
|
285
|
-
ruleId: 'IMPACT-
|
|
286
|
-
severity:
|
|
287
|
-
message:
|
|
288
|
-
file: categories.api[0],
|
|
401
|
+
ruleId: 'IMPACT-ENDPOINTS',
|
|
402
|
+
severity: impact.affectedEndpoints.length > 5 ? 'error' : 'warning',
|
|
403
|
+
message: `${impact.affectedEndpoints.length} API endpoint(s) affected by changes`,
|
|
289
404
|
line: 1,
|
|
290
405
|
column: 1,
|
|
291
406
|
category: 'impact',
|
|
292
407
|
source: 'impact-analyzer',
|
|
293
408
|
details: {
|
|
294
|
-
|
|
295
|
-
|
|
409
|
+
endpoints: impact.affectedEndpoints.slice(0, 10).map(ep =>
|
|
410
|
+
`${ep.method || 'GET'} ${ep.path}`
|
|
411
|
+
),
|
|
412
|
+
total: impact.affectedEndpoints.length,
|
|
413
|
+
recommendation: 'Review API contracts, update documentation, and verify backward compatibility',
|
|
296
414
|
},
|
|
297
415
|
});
|
|
298
416
|
}
|
|
299
417
|
|
|
300
|
-
|
|
418
|
+
// Database impact violations
|
|
419
|
+
if (impact.databaseImpact?.migrations?.length > 0 ||
|
|
420
|
+
impact.databaseImpact?.riskLevel !== 'none') {
|
|
301
421
|
violations.push({
|
|
302
422
|
ruleId: 'IMPACT-DATABASE',
|
|
303
|
-
severity:
|
|
304
|
-
message: `Database changes detected
|
|
305
|
-
file: categories.database[0],
|
|
423
|
+
severity: impact.databaseImpact.riskLevel === 'high' ? 'error' : 'warning',
|
|
424
|
+
message: `Database changes detected (${impact.databaseImpact.riskLevel} risk)`,
|
|
306
425
|
line: 1,
|
|
307
426
|
column: 1,
|
|
308
427
|
category: 'impact',
|
|
309
428
|
source: 'impact-analyzer',
|
|
310
429
|
details: {
|
|
311
|
-
|
|
312
|
-
|
|
430
|
+
migrations: impact.databaseImpact.migrations || [],
|
|
431
|
+
schemaChanges: impact.databaseImpact.schemaChanges || [],
|
|
432
|
+
riskLevel: impact.databaseImpact.riskLevel,
|
|
433
|
+
recommendation: 'Review migrations, ensure backward compatibility, and plan database rollback strategy',
|
|
313
434
|
},
|
|
314
435
|
});
|
|
315
436
|
}
|
|
316
437
|
|
|
317
|
-
|
|
438
|
+
// Logic impact violations
|
|
439
|
+
if (impact.logicImpact?.directCallers?.length > 5 ||
|
|
440
|
+
impact.logicImpact?.riskLevel === 'high') {
|
|
318
441
|
violations.push({
|
|
319
|
-
ruleId: 'IMPACT-
|
|
320
|
-
severity: 'warning',
|
|
321
|
-
message: `
|
|
322
|
-
file: categories.core[0],
|
|
442
|
+
ruleId: 'IMPACT-LOGIC',
|
|
443
|
+
severity: impact.logicImpact.riskLevel === 'high' ? 'error' : 'warning',
|
|
444
|
+
message: `High logic impact: ${impact.logicImpact.directCallers?.length || 0} direct callers affected`,
|
|
323
445
|
line: 1,
|
|
324
446
|
column: 1,
|
|
325
447
|
category: 'impact',
|
|
326
448
|
source: 'impact-analyzer',
|
|
327
449
|
details: {
|
|
328
|
-
|
|
329
|
-
|
|
450
|
+
directCallers: impact.logicImpact.directCallers || [],
|
|
451
|
+
indirectCallers: impact.logicImpact.indirectCallers || [],
|
|
452
|
+
riskLevel: impact.logicImpact.riskLevel,
|
|
453
|
+
recommendation: 'Ensure comprehensive test coverage for affected code paths',
|
|
330
454
|
},
|
|
331
455
|
});
|
|
332
456
|
}
|
|
333
457
|
|
|
334
|
-
|
|
335
|
-
|
|
458
|
+
// High severity overall impact
|
|
459
|
+
if (impact.severity === 'critical' || impact.impactScore > 70) {
|
|
460
|
+
violations.push({
|
|
461
|
+
ruleId: 'IMPACT-CRITICAL',
|
|
462
|
+
severity: 'error',
|
|
463
|
+
message: `Critical impact detected (score: ${impact.impactScore}/100)`,
|
|
464
|
+
line: 1,
|
|
465
|
+
column: 1,
|
|
466
|
+
category: 'impact',
|
|
467
|
+
source: 'impact-analyzer',
|
|
468
|
+
details: {
|
|
469
|
+
impactScore: impact.impactScore,
|
|
470
|
+
severity: impact.severity,
|
|
471
|
+
recommendation: 'This change has wide-reaching impact. Consider phased rollout and comprehensive testing.',
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
}
|
|
336
475
|
|
|
337
|
-
|
|
338
|
-
* Create empty result when no changes
|
|
339
|
-
*/
|
|
340
|
-
createEmptyResult(baseRef) {
|
|
341
|
-
return {
|
|
342
|
-
summary: {
|
|
343
|
-
baseRef,
|
|
344
|
-
totalChanges: 0,
|
|
345
|
-
impactScore: 0,
|
|
346
|
-
severity: 'none',
|
|
347
|
-
categories: {
|
|
348
|
-
api: 0,
|
|
349
|
-
database: 0,
|
|
350
|
-
core: 0,
|
|
351
|
-
ui: 0,
|
|
352
|
-
tests: 0,
|
|
353
|
-
config: 0,
|
|
354
|
-
other: 0,
|
|
355
|
-
}
|
|
356
|
-
},
|
|
357
|
-
changes: {
|
|
358
|
-
api: [],
|
|
359
|
-
database: [],
|
|
360
|
-
core: [],
|
|
361
|
-
ui: [],
|
|
362
|
-
tests: [],
|
|
363
|
-
config: [],
|
|
364
|
-
other: [],
|
|
365
|
-
},
|
|
366
|
-
changedFiles: [],
|
|
367
|
-
violations: [],
|
|
368
|
-
};
|
|
476
|
+
return violations;
|
|
369
477
|
}
|
|
370
478
|
|
|
371
479
|
/**
|
|
372
|
-
* Generate markdown report
|
|
480
|
+
* Generate markdown report (legacy - now handled by ReportGenerator)
|
|
373
481
|
*/
|
|
374
482
|
generateMarkdownReport(result) {
|
|
375
|
-
const { summary
|
|
483
|
+
const { summary } = result;
|
|
376
484
|
const severityEmoji = {
|
|
377
485
|
critical: '🔴',
|
|
378
486
|
high: '🟠',
|
|
@@ -428,6 +536,16 @@ class ImpactIntegration {
|
|
|
428
536
|
fs.writeFileSync(resolvedPath, markdownContent, 'utf8');
|
|
429
537
|
return resolvedPath;
|
|
430
538
|
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Save JSON report to file
|
|
542
|
+
*/
|
|
543
|
+
async saveJsonReport(results, outputPath) {
|
|
544
|
+
const resolvedPath = path.resolve(outputPath);
|
|
545
|
+
const jsonContent = JSON.stringify(results, null, 2);
|
|
546
|
+
fs.writeFileSync(resolvedPath, jsonContent, 'utf8');
|
|
547
|
+
return resolvedPath;
|
|
548
|
+
}
|
|
431
549
|
}
|
|
432
550
|
|
|
433
551
|
module.exports = { ImpactIntegration };
|
package/package.json
CHANGED