@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.
- package/README.md +506 -0
- package/cli.js +38 -0
- package/config/default-config.js +56 -0
- package/index.js +128 -0
- package/modules/change-detector.js +258 -0
- package/modules/detectors/database-detector.js +182 -0
- package/modules/detectors/endpoint-detector.js +52 -0
- package/modules/impact-analyzer.js +124 -0
- package/modules/report-generator.js +373 -0
- package/modules/utils/ast-parser.js +241 -0
- package/modules/utils/dependency-graph.js +159 -0
- package/modules/utils/file-utils.js +116 -0
- package/modules/utils/git-utils.js +198 -0
- package/modules/utils/method-call-graph.js +952 -0
- package/package.json +26 -0
- package/run-impact-analysis.sh +124 -0
|
@@ -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
|
+
}
|