@sun-asterisk/impact-analyzer 1.0.4 → 1.0.5
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/.github/copilot-instructions.md +116 -0
- package/.github/prompts/README.md +91 -0
- package/.github/prompts/task-001-refactor.prompt.md +241 -0
- package/.specify/bugs/bug-001-database-detector.md +222 -0
- package/.specify/plans/architecture.md +186 -0
- package/.specify/specs/features/api-impact-detection.md +317 -0
- package/.specify/specs/features/component-impact-detection.md +263 -0
- package/.specify/specs/features/database-impact-detection.md +247 -0
- package/.specify/tasks/task-001-refactor-api-detector.md +284 -0
- package/.specify/tasks/task-002-database-detector.md +593 -0
- package/.specify/tasks/task-003-component-detector.md +0 -0
- package/.specify/tasks/task-004-report.md +484 -0
- package/README.md +13 -19
- package/core/detectors/database-detector.js +702 -0
- package/{modules → core}/detectors/endpoint-detector.js +11 -8
- package/{modules → core}/report-generator.js +102 -20
- package/core/utils/logger.js +12 -0
- package/index.js +6 -5
- package/package.json +1 -1
- package/modules/detectors/database-detector.js +0 -182
- /package/{modules → core}/change-detector.js +0 -0
- /package/{modules → core}/impact-analyzer.js +0 -0
- /package/{modules → core}/utils/ast-parser.js +0 -0
- /package/{modules → core}/utils/dependency-graph.js +0 -0
- /package/{modules → core}/utils/file-utils.js +0 -0
- /package/{modules → core}/utils/git-utils.js +0 -0
- /package/{modules → core}/utils/method-call-graph.js +0 -0
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Endpoint Impact Detector
|
|
3
|
-
* Detects affected API endpoints using method-level call graph
|
|
3
|
+
* Detects affected API endpoints using method-level call graph with performance optimization
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { createLogger } from '../utils/logger.js';
|
|
7
|
+
|
|
6
8
|
export class EndpointDetector {
|
|
7
9
|
constructor(methodCallGraph, config) {
|
|
8
10
|
this.methodCallGraph = methodCallGraph;
|
|
9
11
|
this.config = config;
|
|
12
|
+
this.logger = createLogger(config.verbose);
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Detect affected endpoints from changed files
|
|
14
17
|
*/
|
|
15
18
|
async detect(changedFiles) {
|
|
19
|
+
const startTime = Date.now();
|
|
16
20
|
const allChangedMethods = [];
|
|
17
21
|
|
|
18
22
|
for (const changedFile of changedFiles) {
|
|
@@ -25,19 +29,18 @@ export class EndpointDetector {
|
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
if (allChangedMethods.length === 0) {
|
|
28
|
-
|
|
29
|
-
console.log(' ℹ️ No method-level changes detected');
|
|
30
|
-
}
|
|
32
|
+
this.logger.info(' ℹ️ No method-level changes detected');
|
|
31
33
|
return [];
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
console.log(` Call graph size: ${this.methodCallGraph.methodCallMap.size} entries`);
|
|
37
|
-
}
|
|
36
|
+
this.logger.info(`\n 🔍 Finding affected endpoints for ${allChangedMethods.length} changed methods...`);
|
|
37
|
+
this.logger.verbose('EndpointDetector', `Call graph size: ${this.methodCallGraph.methodCallMap.size} entries`);
|
|
38
38
|
|
|
39
39
|
const affectedEndpoints = this.methodCallGraph.findAffectedEndpoints(allChangedMethods);
|
|
40
40
|
|
|
41
|
+
const duration = Date.now() - startTime;
|
|
42
|
+
this.logger.verbose('EndpointDetector', `Completed in ${duration}ms`);
|
|
43
|
+
|
|
41
44
|
if (this.config.verbose) {
|
|
42
45
|
for (const endpoint of affectedEndpoints) {
|
|
43
46
|
console.log(`\n 📍 Analyzing: ${endpoint.affectedBy}`);
|
|
@@ -61,9 +61,10 @@ export class ReportGenerator {
|
|
|
61
61
|
if (impact.databaseImpact.length > 0) {
|
|
62
62
|
console.log('💾 DATABASE IMPACT:');
|
|
63
63
|
impact.databaseImpact.forEach(d => {
|
|
64
|
-
console.log(` • ${d.
|
|
64
|
+
console.log(` • ${d.tableName} (${d.impactType}, ${d.severity})`);
|
|
65
|
+
console.log(` Operations: ${d.operations.join(', ')}`);
|
|
65
66
|
if (d.fields && d.fields.length > 0) {
|
|
66
|
-
console.log(` Fields: ${d.fields.slice(0,
|
|
67
|
+
console.log(` Fields: ${d.fields.slice(0, 5).join(', ')}${d.fields.length > 5 ? '...' : ''}`);
|
|
67
68
|
}
|
|
68
69
|
});
|
|
69
70
|
console.log('');
|
|
@@ -162,31 +163,32 @@ export class ReportGenerator {
|
|
|
162
163
|
|
|
163
164
|
lines.push(`**Total Tables Affected:** ${dbImpacts.length}\n`);
|
|
164
165
|
|
|
165
|
-
//
|
|
166
|
-
lines.push('| Table |
|
|
167
|
-
lines.push('
|
|
166
|
+
// Summary table with new fields
|
|
167
|
+
lines.push('| Table | Model | Impact Type | Severity | Operations |');
|
|
168
|
+
lines.push('|-------|-------|-------------|----------|-----------|');
|
|
168
169
|
|
|
169
170
|
dbImpacts.forEach(db => {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
: '-';
|
|
173
|
-
lines.push(`| \`${db.table}\` | ${db.operations.join(', ')} | ${fieldsDisplay} |`);
|
|
171
|
+
const opsDisplay = db.operations.join(', ');
|
|
172
|
+
lines.push(`| \`${db.tableName}\` | ${db.modelName} | ${db.impactType} | ${db.severity} | ${opsDisplay} |`);
|
|
174
173
|
});
|
|
175
174
|
|
|
176
175
|
lines.push('');
|
|
177
176
|
|
|
178
|
-
// Detailed breakdown
|
|
179
|
-
|
|
177
|
+
// Detailed breakdown
|
|
178
|
+
lines.push('### Details\n');
|
|
180
179
|
|
|
181
|
-
|
|
182
|
-
lines.push(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
180
|
+
dbImpacts.forEach(db => {
|
|
181
|
+
lines.push(`#### \`${db.tableName}\` (${db.modelName})\n`);
|
|
182
|
+
lines.push(`- **Impact Type:** ${db.impactType}`);
|
|
183
|
+
lines.push(`- **Severity:** ${db.severity}`);
|
|
184
|
+
lines.push(`- **Operations:** ${db.operations.join(', ')}`);
|
|
185
|
+
lines.push(`- **Entity File:** ${db.modelPath}`);
|
|
186
|
+
|
|
187
|
+
if (db.fields && db.fields.length > 0) {
|
|
188
|
+
lines.push(`- **Affected Fields:** ${db.fields.map(f => `\`${f}\``).join(', ')}`);
|
|
189
|
+
}
|
|
190
|
+
lines.push('');
|
|
191
|
+
});
|
|
190
192
|
|
|
191
193
|
return lines.join('\n');
|
|
192
194
|
}
|
|
@@ -377,4 +379,84 @@ export class ReportGenerator {
|
|
|
377
379
|
capitalizeFirst(str) {
|
|
378
380
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
379
381
|
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Generate clean JSON report suitable for CI/CD
|
|
385
|
+
* Excludes source code, diffs, and internal data structures
|
|
386
|
+
*/
|
|
387
|
+
generateJSONReport(changes, impact) {
|
|
388
|
+
return {
|
|
389
|
+
metadata: {
|
|
390
|
+
timestamp: new Date().toISOString(),
|
|
391
|
+
filesChanged: changes.changedFiles.length,
|
|
392
|
+
symbolsChanged: changes.changedSymbols?.length || 0,
|
|
393
|
+
},
|
|
394
|
+
summary: {
|
|
395
|
+
impactScore: impact.impactScore,
|
|
396
|
+
severity: impact.severity,
|
|
397
|
+
affectedEndpoints: impact.affectedEndpoints?.length || 0,
|
|
398
|
+
affectedTables: impact.databaseImpact?.length || 0,
|
|
399
|
+
affectedComponents: this.countComponents(changes.changedFiles),
|
|
400
|
+
},
|
|
401
|
+
endpoints: this.formatEndpointsForJSON(impact.affectedEndpoints),
|
|
402
|
+
database: this.formatDatabaseForJSON(impact.databaseImpact),
|
|
403
|
+
components: this.formatComponentsForJSON(changes.changedFiles),
|
|
404
|
+
logic: {
|
|
405
|
+
riskLevel: impact.logicImpact?.riskLevel || 'low',
|
|
406
|
+
directCallers: impact.logicImpact?.directCallers?.length || 0,
|
|
407
|
+
indirectCallers: impact.logicImpact?.indirectCallers?.length || 0,
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
formatEndpointsForJSON(endpoints) {
|
|
413
|
+
if (!endpoints || endpoints.length === 0) return [];
|
|
414
|
+
|
|
415
|
+
return endpoints.map(e => ({
|
|
416
|
+
method: e.method,
|
|
417
|
+
path: e.path,
|
|
418
|
+
controller: e.controller,
|
|
419
|
+
impactLevel: e.impactLevel,
|
|
420
|
+
layers: e.layers,
|
|
421
|
+
}));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
formatDatabaseForJSON(dbImpacts) {
|
|
425
|
+
if (!dbImpacts || dbImpacts.length === 0) return [];
|
|
426
|
+
|
|
427
|
+
return dbImpacts.map(db => ({
|
|
428
|
+
tableName: db.tableName,
|
|
429
|
+
modelName: db.modelName,
|
|
430
|
+
modelPath: db.modelPath,
|
|
431
|
+
impactType: db.impactType,
|
|
432
|
+
severity: db.severity,
|
|
433
|
+
operations: db.operations,
|
|
434
|
+
fields: db.fields || [],
|
|
435
|
+
}));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
formatComponentsForJSON(changedFiles) {
|
|
439
|
+
const components = changedFiles.filter(f => this.isComponentFile(f));
|
|
440
|
+
|
|
441
|
+
return components.map(c => ({
|
|
442
|
+
path: c.path,
|
|
443
|
+
type: this.detectPageType(c.path),
|
|
444
|
+
status: c.status,
|
|
445
|
+
linesAdded: c.changes?.added || 0,
|
|
446
|
+
linesDeleted: c.changes?.deleted || 0,
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
isComponentFile(file) {
|
|
451
|
+
const path = file.path.toLowerCase();
|
|
452
|
+
return (
|
|
453
|
+
path.includes('/page') || path.includes('/component') ||
|
|
454
|
+
path.includes('/view') || path.includes('/screen') ||
|
|
455
|
+
path.includes('.page.') || path.includes('.component.')
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
countComponents(changedFiles) {
|
|
460
|
+
return changedFiles.filter(f => this.isComponentFile(f)).length;
|
|
461
|
+
}
|
|
380
462
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Utility
|
|
3
|
+
* Provides structured logging with verbose mode support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function createLogger(verbose = false) {
|
|
7
|
+
return {
|
|
8
|
+
info: (msg) => console.log(msg),
|
|
9
|
+
debug: (msg) => verbose && console.log(`[DEBUG] ${msg}`),
|
|
10
|
+
verbose: (component, msg) => verbose && console.log(`[${component}] ${msg}`)
|
|
11
|
+
};
|
|
12
|
+
}
|
package/index.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { CLI } from './cli.js';
|
|
9
9
|
import { loadConfig } from './config/default-config.js';
|
|
10
|
-
import { ChangeDetector } from './
|
|
11
|
-
import { ImpactAnalyzer } from './
|
|
12
|
-
import { ReportGenerator } from './
|
|
13
|
-
import { GitUtils } from './
|
|
10
|
+
import { ChangeDetector } from './core/change-detector.js';
|
|
11
|
+
import { ImpactAnalyzer } from './core/impact-analyzer.js';
|
|
12
|
+
import { ReportGenerator } from './core/report-generator.js';
|
|
13
|
+
import { GitUtils } from './core/utils/git-utils.js';
|
|
14
14
|
import fs from 'fs';
|
|
15
15
|
import path from 'path';
|
|
16
16
|
|
|
@@ -102,7 +102,8 @@ async function main() {
|
|
|
102
102
|
// JSON output (optional)
|
|
103
103
|
if (cli.hasArg('json')) {
|
|
104
104
|
const jsonOutput = cli.getArg('json');
|
|
105
|
-
|
|
105
|
+
const jsonReport = reporter.generateJSONReport(changes, impact);
|
|
106
|
+
fs.writeFileSync(jsonOutput, JSON.stringify(jsonReport, null, 2));
|
|
106
107
|
console.log(`✅ JSON report saved to: ${jsonOutput}\n`);
|
|
107
108
|
}
|
|
108
109
|
|
package/package.json
CHANGED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Database Impact Detector
|
|
3
|
-
* Detects database changes using layer-aware method tracking
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import path from 'path';
|
|
7
|
-
|
|
8
|
-
export class DatabaseDetector {
|
|
9
|
-
constructor(methodCallGraph, config) {
|
|
10
|
-
this.methodCallGraph = methodCallGraph;
|
|
11
|
-
this.config = config;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Detect database impact from changed files using layer-aware tracking
|
|
16
|
-
*/
|
|
17
|
-
async detect(changedFiles) {
|
|
18
|
-
const databaseChanges = {
|
|
19
|
-
tables: new Set(),
|
|
20
|
-
fields: new Map(), // table -> Set of fields
|
|
21
|
-
operations: new Map(), // table -> Set of operations
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// Extract changed methods
|
|
25
|
-
const allChangedMethods = [];
|
|
26
|
-
for (const changedFile of changedFiles) {
|
|
27
|
-
const changedMethods = this.methodCallGraph.getChangedMethods(
|
|
28
|
-
changedFile.diff || '',
|
|
29
|
-
changedFile.absolutePath
|
|
30
|
-
);
|
|
31
|
-
allChangedMethods.push(...changedMethods);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (this.config.verbose) {
|
|
35
|
-
console.log(`\n 🔍 Analyzing database impact for ${allChangedMethods.length} changed methods`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Find affected repository methods through call graph
|
|
39
|
-
const affectedRepoMethods = this.findAffectedRepositoryMethods(allChangedMethods);
|
|
40
|
-
|
|
41
|
-
if (this.config.verbose) {
|
|
42
|
-
console.log(`\n 📊 Found ${affectedRepoMethods.length} affected repository methods`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Analyze repository methods for database operations
|
|
46
|
-
for (const repoMethod of affectedRepoMethods) {
|
|
47
|
-
const dbOps = this.extractDatabaseOperationsFromCallGraph(repoMethod);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
for (const op of dbOps) {
|
|
52
|
-
databaseChanges.tables.add(op.table);
|
|
53
|
-
|
|
54
|
-
if (!databaseChanges.operations.has(op.table)) {
|
|
55
|
-
databaseChanges.operations.set(op.table, new Set());
|
|
56
|
-
}
|
|
57
|
-
databaseChanges.operations.get(op.table).add(op.operation);
|
|
58
|
-
|
|
59
|
-
if (op.fields && op.fields.length > 0) {
|
|
60
|
-
if (!databaseChanges.fields.has(op.table)) {
|
|
61
|
-
databaseChanges.fields.set(op.table, new Set());
|
|
62
|
-
}
|
|
63
|
-
op.fields.forEach(field => databaseChanges.fields.get(op.table).add(field));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return this.formatDatabaseImpact(databaseChanges);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
findAffectedRepositoryMethods(changedMethods) {
|
|
72
|
-
const visited = new Set();
|
|
73
|
-
const repoMethods = [];
|
|
74
|
-
const queue = [...changedMethods];
|
|
75
|
-
|
|
76
|
-
while (queue.length > 0) {
|
|
77
|
-
const method = queue.shift();
|
|
78
|
-
|
|
79
|
-
if (!method || !method.file) continue;
|
|
80
|
-
|
|
81
|
-
const key = `${method.file}:${method.className}.${method.methodName}`;
|
|
82
|
-
if (visited.has(key)) continue;
|
|
83
|
-
visited.add(key);
|
|
84
|
-
|
|
85
|
-
const isRepository = method.file.includes('repository') ||
|
|
86
|
-
(method.className && method.className.toLowerCase().includes('repository'));
|
|
87
|
-
|
|
88
|
-
if (isRepository) {
|
|
89
|
-
repoMethods.push(method);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const callers = this.methodCallGraph.getCallers(method);
|
|
93
|
-
queue.push(...callers);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return repoMethods;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
extractDatabaseOperationsFromCallGraph(repoMethod) {
|
|
100
|
-
const operations = [];
|
|
101
|
-
|
|
102
|
-
const methodKey = `${repoMethod.className}.${repoMethod.methodName}`;
|
|
103
|
-
const methodCalls = this.methodCallGraph.methodCallsMap?.get(methodKey) || [];
|
|
104
|
-
|
|
105
|
-
if (this.config.verbose) {
|
|
106
|
-
console.log(` 📞 Analyzing ${methodCalls.length} calls in ${repoMethod.className}.${repoMethod.methodName}`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
for (const call of methodCalls) {
|
|
110
|
-
const dbOp = this.detectDatabaseOperationFromCall(call);
|
|
111
|
-
|
|
112
|
-
if (dbOp) {
|
|
113
|
-
operations.push({
|
|
114
|
-
...dbOp,
|
|
115
|
-
method: `${repoMethod.className}.${repoMethod.methodName}`,
|
|
116
|
-
file: repoMethod.file,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return operations;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
detectDatabaseOperationFromCall(call) {
|
|
125
|
-
const typeOrmOps = {
|
|
126
|
-
'insert': 'INSERT',
|
|
127
|
-
'save': 'INSERT/UPDATE',
|
|
128
|
-
'create': 'INSERT',
|
|
129
|
-
'update': 'UPDATE',
|
|
130
|
-
'merge': 'UPDATE',
|
|
131
|
-
'delete': 'DELETE',
|
|
132
|
-
'remove': 'DELETE',
|
|
133
|
-
'softDelete': 'SOFT_DELETE',
|
|
134
|
-
'find': 'SELECT',
|
|
135
|
-
'findOne': 'SELECT',
|
|
136
|
-
'findAndCount': 'SELECT',
|
|
137
|
-
'findBy': 'SELECT',
|
|
138
|
-
'findOneBy': 'SELECT',
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const methodName = call.method;
|
|
142
|
-
if (!typeOrmOps[methodName]) return null;
|
|
143
|
-
|
|
144
|
-
const target = call.target;
|
|
145
|
-
const entityMatch = target?.match(/(\w+Repository)/);
|
|
146
|
-
|
|
147
|
-
if (!entityMatch) return null;
|
|
148
|
-
|
|
149
|
-
const entityName = entityMatch[1].replace(/Repository$/, '') + 'Entity';
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
table: this.entityToTableName(entityName),
|
|
153
|
-
operation: typeOrmOps[methodName],
|
|
154
|
-
fields: [],
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
entityToTableName(entityName) {
|
|
159
|
-
return entityName
|
|
160
|
-
.replace(/Entity$/, '')
|
|
161
|
-
.replace(/([A-Z])/g, '_$1')
|
|
162
|
-
.toLowerCase()
|
|
163
|
-
.replace(/^_/, '');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
formatDatabaseImpact(databaseChanges) {
|
|
167
|
-
const tables = [];
|
|
168
|
-
|
|
169
|
-
for (const tableName of databaseChanges.tables) {
|
|
170
|
-
const operations = Array.from(databaseChanges.operations.get(tableName) || []);
|
|
171
|
-
const fields = Array.from(databaseChanges.fields.get(tableName) || []);
|
|
172
|
-
|
|
173
|
-
tables.push({
|
|
174
|
-
name: tableName,
|
|
175
|
-
operations,
|
|
176
|
-
fields: fields.length > 0 ? fields : undefined,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return tables;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|