@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.
@@ -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}`));
@@ -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 # Analyze code changes
127
- sunlint --impact --impact-base=origin/main # Compare with main
128
- sunlint --impact --impact-report=report.md # Custom output file
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.impactModule = null;
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.impactModule) {
24
- return this.impactModule;
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
- // For ESM modules, we need to use dynamic import
32
- const modulePath = path.join(bundledPath, 'core', 'impact-analyzer.js');
33
- if (fs.existsSync(modulePath)) {
34
- this.impactModule = {
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
- this.impactModule = {
59
- bundledPath: devPath,
60
- type: 'development'
61
- };
62
- if (this.options.verbose) {
63
- console.log(chalk.gray(`📦 Found impact-analyzer at: ${devPath}`));
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. Run "npm run build" in sunlint directory.'
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
- * Get changed files from git
124
+ * Build config object for impact analyzer
106
125
  */
107
- getChangedFiles(baseRef, projectPath) {
108
- try {
109
- const result = execSync(`git diff --name-only ${baseRef}`, {
110
- cwd: projectPath,
111
- encoding: 'utf-8'
112
- });
113
- return result.trim().split('\n').filter(Boolean);
114
- } catch (error) {
115
- throw new Error(`Failed to get changed files: ${error.message}`);
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 baseRef = this.options.impactBase || 'HEAD~1';
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
- // Get changed files
150
- const changedFiles = this.getChangedFiles(baseRef, absolutePath);
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
- if (changedFiles.length === 0) {
153
- return this.createEmptyResult(baseRef);
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
- // Categorize changes
157
- const categorizedChanges = this.categorizeChanges(changedFiles, absolutePath);
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
- // Calculate impact score
160
- const impactScore = this.calculateImpactScore(categorizedChanges);
161
- const severity = this.getSeverity(impactScore);
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
- // Build result
164
- const result = {
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
- totalChanges: changedFiles.length,
168
- impactScore,
169
- severity,
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
- api: categorizedChanges.api.length,
172
- database: categorizedChanges.database.length,
173
- core: categorizedChanges.core.length,
174
- ui: categorizedChanges.ui.length,
175
- tests: categorizedChanges.tests.length,
176
- config: categorizedChanges.config.length,
177
- other: categorizedChanges.other.length,
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
- changes: categorizedChanges,
181
- changedFiles,
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 by type
350
+ * Categorize changed files for reporting
195
351
  */
196
- categorizeChanges(files, basePath) {
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 files) {
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(file))) {
221
- categories[category].push(file);
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(file);
385
+ categories.other.push(filePath);
229
386
  }
230
387
  }
231
388
 
@@ -233,146 +390,97 @@ class ImpactIntegration {
233
390
  }
234
391
 
235
392
  /**
236
- * Calculate impact score based on changes
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
- generateViolations(categories, impactScore) {
395
+ generateViolationsFromImpact(impact) {
280
396
  const violations = [];
281
397
 
282
- // Add violations for high-impact changes
283
- if (categories.api.length > 0) {
398
+ // Endpoint impact violations
399
+ if (impact.affectedEndpoints && impact.affectedEndpoints.length > 0) {
284
400
  violations.push({
285
- ruleId: 'IMPACT-API',
286
- severity: categories.api.length > 3 ? 'error' : 'warning',
287
- message: `API changes detected: ${categories.api.length} file(s) modified`,
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
- files: categories.api,
295
- recommendation: 'Review API contracts and update documentation',
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
- if (categories.database.length > 0) {
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: categories.database.length > 2 ? 'error' : 'warning',
304
- message: `Database changes detected: ${categories.database.length} file(s) modified`,
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
- files: categories.database,
312
- recommendation: 'Verify migrations and backup strategy',
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
- if (categories.core.length > 5) {
438
+ // Logic impact violations
439
+ if (impact.logicImpact?.directCallers?.length > 5 ||
440
+ impact.logicImpact?.riskLevel === 'high') {
318
441
  violations.push({
319
- ruleId: 'IMPACT-CORE',
320
- severity: 'warning',
321
- message: `Large core logic changes: ${categories.core.length} file(s) modified`,
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
- files: categories.core,
329
- recommendation: 'Ensure adequate test coverage for core changes',
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
- return violations;
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, changes } = result;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sunlint",
3
- "version": "1.3.35",
3
+ "version": "1.3.36",
4
4
  "description": "☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards",
5
5
  "main": "cli.js",
6
6
  "bin": {