@sun-asterisk/sunlint 1.3.34 → 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.
Files changed (90) hide show
  1. package/core/architecture-integration.js +16 -7
  2. package/core/auto-performance-manager.js +1 -1
  3. package/core/cli-action-handler.js +102 -2
  4. package/core/cli-program.js +102 -138
  5. package/core/file-targeting-service.js +62 -4
  6. package/core/git-utils.js +19 -12
  7. package/core/github-annotate-service.js +326 -11
  8. package/core/html-report-generator.js +326 -731
  9. package/core/impact-integration.js +551 -0
  10. package/core/output-service.js +293 -21
  11. package/core/scoring-service.js +3 -2
  12. package/engines/arch-detect/core/analyzer.js +413 -0
  13. package/engines/arch-detect/core/index.js +22 -0
  14. package/engines/arch-detect/engine/hybrid-detector.js +176 -0
  15. package/engines/arch-detect/engine/index.js +24 -0
  16. package/engines/arch-detect/engine/rule-executor.js +228 -0
  17. package/engines/arch-detect/engine/score-calculator.js +214 -0
  18. package/engines/arch-detect/engine/violation-detector.js +616 -0
  19. package/engines/arch-detect/index.js +50 -0
  20. package/engines/arch-detect/rules/base-rule.js +187 -0
  21. package/engines/arch-detect/rules/index.js +35 -0
  22. package/engines/arch-detect/rules/layered/index.js +28 -0
  23. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +237 -0
  24. package/engines/arch-detect/rules/layered/l002-business-layer.js +215 -0
  25. package/engines/arch-detect/rules/layered/l003-data-layer.js +229 -0
  26. package/engines/arch-detect/rules/layered/l004-model-layer.js +204 -0
  27. package/engines/arch-detect/rules/layered/l005-layer-separation.js +215 -0
  28. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +221 -0
  29. package/engines/arch-detect/rules/layered/layered-rules-collection.js +445 -0
  30. package/engines/arch-detect/rules/modular/index.js +27 -0
  31. package/engines/arch-detect/rules/modular/m001-feature-modules.js +238 -0
  32. package/engines/arch-detect/rules/modular/m002-core-module.js +169 -0
  33. package/engines/arch-detect/rules/modular/m003-module-declaration.js +186 -0
  34. package/engines/arch-detect/rules/modular/m004-public-api.js +171 -0
  35. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +220 -0
  36. package/engines/arch-detect/rules/modular/modular-rules-collection.js +357 -0
  37. package/engines/arch-detect/rules/presentation/index.js +27 -0
  38. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +221 -0
  39. package/engines/arch-detect/rules/presentation/pr002-presentation-logic.js +192 -0
  40. package/engines/arch-detect/rules/presentation/pr004-data-binding.js +187 -0
  41. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +185 -0
  42. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +181 -0
  43. package/engines/arch-detect/rules/presentation/presentation-rules-collection.js +507 -0
  44. package/engines/arch-detect/rules/project-scanner/index.js +31 -0
  45. package/engines/arch-detect/rules/project-scanner/ps001-project-root.js +213 -0
  46. package/engines/arch-detect/rules/project-scanner/ps002-language-detection.js +192 -0
  47. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +339 -0
  48. package/engines/arch-detect/rules/project-scanner/ps004-build-system.js +171 -0
  49. package/engines/arch-detect/rules/project-scanner/ps005-source-directory.js +163 -0
  50. package/engines/arch-detect/rules/project-scanner/ps006-test-directory.js +184 -0
  51. package/engines/arch-detect/rules/project-scanner/ps007-documentation.js +149 -0
  52. package/engines/arch-detect/rules/project-scanner/ps008-cicd-detection.js +163 -0
  53. package/engines/arch-detect/rules/project-scanner/ps009-code-quality.js +152 -0
  54. package/engines/arch-detect/rules/project-scanner/ps010-statistics.js +180 -0
  55. package/engines/arch-detect/rules/rule-registry.js +111 -0
  56. package/engines/arch-detect/types/context.types.js +60 -0
  57. package/engines/arch-detect/types/enums.js +161 -0
  58. package/engines/arch-detect/types/index.js +25 -0
  59. package/engines/arch-detect/types/result.types.js +7 -0
  60. package/engines/arch-detect/types/rule.types.js +7 -0
  61. package/engines/arch-detect/utils/file-scanner.js +411 -0
  62. package/engines/arch-detect/utils/index.js +23 -0
  63. package/engines/arch-detect/utils/pattern-matcher.js +328 -0
  64. package/engines/impact/cli.js +106 -0
  65. package/engines/impact/config/default-config.js +54 -0
  66. package/engines/impact/core/change-detector.js +258 -0
  67. package/engines/impact/core/detectors/database-detector.js +1317 -0
  68. package/engines/impact/core/detectors/endpoint-detector.js +55 -0
  69. package/engines/impact/core/impact-analyzer.js +124 -0
  70. package/engines/impact/core/report-generator.js +462 -0
  71. package/engines/impact/core/utils/ast-parser.js +241 -0
  72. package/engines/impact/core/utils/dependency-graph.js +159 -0
  73. package/engines/impact/core/utils/file-utils.js +116 -0
  74. package/engines/impact/core/utils/git-utils.js +203 -0
  75. package/engines/impact/core/utils/logger.js +13 -0
  76. package/engines/impact/core/utils/method-call-graph.js +1192 -0
  77. package/engines/impact/index.js +135 -0
  78. package/engines/impact/package.json +29 -0
  79. package/package.json +18 -43
  80. package/scripts/build-release.sh +0 -0
  81. package/scripts/copy-impact-analyzer.js +135 -0
  82. package/scripts/install.sh +0 -0
  83. package/scripts/manual-release.sh +0 -0
  84. package/scripts/pre-release-test.sh +0 -0
  85. package/scripts/prepare-release.sh +0 -0
  86. package/scripts/quick-performance-test.js +0 -0
  87. package/scripts/setup-github-registry.sh +0 -0
  88. package/scripts/trigger-release.sh +0 -0
  89. package/scripts/verify-install.sh +0 -0
  90. package/templates/combined-report.html +1418 -0
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Impact Integration for SunLint
3
+ * Wraps impact-analyzer module for seamless integration
4
+ * Following Rule C005: Single responsibility - handle impact analysis integration
5
+ */
6
+
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const chalk = require('chalk');
10
+ const { execSync } = require('child_process');
11
+
12
+ class ImpactIntegration {
13
+ constructor(options = {}) {
14
+ this.options = options;
15
+ this.impactModulePath = null;
16
+ this.ChangeDetector = null;
17
+ this.ImpactAnalyzer = null;
18
+ this.ReportGenerator = null;
19
+ }
20
+
21
+ /**
22
+ * Load impact analyzer module
23
+ * Note: impact-analyzer is ESM, so we use dynamic import
24
+ */
25
+ async loadImpactModule() {
26
+ if (this.ChangeDetector && this.ImpactAnalyzer) {
27
+ return;
28
+ }
29
+
30
+ // Priority 1: Try bundled version (engines/impact)
31
+ const bundledPath = path.join(__dirname, '..', 'engines', 'impact');
32
+ if (fs.existsSync(path.join(bundledPath, 'index.js'))) {
33
+ try {
34
+ this.impactModulePath = bundledPath;
35
+ await this.loadModules(bundledPath);
36
+ if (this.options.verbose) {
37
+ console.log(chalk.gray('📦 Loaded bundled impact-analyzer'));
38
+ }
39
+ return;
40
+ } catch (error) {
41
+ if (this.options.verbose) {
42
+ console.log(chalk.yellow(`⚠️ Failed to load bundled: ${error.message}`));
43
+ }
44
+ }
45
+ }
46
+
47
+ // Priority 2: Try local development path
48
+ const devPaths = [
49
+ path.join(__dirname, '..', '..', '..', '..', 'impact-analyzer'),
50
+ path.join(__dirname, '..', '..', '..', 'impact-analyzer'),
51
+ ];
52
+
53
+ for (const devPath of devPaths) {
54
+ if (fs.existsSync(path.join(devPath, 'index.js'))) {
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
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ throw new Error(
71
+ 'Impact analyzer module not found. Ensure impact-analyzer exists in the monorepo.'
72
+ );
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
+
93
+ /**
94
+ * Check if git repository
95
+ */
96
+ isGitRepo(projectPath) {
97
+ try {
98
+ execSync('git rev-parse --git-dir', {
99
+ cwd: projectPath,
100
+ stdio: 'pipe'
101
+ });
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Check if git ref exists
110
+ */
111
+ refExists(ref, projectPath) {
112
+ try {
113
+ execSync(`git rev-parse ${ref}`, {
114
+ cwd: projectPath,
115
+ stdio: 'pipe'
116
+ });
117
+ return true;
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Build config object for impact analyzer
125
+ */
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
+ };
138
+ }
139
+
140
+ /**
141
+ * Run impact analysis on project
142
+ * @param {string} projectPath - Path to analyze
143
+ * @returns {Object} Impact analysis results
144
+ */
145
+ async analyze(projectPath) {
146
+ await this.loadImpactModule();
147
+
148
+ const absolutePath = path.resolve(projectPath);
149
+ const config = this.buildConfig(absolutePath);
150
+
151
+ // Validation
152
+ if (!fs.existsSync(absolutePath)) {
153
+ throw new Error(`Directory does not exist: ${absolutePath}`);
154
+ }
155
+
156
+ if (!this.isGitRepo(absolutePath)) {
157
+ throw new Error(`Not a git repository: ${absolutePath}`);
158
+ }
159
+
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}`);
166
+ }
167
+
168
+ // Ensure tsconfig.json exists (required by ts-morph)
169
+ await this.ensureTsConfig(absolutePath);
170
+
171
+ if (this.options.verbose) {
172
+ console.log(chalk.blue(`\n🔍 Impact Analysis Configuration`));
173
+ console.log(chalk.gray(` Path: ${absolutePath}`));
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}`));
178
+ }
179
+
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;
237
+
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
252
+ }
253
+
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
+ };
270
+
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
+ }
280
+
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 {
308
+ summary: {
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',
315
+ categories: {
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',
342
+ }
343
+ },
344
+ violations: this.generateViolationsFromImpact(impact),
345
+ fileCategories,
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Categorize changed files for reporting
351
+ */
352
+ categorizeFiles(changedFiles) {
353
+ const categories = {
354
+ api: [],
355
+ database: [],
356
+ core: [],
357
+ ui: [],
358
+ tests: [],
359
+ config: [],
360
+ other: [],
361
+ };
362
+
363
+ const patterns = {
364
+ api: [/controller/i, /route/i, /endpoint/i, /api\//i, /handler/i],
365
+ database: [/repository/i, /model/i, /schema/i, /migration/i, /entity/i, /dao/i],
366
+ core: [/service/i, /usecase/i, /domain/i, /core\//i, /lib\//i],
367
+ ui: [/component/i, /view/i, /page/i, /screen/i, /widget/i, /\.tsx$/i, /\.vue$/i],
368
+ tests: [/test/i, /spec/i, /__tests__/i],
369
+ config: [/config/i, /\.json$/i, /\.ya?ml$/i, /\.env/i, /dockerfile/i],
370
+ };
371
+
372
+ for (const file of changedFiles) {
373
+ const filePath = file.path || file;
374
+ let categorized = false;
375
+
376
+ for (const [category, regexes] of Object.entries(patterns)) {
377
+ if (regexes.some(regex => regex.test(filePath))) {
378
+ categories[category].push(filePath);
379
+ categorized = true;
380
+ break;
381
+ }
382
+ }
383
+
384
+ if (!categorized) {
385
+ categories.other.push(filePath);
386
+ }
387
+ }
388
+
389
+ return categories;
390
+ }
391
+
392
+ /**
393
+ * Generate violations from impact analysis results
394
+ */
395
+ generateViolationsFromImpact(impact) {
396
+ const violations = [];
397
+
398
+ // Endpoint impact violations
399
+ if (impact.affectedEndpoints && impact.affectedEndpoints.length > 0) {
400
+ violations.push({
401
+ ruleId: 'IMPACT-ENDPOINTS',
402
+ severity: impact.affectedEndpoints.length > 5 ? 'error' : 'warning',
403
+ message: `${impact.affectedEndpoints.length} API endpoint(s) affected by changes`,
404
+ line: 1,
405
+ column: 1,
406
+ category: 'impact',
407
+ source: 'impact-analyzer',
408
+ details: {
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',
414
+ },
415
+ });
416
+ }
417
+
418
+ // Database impact violations
419
+ if (impact.databaseImpact?.migrations?.length > 0 ||
420
+ impact.databaseImpact?.riskLevel !== 'none') {
421
+ violations.push({
422
+ ruleId: 'IMPACT-DATABASE',
423
+ severity: impact.databaseImpact.riskLevel === 'high' ? 'error' : 'warning',
424
+ message: `Database changes detected (${impact.databaseImpact.riskLevel} risk)`,
425
+ line: 1,
426
+ column: 1,
427
+ category: 'impact',
428
+ source: 'impact-analyzer',
429
+ details: {
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',
434
+ },
435
+ });
436
+ }
437
+
438
+ // Logic impact violations
439
+ if (impact.logicImpact?.directCallers?.length > 5 ||
440
+ impact.logicImpact?.riskLevel === 'high') {
441
+ violations.push({
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`,
445
+ line: 1,
446
+ column: 1,
447
+ category: 'impact',
448
+ source: 'impact-analyzer',
449
+ details: {
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',
454
+ },
455
+ });
456
+ }
457
+
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
+ }
475
+
476
+ return violations;
477
+ }
478
+
479
+ /**
480
+ * Generate markdown report (legacy - now handled by ReportGenerator)
481
+ */
482
+ generateMarkdownReport(result) {
483
+ const { summary } = result;
484
+ const severityEmoji = {
485
+ critical: '🔴',
486
+ high: '🟠',
487
+ medium: '🟡',
488
+ low: '🟢',
489
+ none: '⚪',
490
+ };
491
+
492
+ let md = `# Impact Analysis Report\n\n`;
493
+ md += `**Base Reference:** \`${summary.baseRef}\`\n`;
494
+ md += `**Generated:** ${new Date().toISOString()}\n\n`;
495
+
496
+ md += `## Summary\n\n`;
497
+ md += `| Metric | Value |\n`;
498
+ md += `|--------|-------|\n`;
499
+ md += `| Total Changes | ${summary.totalChanges} files |\n`;
500
+ md += `| Impact Score | ${summary.impactScore}/100 |\n`;
501
+ md += `| Severity | ${severityEmoji[summary.severity]} ${summary.severity.toUpperCase()} |\n\n`;
502
+
503
+ md += `## Changes by Category\n\n`;
504
+ md += `| Category | Files |\n`;
505
+ md += `|----------|-------|\n`;
506
+
507
+ for (const [category, count] of Object.entries(summary.categories)) {
508
+ if (count > 0) {
509
+ md += `| ${category.charAt(0).toUpperCase() + category.slice(1)} | ${count} |\n`;
510
+ }
511
+ }
512
+
513
+ md += `\n## Changed Files\n\n`;
514
+
515
+ for (const [category, files] of Object.entries(changes)) {
516
+ if (files.length > 0) {
517
+ md += `### ${category.charAt(0).toUpperCase() + category.slice(1)} (${files.length})\n\n`;
518
+ for (const file of files) {
519
+ md += `- \`${file}\`\n`;
520
+ }
521
+ md += `\n`;
522
+ }
523
+ }
524
+
525
+ md += `---\n`;
526
+ md += `*Generated by SunLint Impact Analyzer*\n`;
527
+
528
+ return md;
529
+ }
530
+
531
+ /**
532
+ * Save markdown report to file
533
+ */
534
+ async saveReport(markdownContent, outputPath) {
535
+ const resolvedPath = path.resolve(outputPath || 'impact-report.md');
536
+ fs.writeFileSync(resolvedPath, markdownContent, 'utf8');
537
+ return resolvedPath;
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
+ }
549
+ }
550
+
551
+ module.exports = { ImpactIntegration };