impact-analysis 2.0.0 → 2.0.2
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 +9 -1
- package/dist/ai/explain.js +29 -0
- package/dist/cache/graphCache.js +39 -0
- package/dist/cli.js +136 -0
- package/dist/core/analyzer.js +20 -0
- package/dist/git/getChangedFiles.js +21 -0
- package/{src/graph/buildGraph.ts → dist/graph/buildGraph.js} +44 -63
- package/dist/graph/types.js +2 -0
- package/dist/parser/parseJS.js +70 -0
- package/dist/parser/parseVue.js +24 -0
- package/dist/parser/parseVueTemplate.js +15 -0
- package/{src/report/html-generator.ts → dist/report/html-generator.js} +17 -19
- package/dist/scanner/scanRepo.js +30 -0
- package/package.json +32 -1
- package/git/core/analyzer.ts +0 -28
- package/git/getChangedFiles.ts +0 -15
- package/index.mjs +0 -457
- package/src/ai/explain.ts +0 -29
- package/src/cache/graphCache.ts +0 -49
- package/src/cli.ts +0 -110
- package/src/graph/types.ts +0 -5
- package/src/parser/parseJS.ts +0 -69
- package/src/parser/parseVue.ts +0 -23
- package/src/parser/parseVueTemplate.ts +0 -16
- package/src/scanner/scanRepo.ts +0 -25
- package/tsconfig.json +0 -10
package/package.json
CHANGED
|
@@ -1,17 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "impact-analysis",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Analyze real impact of code changes using dependency graphs",
|
|
5
5
|
"bin": {
|
|
6
6
|
"impact-analysis": "dist/cli.js"
|
|
7
7
|
},
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
10
15
|
"scripts": {
|
|
11
16
|
"build": "tsc",
|
|
12
17
|
"dev": "ts-node src/cli.ts"
|
|
13
18
|
},
|
|
14
19
|
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/sushant3694/impact-analysis.git"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"impact-analysis",
|
|
26
|
+
"dependency-graph",
|
|
27
|
+
"code-analysis",
|
|
28
|
+
"static-analysis",
|
|
29
|
+
"react",
|
|
30
|
+
"vue",
|
|
31
|
+
"angular",
|
|
32
|
+
"typescript",
|
|
33
|
+
"javascript",
|
|
34
|
+
"ast",
|
|
35
|
+
"ast-parser",
|
|
36
|
+
"code-impact",
|
|
37
|
+
"dependency-tracker",
|
|
38
|
+
"git-diff",
|
|
39
|
+
"change-analysis",
|
|
40
|
+
"impact-tracker",
|
|
41
|
+
"code-quality",
|
|
42
|
+
"devtools"
|
|
43
|
+
],
|
|
44
|
+
"author": "sushant3694",
|
|
15
45
|
"dependencies": {
|
|
16
46
|
"@babel/parser": "^7.24.0",
|
|
17
47
|
"@babel/traverse": "^7.24.0",
|
|
@@ -23,6 +53,7 @@
|
|
|
23
53
|
"simple-git": "^3.22.0"
|
|
24
54
|
},
|
|
25
55
|
"devDependencies": {
|
|
56
|
+
"@types/babel__traverse": "^7.28.0",
|
|
26
57
|
"@types/node": "^20.11.0",
|
|
27
58
|
"parse-json": "^8.3.0",
|
|
28
59
|
"ts-node": "^10.9.2",
|
package/git/core/analyzer.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
|
|
3
|
-
export interface ImpactResult {
|
|
4
|
-
changedFile: string;
|
|
5
|
-
impactedFiles: string[];
|
|
6
|
-
risk: 'LOW' | 'MEDIUM' | 'HIGH';
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function analyzeImpact(
|
|
10
|
-
changedFiles: string[],
|
|
11
|
-
dependencyMap: Record<string, string[]>
|
|
12
|
-
): ImpactResult[] {
|
|
13
|
-
return changedFiles.map(changed => {
|
|
14
|
-
const impacted = Object.entries(dependencyMap)
|
|
15
|
-
.filter(([, deps]) => deps.includes(changed))
|
|
16
|
-
.map(([file]) => file);
|
|
17
|
-
|
|
18
|
-
let risk: ImpactResult['risk'] = 'LOW';
|
|
19
|
-
if (impacted.length > 5) risk = 'HIGH';
|
|
20
|
-
else if (impacted.length > 2) risk = 'MEDIUM';
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
changedFile: changed,
|
|
24
|
-
impactedFiles: impacted,
|
|
25
|
-
risk
|
|
26
|
-
};
|
|
27
|
-
});
|
|
28
|
-
}
|
package/git/getChangedFiles.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import simpleGit from "simple-git";
|
|
2
|
-
import path from "path";
|
|
3
|
-
|
|
4
|
-
export async function getChangedFiles(base: string): Promise<string[]> {
|
|
5
|
-
try {
|
|
6
|
-
const git = simpleGit();
|
|
7
|
-
const diff = await git.diff(["--name-only", base]);
|
|
8
|
-
return diff
|
|
9
|
-
.split("\n")
|
|
10
|
-
.filter(f => /\.(js|jsx|ts|tsx|vue)$/.test(f))
|
|
11
|
-
.map(f => path.resolve(process.cwd(), f));
|
|
12
|
-
} catch (error) {
|
|
13
|
-
throw new Error(`Failed to get changed files. Make sure Git is installed and you're in a Git repository. Base branch: ${base}`);
|
|
14
|
-
}
|
|
15
|
-
}
|
package/index.mjs
DELETED
|
@@ -1,457 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import simpleGit from 'simple-git';
|
|
5
|
-
|
|
6
|
-
const git = simpleGit();
|
|
7
|
-
|
|
8
|
-
function readFile(filePath) {
|
|
9
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function extractDependencies(content, filePath, baseDir) {
|
|
13
|
-
const dependencies = [];
|
|
14
|
-
const importRegex = /import\s+.*\s+from\s+['"]([^'"]+)['"]/g;
|
|
15
|
-
|
|
16
|
-
let match;
|
|
17
|
-
while ((match = importRegex.exec(content))) {
|
|
18
|
-
const importPath = match[1];
|
|
19
|
-
const resolvedPath = resolveImportPath(importPath, filePath, baseDir);
|
|
20
|
-
if (resolvedPath) {
|
|
21
|
-
dependencies.push(resolvedPath);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return dependencies;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function resolveImportPath(importPath, filePath, baseDir) {
|
|
29
|
-
if (importPath.startsWith('@/')) {
|
|
30
|
-
return path.resolve(baseDir, importPath.substring(2));
|
|
31
|
-
}
|
|
32
|
-
if (importPath.startsWith('./') || importPath.startsWith('../')) {
|
|
33
|
-
return path.resolve(path.dirname(filePath), importPath);
|
|
34
|
-
}
|
|
35
|
-
const absolutePath = path.resolve(baseDir, importPath);
|
|
36
|
-
if (fs.existsSync(absolutePath)) {
|
|
37
|
-
return absolutePath;
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
43
|
-
const files = fs.readdirSync(dirPath);
|
|
44
|
-
|
|
45
|
-
files.forEach(file => {
|
|
46
|
-
const fullPath = path.join(dirPath, file);
|
|
47
|
-
if (fs.statSync(fullPath).isDirectory()) {
|
|
48
|
-
getAllFiles(fullPath, arrayOfFiles);
|
|
49
|
-
} else if (/\.(js|ts|vue)$/.test(file)) {
|
|
50
|
-
arrayOfFiles.push(fullPath);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
return arrayOfFiles;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function buildDependencyMap(baseDir) {
|
|
58
|
-
const dependencyMap = {};
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const allFiles = getAllFiles(baseDir);
|
|
62
|
-
|
|
63
|
-
for (const file of allFiles) {
|
|
64
|
-
const code = readFile(file);
|
|
65
|
-
const dependencies = extractDependencies(code, file, baseDir);
|
|
66
|
-
dependencyMap[file] = dependencies;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return dependencyMap;
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.error('Error building dependency map:', error);
|
|
72
|
-
throw error;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function generateImpactList(baseDir) {
|
|
77
|
-
const impactMap = {};
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
const dependencyMap = await buildDependencyMap(baseDir);
|
|
81
|
-
|
|
82
|
-
const status = await git.status();
|
|
83
|
-
const changedFiles = status.modified
|
|
84
|
-
.map(file => path.resolve(baseDir, file))
|
|
85
|
-
.filter(file => /\.(js|ts|vue)$/.test(file));
|
|
86
|
-
|
|
87
|
-
const reverseDependencyMap = {};
|
|
88
|
-
Object.keys(dependencyMap).forEach(file => {
|
|
89
|
-
dependencyMap[file].forEach(importPath => {
|
|
90
|
-
if (!reverseDependencyMap[importPath]) {
|
|
91
|
-
reverseDependencyMap[importPath] = new Set();
|
|
92
|
-
}
|
|
93
|
-
reverseDependencyMap[importPath].add(file);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
changedFiles.forEach(changedFile => {
|
|
97
|
-
impactMap[changedFile] = [];
|
|
98
|
-
for (const [file, dependencies] of Object.entries(dependencyMap)) {
|
|
99
|
-
if (dependencies.includes(changedFile)) {
|
|
100
|
-
impactMap[changedFile].push(file);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
const createImpactJson = (changedFiles, dependencyMap) => {
|
|
105
|
-
const result = {};
|
|
106
|
-
|
|
107
|
-
const getLastPart = (path) => path.split('\\').pop().split('/').pop().replace(/\.[^/.]+$/, "");
|
|
108
|
-
|
|
109
|
-
changedFiles.forEach(changedFile => {
|
|
110
|
-
const changedFileName = getLastPart(changedFile);
|
|
111
|
-
result[changedFile] = [];
|
|
112
|
-
|
|
113
|
-
for (const [file, dependencies] of Object.entries(dependencyMap)) {
|
|
114
|
-
if (dependencies.some(dep => getLastPart(dep) === changedFileName)) {
|
|
115
|
-
if (!result[changedFile].includes(file)) {
|
|
116
|
-
result[changedFile].push(file);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
return Object.entries(result).map(([changedFile, impactsTo]) => ({
|
|
123
|
-
changedFile,
|
|
124
|
-
impactsTo
|
|
125
|
-
}));
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const impactJson = createImpactJson(changedFiles, dependencyMap);
|
|
129
|
-
return impactJson;
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.error('Error generating impact list:', error);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const baseDir = process.cwd();
|
|
136
|
-
const dependencyJson = await generateImpactList(baseDir);
|
|
137
|
-
let results = dependencyJson;
|
|
138
|
-
function generateGraphData(dependencyData) {
|
|
139
|
-
const nodes = new Map();
|
|
140
|
-
const links = [];
|
|
141
|
-
|
|
142
|
-
dependencyData.forEach(({ changedFile, impactsTo }) => {
|
|
143
|
-
const changedFileName = path.basename(changedFile);
|
|
144
|
-
if (!nodes.has(changedFile)) {
|
|
145
|
-
nodes.set(changedFile, { id: changedFile, name: changedFileName, isChangedFile: true });
|
|
146
|
-
}
|
|
147
|
-
impactsTo.forEach(impactedFile => {
|
|
148
|
-
const impactedFileName = path.basename(impactedFile);
|
|
149
|
-
if (!nodes.has(impactedFile)) {
|
|
150
|
-
nodes.set(impactedFile, { id: impactedFileName, path: impactedFile, isChangedFile: false });
|
|
151
|
-
}
|
|
152
|
-
links.push({ source: changedFile, target: impactedFileName });
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
nodes: Array.from(nodes.values()),
|
|
158
|
-
links
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const graphData = generateGraphData(results);
|
|
163
|
-
|
|
164
|
-
const htmlContent = `
|
|
165
|
-
<!DOCTYPE html>
|
|
166
|
-
<html lang="en">
|
|
167
|
-
<head>
|
|
168
|
-
<meta charset="UTF-8">
|
|
169
|
-
<title>Impact Analysis</title>
|
|
170
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
|
|
171
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
172
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
173
|
-
<link href="https://fonts.googleapis.com/css2?family=Exo+2:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
|
174
|
-
<style>
|
|
175
|
-
body { font-family: "Exo 2", serif; background-color: #282828; color: #fff; overflow-x: hidden;
|
|
176
|
-
font-optical-sizing: auto; margin: 0; padding: 0; text-align: center; font-weight: 400;}
|
|
177
|
-
svg { width: 100%; height: 100vh; }
|
|
178
|
-
circle { cursor: pointer; }
|
|
179
|
-
text { font-size: 12px; fill: #fff; }
|
|
180
|
-
line { stroke: #fefefe; stroke-opacity: 0.6; }
|
|
181
|
-
#tooltip { position: absolute; background: #fff;color: #000; padding: 8px; border: 1px solid #ddd; display: none; }
|
|
182
|
-
.svg-wrapper{ padding: 10px; }
|
|
183
|
-
.impact-file-list {
|
|
184
|
-
display: list-item;
|
|
185
|
-
list-style: number;
|
|
186
|
-
text-align: left;
|
|
187
|
-
padding-left: 7px;
|
|
188
|
-
}
|
|
189
|
-
.impact-analysis-table {
|
|
190
|
-
width: 100%; border-collapse: collapse;
|
|
191
|
-
}
|
|
192
|
-
.impact-analysis-table td {
|
|
193
|
-
padding: 10px 20px;
|
|
194
|
-
}
|
|
195
|
-
th {
|
|
196
|
-
padding: 10px;
|
|
197
|
-
}
|
|
198
|
-
td:first-child {
|
|
199
|
-
background: #91fd9117;
|
|
200
|
-
}
|
|
201
|
-
.btn-theme {
|
|
202
|
-
min-width: 142px;
|
|
203
|
-
padding: 12px 16px;
|
|
204
|
-
font-size: 14px;
|
|
205
|
-
border-radius: 100px;
|
|
206
|
-
border: 1px solid #565454;
|
|
207
|
-
outline: none;
|
|
208
|
-
box-shadow: none;
|
|
209
|
-
cursor: pointer;
|
|
210
|
-
transition: all .3s ease-in-out;
|
|
211
|
-
text-decoration: none;
|
|
212
|
-
display: inline-flex;
|
|
213
|
-
align-items: center;
|
|
214
|
-
font-family: "Exo 2", serif;
|
|
215
|
-
background: #222;
|
|
216
|
-
color: #fff;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
.btn-active {
|
|
220
|
-
background: #1f4afe;
|
|
221
|
-
background: linear-gradient(90deg,#1f4afe 35%,#167cfc);
|
|
222
|
-
color: #fff
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
.btn-active:hover svg path {
|
|
226
|
-
fill: #1f4afe;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
.btn-theme:hover {
|
|
230
|
-
background: #fff;
|
|
231
|
-
color: #1f4afe;
|
|
232
|
-
border: 1px solid #1F4AFE
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
.btn-svg {
|
|
236
|
-
width: 20px;
|
|
237
|
-
height: 20px;
|
|
238
|
-
margin-right: 10px;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
.btn-svg path {
|
|
242
|
-
fill: #fefefe;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
th {
|
|
246
|
-
background: #1f4afe;
|
|
247
|
-
background: linear-gradient(90deg,#1f4afe 35%,#167cfc);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
th:last-child {
|
|
251
|
-
background: #1f4afe;
|
|
252
|
-
background: linear-gradient(-90deg,#1f4afe 35%,#167cfc);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
</style>
|
|
256
|
-
</head>
|
|
257
|
-
<body>
|
|
258
|
-
<h1>Impact Analysis</h1>
|
|
259
|
-
<div id="tooltip"></div>
|
|
260
|
-
<div style="text-align: center; margin-bottom: 10px;">
|
|
261
|
-
<button id="graphViewBtn" class="btn-theme btn-active"><svg class="btn-svg" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
|
|
262
|
-
<g><g><path fill="#000000" d="M224.5,7.9c-11.8,0-21.5,9.6-21.5,21.5c0,7.4,3.8,14,9.6,17.9l-40.5,106.9c-1.1-0.2-2.3-0.3-3.4-0.3c-5.2,0-9.8,1.9-13.5,4.9l-40.6-33.5c1.6-3,2.6-6.4,2.6-10c0-11.8-9.6-21.5-21.5-21.5c-11.8,0-21.5,9.6-21.5,21.5c0,6.4,2.9,12.1,7.3,16l-43.3,75.3c-2.2-0.7-4.5-1.3-6.9-1.3c-11.8,0-21.5,9.6-21.5,21.5c0,11.8,9.6,21.5,21.5,21.5c11.8,0,21.5-9.6,21.5-21.5c0-6.3-2.8-11.9-7.1-15.8l43.3-75.4c2.1,0.7,4.3,1.2,6.7,1.2c5.1,0,9.7-1.9,13.4-4.8l40.6,33.6c-1.6,3-2.5,6.3-2.5,9.9c0,11.8,9.6,21.5,21.5,21.5c11.8,0,21.5-9.6,21.5-21.5c0-7.6-4-14.3-10-18.1l40.5-106.7c1.3,0.2,2.6,0.4,3.9,0.4c11.8,0,21.5-9.6,21.5-21.5C246,17.5,236.4,7.9,224.5,7.9L224.5,7.9z M31.5,239.6c-7.1,0-12.9-5.8-12.9-12.9c0-7.1,5.8-12.9,12.9-12.9c7.1,0,12.9,5.8,12.9,12.9C44.3,233.8,38.6,239.6,31.5,239.6z M95.7,128c-7.1,0-12.9-5.8-12.9-12.9c0-7.1,5.8-12.9,12.9-12.9c7.1,0,12.9,5.8,12.9,12.9C108.6,122.3,102.8,128,95.7,128z M168.7,188.1c-7.1,0-12.9-5.8-12.9-12.9c0-7.1,5.8-12.9,12.9-12.9c7.1,0,12.9,5.8,12.9,12.9C181.6,182.3,175.8,188.1,168.7,188.1L168.7,188.1z M224.5,42.2c-7.1,0-12.9-5.8-12.9-12.9c0-7.1,5.8-12.9,12.9-12.9c7.1,0,12.9,5.8,12.9,12.9C237.4,36.5,231.6,42.2,224.5,42.2L224.5,42.2z"/></g></g>
|
|
263
|
-
</svg> Graph View</button>
|
|
264
|
-
<button id="tableViewBtn" class="btn-theme"><svg class="btn-svg" width="12px" height="12px" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
265
|
-
</defs>
|
|
266
|
-
<g id="64px-Line" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
267
|
-
<g id="db-table">
|
|
268
|
-
|
|
269
|
-
</g>
|
|
270
|
-
<path d="M56,12 C56,9.794 54.206,8 52,8 L10,8 C7.794,8 6,9.794 6,12 L6,54 C6,56.206 7.794,58 10,58 L52,58 C54.206,58 56,56.206 56,54 L56,12 L56,12 Z M30,22 L30,32 L20,32 L20,22 L30,22 L30,22 Z M42,22 L42,32 L32,32 L32,22 L42,22 L42,22 Z M54,22 L54,32 L44,32 L44,22 L54,22 L54,22 Z M18,32 L8,32 L8,22 L18,22 L18,32 L18,32 Z M8,34 L18,34 L18,44 L8,44 L8,34 L8,34 Z M20,34 L30,34 L30,44 L20,44 L20,34 L20,34 Z M30,46 L30,56 L20,56 L20,46 L30,46 L30,46 Z M32,46 L42,46 L42,56 L32,56 L32,46 L32,46 Z M32,44 L32,34 L42,34 L42,44 L32,44 L32,44 Z M44,34 L54,34 L54,44 L44,44 L44,34 L44,34 Z M10,10 L52,10 C53.103,10 54,10.897 54,12 L54,20 L8,20 L8,12 C8,10.897 8.897,10 10,10 L10,10 Z M8,54 L8,46 L18,46 L18,56 L10,56 C8.897,56 8,55.103 8,54 L8,54 Z M52,56 L44,56 L44,46 L54,46 L54,54 C54,55.103 53.103,56 52,56 L52,56 Z" id="Shape" fill="#000000">
|
|
271
|
-
|
|
272
|
-
</path>
|
|
273
|
-
</g>
|
|
274
|
-
</svg> Table View</button>
|
|
275
|
-
</div>
|
|
276
|
-
|
|
277
|
-
<div id="graphView" style="display: block;">
|
|
278
|
-
<svg id="svgGraph" width="1000" height="600"></svg>
|
|
279
|
-
</div>
|
|
280
|
-
|
|
281
|
-
<div id="tableView" style="display: none;padding: 20px;">
|
|
282
|
-
<table class="impact-analysis-table" border="1">
|
|
283
|
-
<thead>
|
|
284
|
-
<tr>
|
|
285
|
-
<th>Changed File</th>
|
|
286
|
-
<th>Impacted Files</th>
|
|
287
|
-
</tr>
|
|
288
|
-
</thead>
|
|
289
|
-
<tbody id="tableBody"></tbody>
|
|
290
|
-
</table>
|
|
291
|
-
</div>
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
<script>
|
|
295
|
-
const graph = ${JSON.stringify(graphData)};
|
|
296
|
-
|
|
297
|
-
const width = window.innerWidth;
|
|
298
|
-
const height = window.innerHeight - 100;
|
|
299
|
-
|
|
300
|
-
const svg = d3.select("#svgGraph")
|
|
301
|
-
.attr("width", width)
|
|
302
|
-
.attr("height", height);
|
|
303
|
-
|
|
304
|
-
const simulation = d3.forceSimulation(graph.nodes)
|
|
305
|
-
.force("link", d3.forceLink(graph.links).id(d => d.id).distance(150)) // Increased distance for better spacing
|
|
306
|
-
.force("charge", d3.forceManyBody().strength(-200)) // Stronger repulsion to avoid overlap
|
|
307
|
-
.force("center", d3.forceCenter(width / 2, height / 2)) // Centering the graph
|
|
308
|
-
.force("collision", d3.forceCollide().radius(20)) // Prevents overlap
|
|
309
|
-
.on("tick", ticked);
|
|
310
|
-
|
|
311
|
-
const link = svg.selectAll("line")
|
|
312
|
-
.data(graph.links)
|
|
313
|
-
.enter().append("line");
|
|
314
|
-
|
|
315
|
-
const node = svg.selectAll("circle")
|
|
316
|
-
.data(graph.nodes)
|
|
317
|
-
.enter().append("circle")
|
|
318
|
-
.attr("r", d => d.isChangedFile ? 18 : 12) // Slightly larger nodes
|
|
319
|
-
.attr("fill", d => d.isChangedFile ? "#ff4500" : "#007bff") // Bright orange for changedFile, blue for others
|
|
320
|
-
.style("stroke", "#fff") // White border for clarity
|
|
321
|
-
.style("stroke-width", 2)
|
|
322
|
-
.call(drag(simulation));
|
|
323
|
-
|
|
324
|
-
const tooltip = d3.select("#tooltip");
|
|
325
|
-
|
|
326
|
-
node.on("mouseover", (event, d) => {
|
|
327
|
-
tooltip.style("display", "block")
|
|
328
|
-
.html(\`<strong>\${d.isChangedFile ? d.id : d.path}</strong>\`)
|
|
329
|
-
.style("left", (event.pageX + 10) + "px")
|
|
330
|
-
.style("top", (event.pageY - 10) + "px");
|
|
331
|
-
}).on("mouseout", () => tooltip.style("display", "none"));
|
|
332
|
-
|
|
333
|
-
svg.selectAll("text")
|
|
334
|
-
.data(graph.nodes)
|
|
335
|
-
.enter().append("text")
|
|
336
|
-
.attr("dy", 4)
|
|
337
|
-
.attr("x", 12)
|
|
338
|
-
.text(d => d.isChangedFile ? d.name : d.id);
|
|
339
|
-
|
|
340
|
-
simulation.on("tick", () => {
|
|
341
|
-
link.attr("x1", d => clamp(d.source.x, 0, width))
|
|
342
|
-
.attr("y1", d => clamp(d.source.y, 0, height))
|
|
343
|
-
.attr("x2", d => clamp(d.target.x, 0, width))
|
|
344
|
-
.attr("y2", d => clamp(d.target.y, 0, height));
|
|
345
|
-
|
|
346
|
-
node.attr("cx", d => clamp(d.x, 10, width - 10))
|
|
347
|
-
.attr("cy", d => clamp(d.y, 10, height - 10));
|
|
348
|
-
|
|
349
|
-
svg.selectAll("text")
|
|
350
|
-
.attr("x", d => clamp(d.x + 20, 10, width - 10))
|
|
351
|
-
.attr("y", d => clamp(d.y + 6, 10, height - 10));
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
// Helper function to keep nodes within boundaries
|
|
355
|
-
function clamp(value, min, max) {
|
|
356
|
-
return Math.max(min, Math.min(max, value));
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function drag(simulation) {
|
|
360
|
-
function dragstarted(event, d) {
|
|
361
|
-
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
362
|
-
d.fx = d.x;
|
|
363
|
-
d.fy = d.y;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function dragged(event, d) {
|
|
367
|
-
d.fx = clamp(event.x, 10, width - 10);
|
|
368
|
-
d.fy = clamp(event.y, 10, height - 10);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function dragended(event, d) {
|
|
372
|
-
if (!event.active) simulation.alphaTarget(0);
|
|
373
|
-
d.fx = null;
|
|
374
|
-
d.fy = null;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
return d3.drag()
|
|
378
|
-
.on("start", dragstarted)
|
|
379
|
-
.on("drag", dragged)
|
|
380
|
-
.on("end", dragended);
|
|
381
|
-
}
|
|
382
|
-
function ticked() {
|
|
383
|
-
link
|
|
384
|
-
.attr("x1", d => d.source.x)
|
|
385
|
-
.attr("y1", d => d.source.y)
|
|
386
|
-
.attr("x2", d => d.target.x)
|
|
387
|
-
.attr("y2", d => d.target.y);
|
|
388
|
-
|
|
389
|
-
node
|
|
390
|
-
.attr("cx", d => d.x)
|
|
391
|
-
.attr("cy", d => d.y);
|
|
392
|
-
|
|
393
|
-
label
|
|
394
|
-
.attr("x", d => d.x + 10)
|
|
395
|
-
.attr("y", d => d.y + 5);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
document.getElementById("graphViewBtn").addEventListener("click", function () {
|
|
399
|
-
document.getElementById("graphView").style.display = "block";
|
|
400
|
-
document.getElementById("tableView").style.display = "none";
|
|
401
|
-
setActiveButton(this);
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
document.getElementById("tableViewBtn").addEventListener("click", function () {
|
|
405
|
-
document.getElementById("graphView").style.display = "none";
|
|
406
|
-
document.getElementById("tableView").style.display = "block";
|
|
407
|
-
setActiveButton(this);
|
|
408
|
-
generateTableView(${JSON.stringify(dependencyJson)});
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
// Function to highlight the active button
|
|
412
|
-
function setActiveButton(activeBtn) {
|
|
413
|
-
document.querySelectorAll(".btn-theme").forEach(button => {
|
|
414
|
-
button.classList.remove("btn-active");
|
|
415
|
-
});
|
|
416
|
-
activeBtn.classList.add("btn-active");
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Function to generate the table
|
|
420
|
-
function generateTableView(data) {
|
|
421
|
-
const tableData = data;
|
|
422
|
-
var tableBody = document.getElementById("tableBody");
|
|
423
|
-
tableBody.innerHTML = ""; // Clear previous content
|
|
424
|
-
|
|
425
|
-
// Ensure tableData is an array
|
|
426
|
-
if (!Array.isArray(tableData) || tableData.length === 0) {
|
|
427
|
-
tableBody.innerHTML = '<tr><td colspan="2">No data available</td></tr>';
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Populate table
|
|
432
|
-
tableData.forEach(function (result) {
|
|
433
|
-
var row = document.createElement("tr");
|
|
434
|
-
|
|
435
|
-
var changedFileCell = document.createElement("td");
|
|
436
|
-
changedFileCell.textContent = result.changedFile;
|
|
437
|
-
|
|
438
|
-
var impactsToCell = document.createElement("td");
|
|
439
|
-
impactsToCell.innerHTML = result.impactsTo.map(function (file) {
|
|
440
|
-
return '<div class="impact-file-list">' + file + '</div>';
|
|
441
|
-
}).join("");
|
|
442
|
-
|
|
443
|
-
row.appendChild(changedFileCell);
|
|
444
|
-
row.appendChild(impactsToCell);
|
|
445
|
-
tableBody.appendChild(row);
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
</script>
|
|
451
|
-
</body>
|
|
452
|
-
</html>
|
|
453
|
-
`;
|
|
454
|
-
|
|
455
|
-
// Write HTML file
|
|
456
|
-
fs.writeFileSync('impact-analysis.html', htmlContent);
|
|
457
|
-
console.log('✅ Dependency graph generated: impact-analysis.html');
|
package/src/ai/explain.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
2
|
-
|
|
3
|
-
export async function explainImpact(results: any[]) {
|
|
4
|
-
if (!process.env.GEMINI_API_KEY) {
|
|
5
|
-
console.log('No GEMINI_API_KEY found. Skipping AI explanation.');
|
|
6
|
-
return null;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
|
|
11
|
-
const model = genAI.getGenerativeModel({ model: 'gemini-3-flash-preview' });
|
|
12
|
-
|
|
13
|
-
const prompt = `Analyze these code changes and explain their impact:
|
|
14
|
-
|
|
15
|
-
${JSON.stringify(results.map(r => ({
|
|
16
|
-
file: r.changedFile,
|
|
17
|
-
impactedFiles: r.impactedFiles.length,
|
|
18
|
-
risk: r.risk
|
|
19
|
-
})), null, 2)}
|
|
20
|
-
|
|
21
|
-
Provide a brief summary of the overall risk and key concerns.`;
|
|
22
|
-
|
|
23
|
-
const result = await model.generateContent(prompt);
|
|
24
|
-
const response = await result.response;
|
|
25
|
-
return response.text();
|
|
26
|
-
} catch (error) {
|
|
27
|
-
throw new Error(`AI explanation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
28
|
-
}
|
|
29
|
-
}
|
package/src/cache/graphCache.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import { DependencyGraph } from "../graph/types";
|
|
3
|
-
|
|
4
|
-
const CACHE_FILE = ".impact-analysis-cache.json";
|
|
5
|
-
|
|
6
|
-
export function loadCache(): DependencyGraph | null {
|
|
7
|
-
try {
|
|
8
|
-
if (!fs.existsSync(CACHE_FILE)) {
|
|
9
|
-
return null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const data = JSON.parse(fs.readFileSync(CACHE_FILE, "utf-8"));
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
imports: new Map(
|
|
16
|
-
Object.entries(data.imports).map(([key, value]) => [key, new Set(value as string[])])
|
|
17
|
-
),
|
|
18
|
-
importedBy: new Map(
|
|
19
|
-
Object.entries(data.importedBy).map(([key, value]) => [key, new Set(value as string[])])
|
|
20
|
-
),
|
|
21
|
-
componentUsage: new Map(
|
|
22
|
-
Object.entries(data.componentUsage).map(([key, value]) => [key, new Set(value as string[])])
|
|
23
|
-
)
|
|
24
|
-
};
|
|
25
|
-
} catch (error) {
|
|
26
|
-
console.warn('Cache file corrupted, rebuilding...');
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function saveCache(data: DependencyGraph) {
|
|
32
|
-
try {
|
|
33
|
-
const serializable = {
|
|
34
|
-
imports: Object.fromEntries(
|
|
35
|
-
Array.from(data.imports.entries()).map(([key, value]) => [key, Array.from(value)])
|
|
36
|
-
),
|
|
37
|
-
importedBy: Object.fromEntries(
|
|
38
|
-
Array.from(data.importedBy.entries()).map(([key, value]) => [key, Array.from(value)])
|
|
39
|
-
),
|
|
40
|
-
componentUsage: Object.fromEntries(
|
|
41
|
-
Array.from(data.componentUsage.entries()).map(([key, value]) => [key, Array.from(value)])
|
|
42
|
-
)
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
fs.writeFileSync(CACHE_FILE, JSON.stringify(serializable, null, 2));
|
|
46
|
-
} catch (error) {
|
|
47
|
-
console.warn('Failed to save cache:', error instanceof Error ? error.message : String(error));
|
|
48
|
-
}
|
|
49
|
-
}
|