@sun-asterisk/impact-analyzer 1.0.3 → 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.
@@ -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
- if (this.config.verbose) {
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
- if (this.config.verbose) {
35
- console.log(`\n 🔍 Finding affected endpoints for ${allChangedMethods.length} changed methods...`);
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}`);
@@ -35,9 +35,14 @@ export class ReportGenerator {
35
35
  // Summary
36
36
  console.log('📊 SUMMARY:');
37
37
  console.log(` Files Changed: ${changes.changedFiles.length}`);
38
- console.log(` Symbols Modified: ${changes.changedSymbols.length}`);
39
38
  console.log(` Impact Score: ${impact.impactScore}`);
40
39
  console.log(` Severity: ${this.getSeverityEmoji(impact.severity)} ${impact.severity.toUpperCase()}`);
40
+ if (impact.affectedEndpoints.length > 0) {
41
+ console.log(` Affected Endpoints: ${impact.affectedEndpoints.length}`);
42
+ }
43
+ if (impact.databaseImpact.length > 0) {
44
+ console.log(` Database Tables: ${impact.databaseImpact.length}`);
45
+ }
41
46
  console.log('');
42
47
 
43
48
  // Endpoints
@@ -56,9 +61,10 @@ export class ReportGenerator {
56
61
  if (impact.databaseImpact.length > 0) {
57
62
  console.log('💾 DATABASE IMPACT:');
58
63
  impact.databaseImpact.forEach(d => {
59
- console.log(` • ${d.table} (${d.operations.join(', ')})`);
64
+ console.log(` • ${d.tableName} (${d.impactType}, ${d.severity})`);
65
+ console.log(` Operations: ${d.operations.join(', ')}`);
60
66
  if (d.fields && d.fields.length > 0) {
61
- console.log(` Fields: ${d.fields.slice(0, 10).join(', ')}${d.fields.length > 10 ? '...' : ''}`);
67
+ console.log(` Fields: ${d.fields.slice(0, 5).join(', ')}${d.fields.length > 5 ? '...' : ''}`);
62
68
  }
63
69
  });
64
70
  console.log('');
@@ -106,7 +112,8 @@ export class ReportGenerator {
106
112
  timestamp: new Date().toISOString(),
107
113
  summary: {
108
114
  filesChanged: changes.changedFiles.length,
109
- symbolsModified: changes.changedSymbols.length,
115
+ affectedEndpoints: impact.affectedEndpoints.length,
116
+ databaseTables: impact.databaseImpact.length,
110
117
  impactScore: impact.impactScore,
111
118
  severity: impact.severity,
112
119
  },
@@ -124,7 +131,8 @@ export class ReportGenerator {
124
131
  | Metric | Value |
125
132
  |--------|-------|
126
133
  | Files Changed | ${changes.changedFiles.length} |
127
- | Symbols Modified | ${changes.changedSymbols.length} |
134
+ | Affected Endpoints | ${impact.affectedEndpoints.length} |
135
+ | Database Tables | ${impact.databaseImpact.length} |
128
136
  | Impact Score | **${impact.impactScore}** |
129
137
  | Severity | ${this.getSeverityEmoji(impact.severity)} **${impact.severity.toUpperCase()}** |
130
138
 
@@ -155,31 +163,32 @@ export class ReportGenerator {
155
163
 
156
164
  lines.push(`**Total Tables Affected:** ${dbImpacts.length}\n`);
157
165
 
158
- // Simple table summary
159
- lines.push('| Table | Operations | Fields |');
160
- lines.push('|-------|------------|--------|');
166
+ // Summary table with new fields
167
+ lines.push('| Table | Model | Impact Type | Severity | Operations |');
168
+ lines.push('|-------|-------|-------------|----------|-----------|');
161
169
 
162
170
  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} |`);
171
+ const opsDisplay = db.operations.join(', ');
172
+ lines.push(`| \`${db.tableName}\` | ${db.modelName} | ${db.impactType} | ${db.severity} | ${opsDisplay} |`);
167
173
  });
168
174
 
169
175
  lines.push('');
170
176
 
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');
177
+ // Detailed breakdown
178
+ lines.push('### Details\n');
176
179
 
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
- }
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
+ });
183
192
 
184
193
  return lines.join('\n');
185
194
  }
@@ -370,4 +379,84 @@ export class ReportGenerator {
370
379
  capitalizeFirst(str) {
371
380
  return str.charAt(0).toUpperCase() + str.slice(1);
372
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
+ }
373
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
+ }
@@ -704,6 +704,7 @@ export class MethodCallGraph {
704
704
  affectedBy: changedMethod,
705
705
  callChain: callChain,
706
706
  layers: this.getCallChainLayers(callChain),
707
+ impactLevel: this.calculateImpactLevel(callChain),
707
708
  });
708
709
  }
709
710
  }
@@ -721,6 +722,7 @@ export class MethodCallGraph {
721
722
  affectedBy: changedMethod,
722
723
  callChain: [changedMethod],
723
724
  layers: [startLayer],
725
+ impactLevel: 'high', // Direct endpoint change is always high impact
724
726
  });
725
727
  }
726
728
 
@@ -793,6 +795,7 @@ export class MethodCallGraph {
793
795
  layers: [this.getMethodLayer(changedMethod), 'Command', this.getMethodLayer(endpointMethod)],
794
796
  viaCommand: commandName,
795
797
  endpointMethod: endpointMethod,
798
+ impactLevel: this.calculateImpactLevel([changedMethod, `Command: '${commandName}'`, endpointMethod]),
796
799
  });
797
800
  }
798
801
  }
@@ -924,4 +927,21 @@ export class MethodCallGraph {
924
927
  .reduce((sum, callers) => sum + callers.length, 0),
925
928
  };
926
929
  }
930
+
931
+ /**
932
+ * Calculate impact level based on call chain length
933
+ * Shorter chain = higher impact (closer to endpoint)
934
+ */
935
+ calculateImpactLevel(callChain) {
936
+ const chainLength = callChain.length;
937
+
938
+ // Direct endpoint change or very short chain
939
+ if (chainLength <= 1) return 'high';
940
+
941
+ // Short chain (2-3 hops)
942
+ if (chainLength <= 3) return 'medium';
943
+
944
+ // Longer chain
945
+ return 'low';
946
+ }
927
947
  }
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 './modules/change-detector.js';
11
- import { ImpactAnalyzer } from './modules/impact-analyzer.js';
12
- import { ReportGenerator } from './modules/report-generator.js';
13
- import { GitUtils } from './modules/utils/git-utils.js';
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
- fs.writeFileSync(jsonOutput, JSON.stringify({ changes, impact }, null, 2));
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/impact-analyzer",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Automated impact analysis for TypeScript/JavaScript projects",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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