@sun-asterisk/impact-analyzer 1.0.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.
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Impact Analyzer Module
3
+ * Analyzes the impact of code changes across multiple dimensions
4
+ */
5
+
6
+ import { MethodCallGraph } from './utils/method-call-graph.js';
7
+ import { EndpointDetector } from './detectors/endpoint-detector.js';
8
+ import { DatabaseDetector } from './detectors/database-detector.js';
9
+ import path from 'path';
10
+
11
+ export class ImpactAnalyzer {
12
+ constructor(config) {
13
+ this.config = config;
14
+ this.absoluteSourceDir = path.resolve(config.sourceDir);
15
+ this.methodCallGraph = null;
16
+ }
17
+
18
+ async initializeMethodCallGraph() {
19
+ this.methodCallGraph = new MethodCallGraph();
20
+ await this.methodCallGraph.initialize(
21
+ this.absoluteSourceDir,
22
+ this.config.excludePaths,
23
+ this.config.verbose
24
+ );
25
+
26
+ this.endpointDetector = new EndpointDetector(this.methodCallGraph, this.config);
27
+ this.databaseDetector = new DatabaseDetector(this.methodCallGraph, this.config);
28
+
29
+ const stats = this.methodCallGraph.getStats();
30
+ if (this.config.verbose) {
31
+ console.log(`\nšŸ“Š Call Graph Statistics:`);
32
+ console.log(` Total methods: ${stats.totalMethods}`);
33
+ console.log(` Total endpoints: ${stats.totalEndpoints}`);
34
+ console.log(` Call relationships: ${stats.totalCallRelationships}\n`);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Perform comprehensive impact analysis
40
+ */
41
+ async analyzeImpact(changes) {
42
+ if (!this.methodCallGraph) {
43
+ await this.initializeMethodCallGraph();
44
+ }
45
+
46
+ const changedFilesWithAbsolutePaths = changes.changedFiles.map(file => ({
47
+ ...file,
48
+ absolutePath: path.join(path.dirname(this.absoluteSourceDir), file.path)
49
+ }));
50
+
51
+ const affectedEndpoints = await this.endpointDetector.detect(changedFilesWithAbsolutePaths);
52
+ const databaseImpact = await this.databaseDetector.detect(changedFilesWithAbsolutePaths);
53
+ const logicImpact = this.detectLogicImpact(changes.changedSymbols);
54
+
55
+ const impactScore = this.calculateImpactScore({
56
+ affectedEndpoints,
57
+ databaseImpact,
58
+ logicImpact,
59
+ });
60
+
61
+ return {
62
+ affectedEndpoints,
63
+ databaseImpact,
64
+ logicImpact,
65
+ impactScore,
66
+ severity: this.determineSeverity(impactScore),
67
+ };
68
+ }
69
+
70
+ detectLogicImpact(changedSymbols) {
71
+ const directCallers = new Set();
72
+ const indirectCallers = new Set();
73
+
74
+ for (const symbol of changedSymbols) {
75
+ const methodName = `${symbol.className || 'global'}.${symbol.name}`;
76
+ const callers = this.methodCallGraph.findAllCallers(methodName);
77
+
78
+ callers.forEach(caller => {
79
+ directCallers.add(caller);
80
+
81
+ const indirectCallersOfCaller = this.methodCallGraph.findAllCallers(caller);
82
+ indirectCallersOfCaller.forEach(ic => {
83
+ if (!callers.includes(ic)) {
84
+ indirectCallers.add(ic);
85
+ }
86
+ });
87
+ });
88
+ }
89
+
90
+ return {
91
+ directCallers: Array.from(directCallers),
92
+ indirectCallers: Array.from(indirectCallers),
93
+ riskLevel: this.calculateRiskLevel(directCallers.size, indirectCallers.size),
94
+ };
95
+ }
96
+
97
+ calculateRiskLevel(directCount, indirectCount) {
98
+ const total = directCount + indirectCount;
99
+ if (total > 10) return 'high';
100
+ if (total > 5) return 'medium';
101
+ return 'low';
102
+ }
103
+
104
+ calculateImpactScore(result) {
105
+ let score = 0;
106
+
107
+ score += (result.affectedEndpoints?.length || 0) * 10;
108
+ score += (result.databaseImpact?.length || 0) * 5;
109
+ score += (result.logicImpact?.directCallers.length || 0) * 3;
110
+ score += (result.logicImpact?.indirectCallers.length || 0) * 1;
111
+
112
+ if (result.logicImpact?.riskLevel === 'high') score *= 1.5;
113
+ if (result.databaseImpact?.some(d => d.hasMigration)) score += 20;
114
+
115
+ return Math.round(score);
116
+ }
117
+
118
+ determineSeverity(score) {
119
+ if (score > 100) return 'critical';
120
+ if (score > 50) return 'high';
121
+ if (score > 20) return 'medium';
122
+ return 'low';
123
+ }
124
+ }
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Report Generator Module
3
+ * Generates formatted reports in various formats
4
+ */
5
+
6
+ export class ReportGenerator {
7
+ /**
8
+ * Generate Markdown report
9
+ */
10
+ generateMarkdownReport(changes, impact) {
11
+ const sections = [];
12
+
13
+ // Header
14
+ sections.push('# šŸ” Impact Analysis Report\n');
15
+ sections.push(this.generateSummarySection(changes, impact));
16
+ sections.push(this.generateEndpointsSection(impact.affectedEndpoints));
17
+ sections.push(this.generateDatabaseSection(impact.databaseImpact));
18
+ sections.push(this.generatePagesSection(changes.changedFiles));
19
+ sections.push(this.generateLogicSection(impact.logicImpact));
20
+ sections.push(this.generateChangedFilesSection(changes.changedFiles));
21
+
22
+ return sections.filter(s => s).join('\n');
23
+ }
24
+
25
+ /**
26
+ * Generate console report
27
+ */
28
+ generateConsoleReport(changes, impact) {
29
+ const width = 70;
30
+
31
+ console.log('\n' + '='.repeat(width));
32
+ console.log(this.centerText('IMPACT ANALYSIS REPORT', width));
33
+ console.log('='.repeat(width) + '\n');
34
+
35
+ // Summary
36
+ console.log('šŸ“Š SUMMARY:');
37
+ console.log(` Files Changed: ${changes.changedFiles.length}`);
38
+ console.log(` Symbols Modified: ${changes.changedSymbols.length}`);
39
+ console.log(` Impact Score: ${impact.impactScore}`);
40
+ console.log(` Severity: ${this.getSeverityEmoji(impact.severity)} ${impact.severity.toUpperCase()}`);
41
+ console.log('');
42
+
43
+ // Endpoints
44
+ if (impact.affectedEndpoints.length > 0) {
45
+ console.log('šŸ“” AFFECTED API ENDPOINTS:');
46
+ impact.affectedEndpoints.slice(0, 10).forEach(e => {
47
+ console.log(` ${this.getImpactEmoji(e.impactLevel)} ${e.method.padEnd(7)} ${e.path}`);
48
+ });
49
+ if (impact.affectedEndpoints.length > 10) {
50
+ console.log(` ...and ${impact.affectedEndpoints.length - 10} more`);
51
+ }
52
+ console.log('');
53
+ }
54
+
55
+ // Database
56
+ if (impact.databaseImpact.length > 0) {
57
+ console.log('šŸ’¾ DATABASE IMPACT:');
58
+ impact.databaseImpact.forEach(d => {
59
+ console.log(` • ${d.table} (${d.operations.join(', ')})`);
60
+ if (d.fields && d.fields.length > 0) {
61
+ console.log(` Fields: ${d.fields.slice(0, 10).join(', ')}${d.fields.length > 10 ? '...' : ''}`);
62
+ }
63
+ });
64
+ console.log('');
65
+ }
66
+
67
+ // Pages/Components
68
+ const pageFiles = changes.changedFiles.filter(f => {
69
+ const path = f.path.toLowerCase();
70
+ return (
71
+ path.includes('/page') || path.includes('/component') ||
72
+ path.includes('/view') || path.includes('/screen') ||
73
+ path.includes('.page.') || path.includes('.component.') ||
74
+ path.includes('.view.') || path.includes('.screen.')
75
+ );
76
+ });
77
+
78
+ if (pageFiles.length > 0) {
79
+ console.log('šŸ“„ AFFECTED PAGES/COMPONENTS:');
80
+ pageFiles.slice(0, 10).forEach(f => {
81
+ const type = this.detectPageType(f.path);
82
+ const statusEmoji = this.getStatusEmoji(f.status);
83
+ console.log(` ${statusEmoji} ${type} ${f.path}`);
84
+ });
85
+ if (pageFiles.length > 10) {
86
+ console.log(` ...and ${pageFiles.length - 10} more`);
87
+ }
88
+ console.log('');
89
+ }
90
+
91
+ // Logic Impact
92
+ console.log('šŸ”„ LOGIC IMPACT:');
93
+ console.log(` Risk Level: ${this.getRiskEmoji(impact.logicImpact.riskLevel)} ${impact.logicImpact.riskLevel.toUpperCase()}`);
94
+ console.log(` Direct Callers: ${impact.logicImpact.directCallers.length}`);
95
+ console.log(` Indirect Callers: ${impact.logicImpact.indirectCallers.length}`);
96
+ console.log('');
97
+
98
+ console.log('='.repeat(width) + '\n');
99
+ }
100
+
101
+ /**
102
+ * Generate JSON report
103
+ */
104
+ generateJSONReport(changes, impact) {
105
+ return JSON.stringify({
106
+ timestamp: new Date().toISOString(),
107
+ summary: {
108
+ filesChanged: changes.changedFiles.length,
109
+ symbolsModified: changes.changedSymbols.length,
110
+ impactScore: impact.impactScore,
111
+ severity: impact.severity,
112
+ },
113
+ changes,
114
+ impact,
115
+ }, null, 2);
116
+ }
117
+
118
+ /**
119
+ * Section Generators
120
+ */
121
+ generateSummarySection(changes, impact) {
122
+ return `## šŸ“Š Summary
123
+
124
+ | Metric | Value |
125
+ |--------|-------|
126
+ | Files Changed | ${changes.changedFiles.length} |
127
+ | Symbols Modified | ${changes.changedSymbols.length} |
128
+ | Impact Score | **${impact.impactScore}** |
129
+ | Severity | ${this.getSeverityEmoji(impact.severity)} **${impact.severity.toUpperCase()}** |
130
+
131
+ `;
132
+ }
133
+
134
+ generateEndpointsSection(endpoints) {
135
+ if (! endpoints || endpoints.length === 0) return '';
136
+
137
+ const lines = ['## šŸ“” Affected API Endpoints\n'];
138
+
139
+ lines.push('| Method | Path | Controller | Impact |');
140
+ lines.push('|--------|------|------------|--------|');
141
+
142
+ endpoints.forEach(e => {
143
+ const emoji = this.getImpactEmoji(e.impactLevel);
144
+ lines.push(`| **${e.method}** | \`${e.path}\` | ${e.controller} | ${emoji} ${e.impactLevel} |`);
145
+ });
146
+
147
+ lines.push('');
148
+ return lines.join('\n');
149
+ }
150
+
151
+ generateDatabaseSection(dbImpacts) {
152
+ if (!dbImpacts || dbImpacts.length === 0) return '';
153
+
154
+ const lines = ['## šŸ’¾ Database Impact\n'];
155
+
156
+ lines.push(`**Total Tables Affected:** ${dbImpacts.length}\n`);
157
+
158
+ // Simple table summary
159
+ lines.push('| Table | Operations | Fields |');
160
+ lines.push('|-------|------------|--------|');
161
+
162
+ dbImpacts.forEach(db => {
163
+ const fieldsDisplay = db.fields && db.fields.length > 0
164
+ ? db.fields.slice(0, 3).join(', ') + (db.fields.length > 3 ? '...' : '')
165
+ : '-';
166
+ lines.push(`| \`${db.table}\` | ${db.operations.join(', ')} | ${fieldsDisplay} |`);
167
+ });
168
+
169
+ lines.push('');
170
+
171
+ // Detailed breakdown only if fields exist
172
+ const tablesWithFields = dbImpacts.filter(db => db.fields && db.fields.length > 0);
173
+
174
+ if (tablesWithFields.length > 0) {
175
+ lines.push('### Field Details\n');
176
+
177
+ tablesWithFields.forEach(db => {
178
+ lines.push(`#### \`${db.table}\`\n`);
179
+ lines.push(`**Operations:** ${db.operations.join(', ')}\n`);
180
+ lines.push(`**Fields (${db.fields.length}):** ${db.fields.map(f => `\`${f}\``).join(', ')}\n`);
181
+ });
182
+ }
183
+
184
+ return lines.join('\n');
185
+ }
186
+
187
+ generatePagesSection(changedFiles) {
188
+ if (!changedFiles || changedFiles.length === 0) return '';
189
+
190
+ // Detect page files (components, pages, views, screens)
191
+ const pageFiles = changedFiles.filter(f => {
192
+ const path = f.path.toLowerCase();
193
+ return (
194
+ path.includes('/page') ||
195
+ path.includes('/component') ||
196
+ path.includes('/view') ||
197
+ path.includes('/screen') ||
198
+ path.includes('.page.') ||
199
+ path.includes('.component.') ||
200
+ path.includes('.view.') ||
201
+ path.includes('.screen.')
202
+ );
203
+ });
204
+
205
+ if (pageFiles.length === 0) return '';
206
+
207
+ const lines = ['## šŸ“„ Affected Pages/Components\n'];
208
+
209
+ lines.push(`**Total Pages/Components Affected:** ${pageFiles.length}\n`);
210
+
211
+ // Group by type
212
+ const byType = {
213
+ page: [],
214
+ component: [],
215
+ view: [],
216
+ screen: [],
217
+ other: []
218
+ };
219
+
220
+ pageFiles.forEach(f => {
221
+ const path = f.path.toLowerCase();
222
+ if (path.includes('/page') || path.includes('.page.')) {
223
+ byType.page.push(f);
224
+ } else if (path.includes('/component') || path.includes('.component.')) {
225
+ byType.component.push(f);
226
+ } else if (path.includes('/view') || path.includes('.view.')) {
227
+ byType.view.push(f);
228
+ } else if (path.includes('/screen') || path.includes('.screen.')) {
229
+ byType.screen.push(f);
230
+ } else {
231
+ byType.other.push(f);
232
+ }
233
+ });
234
+
235
+ // List all pages
236
+ lines.push('### Pages/Components List\n');
237
+ lines.push('| Type | File | Status | Changes |');
238
+ lines.push('|------|------|--------|---------|');
239
+
240
+ pageFiles.forEach(f => {
241
+ const statusEmoji = this.getStatusEmoji(f.status);
242
+ const changes = `+${f.changes.added} -${f.changes.deleted}`;
243
+ const type = this.detectPageType(f.path);
244
+ lines.push(`| ${type} | \`${f.path}\` | ${statusEmoji} ${f.status} | ${changes} |`);
245
+ });
246
+
247
+ lines.push('');
248
+
249
+ // Group by type for easier review
250
+ Object.entries(byType).forEach(([type, files]) => {
251
+ if (files.length > 0) {
252
+ lines.push(`### ${this.capitalizeFirst(type)}s (${files.length})\n`);
253
+ files.forEach(f => {
254
+ const statusEmoji = this.getStatusEmoji(f.status);
255
+ const changes = `+${f.changes.added} -${f.changes.deleted}`;
256
+ lines.push(`- ${statusEmoji} \`${f.path}\` (${changes})`);
257
+ });
258
+ lines.push('');
259
+ }
260
+ });
261
+
262
+ return lines.join('\n');
263
+ }
264
+
265
+ generateLogicSection(logicImpact) {
266
+ const lines = ['## šŸ”„ Logic Impact\n'];
267
+
268
+ lines.push(`**Risk Level:** ${this.getRiskEmoji(logicImpact.riskLevel)} ${logicImpact.riskLevel.toUpperCase()}\n`);
269
+
270
+ if (logicImpact.directCallers.length > 0) {
271
+ lines.push('### Direct Callers\n');
272
+ logicImpact.directCallers.slice(0, 15).forEach(c => {
273
+ lines.push(`- \`${c}\``);
274
+ });
275
+ if (logicImpact.directCallers.length > 15) {
276
+ lines.push(`- ...and ${logicImpact.directCallers.length - 15} more`);
277
+ }
278
+ lines.push('');
279
+ }
280
+
281
+ if (logicImpact.indirectCallers.length > 0) {
282
+ lines.push('### Indirect Callers\n');
283
+ logicImpact.indirectCallers.slice(0, 10).forEach(c => {
284
+ lines.push(`- \`${c}\``);
285
+ });
286
+ if (logicImpact.indirectCallers.length > 10) {
287
+ lines.push(`- ...and ${logicImpact.indirectCallers.length - 10} more`);
288
+ }
289
+ lines.push('');
290
+ }
291
+
292
+ return lines.join('\n');
293
+ }
294
+
295
+
296
+
297
+ generateChangedFilesSection(changedFiles) {
298
+ if (!changedFiles || changedFiles.length === 0) return '';
299
+
300
+ const lines = ['## šŸ“ Changed Files\n'];
301
+
302
+ lines.push('| File | Status | Type | Lines Changed |');
303
+ lines.push('|------|--------|------|---------------|');
304
+
305
+ changedFiles.forEach(f => {
306
+ const statusEmoji = this.getStatusEmoji(f.status);
307
+ const changes = `+${f.changes.added} -${f.changes.deleted}`;
308
+ lines.push(`| \`${f.path}\` | ${statusEmoji} ${f.status} | ${f.type} | ${changes} |`);
309
+ });
310
+
311
+ lines.push('');
312
+ return lines.join('\n');
313
+ }
314
+
315
+ /**
316
+ * Helper methods
317
+ */
318
+ getSeverityEmoji(severity) {
319
+ const map = {
320
+ low: '🟢',
321
+ medium: '🟔',
322
+ high: '🟠',
323
+ critical: 'šŸ”“',
324
+ };
325
+ return map[severity] || '⚪';
326
+ }
327
+
328
+ getImpactEmoji(impact) {
329
+ const map = {
330
+ low: '🟢',
331
+ medium: '🟔',
332
+ high: 'šŸ”“',
333
+ };
334
+ return map[impact] || '⚪';
335
+ }
336
+
337
+ getRiskEmoji(risk) {
338
+ const map = {
339
+ low: 'āœ…',
340
+ medium: 'āš ļø',
341
+ high: '🚨',
342
+ };
343
+ return map[risk] || '⚪';
344
+ }
345
+
346
+ getStatusEmoji(status) {
347
+ const map = {
348
+ added: '✨',
349
+ modified: 'šŸ“',
350
+ deleted: 'šŸ—‘ļø',
351
+ renamed: 'šŸ“‹',
352
+ };
353
+ return map[status] || 'šŸ“„';
354
+ }
355
+
356
+ centerText(text, width) {
357
+ const padding = Math.floor((width - text.length) / 2);
358
+ return ' '.repeat(padding) + text;
359
+ }
360
+
361
+ detectPageType(path) {
362
+ const lower = path.toLowerCase();
363
+ if (lower.includes('/page') || lower.includes('.page.')) return 'šŸ“„ Page';
364
+ if (lower.includes('/component') || lower.includes('.component.')) return '🧩 Component';
365
+ if (lower.includes('/view') || lower.includes('.view.')) return 'šŸ‘ļø View';
366
+ if (lower.includes('/screen') || lower.includes('.screen.')) return 'šŸ“± Screen';
367
+ return 'šŸ“„ UI';
368
+ }
369
+
370
+ capitalizeFirst(str) {
371
+ return str.charAt(0).toUpperCase() + str.slice(1);
372
+ }
373
+ }