@sun-asterisk/impact-analyzer 1.0.4 → 1.0.6

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 (29) hide show
  1. package/.github/copilot-instructions.md +116 -0
  2. package/.github/prompts/README.md +91 -0
  3. package/.github/prompts/task-001-refactor.prompt.md +241 -0
  4. package/.specify/bugs/bug-001-database-detector.md +222 -0
  5. package/.specify/bugs/bug-002-database-detector.md +478 -0
  6. package/.specify/bugs/bug-003-multiline-detection.md +527 -0
  7. package/.specify/plans/architecture.md +186 -0
  8. package/.specify/specs/features/api-impact-detection.md +317 -0
  9. package/.specify/specs/features/component-impact-detection.md +263 -0
  10. package/.specify/specs/features/database-impact-detection.md +247 -0
  11. package/.specify/tasks/task-001-refactor-api-detector.md +284 -0
  12. package/.specify/tasks/task-002-database-detector.md +593 -0
  13. package/.specify/tasks/task-003-component-detector.md +0 -0
  14. package/.specify/tasks/task-004-report.md +484 -0
  15. package/README.md +13 -19
  16. package/core/detectors/database-detector.js +912 -0
  17. package/{modules → core}/detectors/endpoint-detector.js +11 -8
  18. package/{modules → core}/report-generator.js +102 -20
  19. package/core/utils/logger.js +12 -0
  20. package/index.js +6 -5
  21. package/package.json +1 -1
  22. package/modules/detectors/database-detector.js +0 -182
  23. /package/{modules → core}/change-detector.js +0 -0
  24. /package/{modules → core}/impact-analyzer.js +0 -0
  25. /package/{modules → core}/utils/ast-parser.js +0 -0
  26. /package/{modules → core}/utils/dependency-graph.js +0 -0
  27. /package/{modules → core}/utils/file-utils.js +0 -0
  28. /package/{modules → core}/utils/git-utils.js +0 -0
  29. /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
- 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}`);
@@ -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.table} (${d.operations.join(', ')})`);
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, 10).join(', ')}${d.fields.length > 10 ? '...' : ''}`);
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
- // Simple table summary
166
- lines.push('| Table | Operations | Fields |');
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 fieldsDisplay = db.fields && db.fields.length > 0
171
- ? db.fields.slice(0, 3).join(', ') + (db.fields.length > 3 ? '...' : '')
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 only if fields exist
179
- const tablesWithFields = dbImpacts.filter(db => db.fields && db.fields.length > 0);
177
+ // Detailed breakdown
178
+ lines.push('### Details\n');
180
179
 
181
- if (tablesWithFields.length > 0) {
182
- lines.push('### Field Details\n');
183
-
184
- tablesWithFields.forEach(db => {
185
- lines.push(`#### \`${db.table}\`\n`);
186
- lines.push(`**Operations:** ${db.operations.join(', ')}\n`);
187
- lines.push(`**Fields (${db.fields.length}):** ${db.fields.map(f => `\`${f}\``).join(', ')}\n`);
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 './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.4",
3
+ "version": "1.0.6",
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
File without changes