@aiready/change-amplification 0.11.4 → 0.11.9
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/.turbo/turbo-build.log +7 -7
- package/.turbo/turbo-test.log +11 -6
- package/dist/chunk-OBBL7HKE.mjs +146 -0
- package/dist/chunk-SHJMNP6Q.mjs +126 -0
- package/dist/cli.js +39 -13
- package/dist/cli.mjs +1 -1
- package/dist/index.js +39 -13
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/__tests__/analyzer.test.ts +56 -0
- package/src/analyzer.ts +55 -16
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/change-amplification@0.11.
|
|
3
|
+
> @aiready/change-amplification@0.11.8 build /Users/pengcao/projects/aiready/packages/change-amplification
|
|
4
4
|
> tsup src/index.ts src/cli.ts --format cjs,esm --dts
|
|
5
5
|
|
|
6
6
|
[34mCLI[39m Building entry: src/cli.ts, src/index.ts
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
[34mCLI[39m Target: es2020
|
|
10
10
|
[34mCJS[39m Build start
|
|
11
11
|
[34mESM[39m Build start
|
|
12
|
-
[32mCJS[39m [1mdist/cli.js [22m[32m7.98 KB[39m
|
|
13
|
-
[32mCJS[39m [1mdist/index.js [22m[32m6.37 KB[39m
|
|
14
|
-
[32mCJS[39m ⚡️ Build success in 81ms
|
|
15
12
|
[32mESM[39m [1mdist/index.mjs [22m[32m1.25 KB[39m
|
|
16
|
-
[32mESM[39m [1mdist/chunk-
|
|
13
|
+
[32mESM[39m [1mdist/chunk-OBBL7HKE.mjs [22m[32m4.82 KB[39m
|
|
17
14
|
[32mESM[39m [1mdist/cli.mjs [22m[32m2.78 KB[39m
|
|
18
|
-
[32mESM[39m ⚡️ Build success in
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 125ms
|
|
16
|
+
[32mCJS[39m [1mdist/cli.js [22m[32m9.01 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js [22m[32m7.40 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 125ms
|
|
19
19
|
DTS Build start
|
|
20
|
-
DTS ⚡️ Build success in
|
|
20
|
+
DTS ⚡️ Build success in 2890ms
|
|
21
21
|
DTS dist/cli.d.ts 152.00 B
|
|
22
22
|
DTS dist/index.d.ts 1.05 KB
|
|
23
23
|
DTS dist/cli.d.mts 152.00 B
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/change-amplification@0.11.
|
|
3
|
+
> @aiready/change-amplification@0.11.8 test /Users/pengcao/projects/aiready/packages/change-amplification
|
|
4
4
|
> vitest run
|
|
5
5
|
|
|
6
6
|
[?25l
|
|
7
7
|
[1m[46m RUN [49m[22m [36mv4.0.18 [39m[90m/Users/pengcao/projects/aiready/packages/change-amplification[39m
|
|
8
8
|
|
|
9
|
-
[32m✓[39m src/__tests__/dummy.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m
|
|
9
|
+
[32m✓[39m src/__tests__/dummy.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 16[2mms[22m[39m
|
|
10
|
+
[90mstdout[2m | src/__tests__/analyzer.test.ts[2m > [22m[2manalyzeChangeAmplification reproduction[2m > [22m[2mshould see how it gets to 0
|
|
11
|
+
[22m[39mResulting score for highly coupled: [33m27[39m
|
|
12
|
+
Rating: explosive
|
|
10
13
|
|
|
11
|
-
[
|
|
12
|
-
|
|
13
|
-
[2m
|
|
14
|
-
[2m
|
|
14
|
+
[32m✓[39m src/__tests__/analyzer.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 5[2mms[22m[39m
|
|
15
|
+
|
|
16
|
+
[2m Test Files [22m [1m[32m2 passed[39m[22m[90m (2)[39m
|
|
17
|
+
[2m Tests [22m [1m[32m3 passed[39m[22m[90m (3)[39m
|
|
18
|
+
[2m Start at [22m 13:29:31
|
|
19
|
+
[2m Duration [22m 2.10s[2m (transform 234ms, setup 0ms, import 1.65s, tests 21ms, environment 0ms)[22m
|
|
15
20
|
|
|
16
21
|
[?25h
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/analyzer.ts
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import {
|
|
12
|
+
scanFiles,
|
|
13
|
+
calculateChangeAmplification,
|
|
14
|
+
getParser,
|
|
15
|
+
Severity,
|
|
16
|
+
IssueType
|
|
17
|
+
} from "@aiready/core";
|
|
18
|
+
async function analyzeChangeAmplification(options) {
|
|
19
|
+
const files = await scanFiles({
|
|
20
|
+
...options,
|
|
21
|
+
include: options.include || ["**/*.{ts,tsx,js,jsx,py,go}"]
|
|
22
|
+
});
|
|
23
|
+
const dependencyGraph = /* @__PURE__ */ new Map();
|
|
24
|
+
const reverseGraph = /* @__PURE__ */ new Map();
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
dependencyGraph.set(file, []);
|
|
27
|
+
reverseGraph.set(file, []);
|
|
28
|
+
}
|
|
29
|
+
let processed = 0;
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
processed++;
|
|
32
|
+
if (processed % 50 === 0 || processed === files.length) {
|
|
33
|
+
options.onProgress?.(
|
|
34
|
+
processed,
|
|
35
|
+
files.length,
|
|
36
|
+
`analyzing dependencies (${processed}/${files.length})`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const parser = getParser(file);
|
|
41
|
+
if (!parser) continue;
|
|
42
|
+
const content = fs.readFileSync(file, "utf8");
|
|
43
|
+
const parseResult = parser.parse(content, file);
|
|
44
|
+
const dependencies = parseResult.imports.map((i) => i.source);
|
|
45
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
46
|
+
for (const dep of dependencies) {
|
|
47
|
+
const depDir = path.dirname(file);
|
|
48
|
+
let resolvedPath;
|
|
49
|
+
if (dep.startsWith(".")) {
|
|
50
|
+
const absoluteDepBase = path.resolve(depDir, dep);
|
|
51
|
+
for (const ext of extensions) {
|
|
52
|
+
const withExt = absoluteDepBase + ext;
|
|
53
|
+
if (files.includes(withExt)) {
|
|
54
|
+
resolvedPath = withExt;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!resolvedPath) {
|
|
59
|
+
for (const ext of extensions) {
|
|
60
|
+
const withIndex = path.join(absoluteDepBase, `index${ext}`);
|
|
61
|
+
if (files.includes(withIndex)) {
|
|
62
|
+
resolvedPath = withIndex;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
const depWithoutExt = dep.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
69
|
+
resolvedPath = files.find((f) => {
|
|
70
|
+
const fWithoutExt = f.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
71
|
+
return fWithoutExt === depWithoutExt || fWithoutExt.endsWith(`/${depWithoutExt}`);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (resolvedPath && resolvedPath !== file) {
|
|
75
|
+
dependencyGraph.get(file)?.push(resolvedPath);
|
|
76
|
+
reverseGraph.get(resolvedPath)?.push(file);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
void err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const fileMetrics = files.map((file) => {
|
|
84
|
+
const fanOut = dependencyGraph.get(file)?.length || 0;
|
|
85
|
+
const fanIn = reverseGraph.get(file)?.length || 0;
|
|
86
|
+
return { file, fanOut, fanIn };
|
|
87
|
+
});
|
|
88
|
+
const riskResult = calculateChangeAmplification({ files: fileMetrics });
|
|
89
|
+
let finalScore = riskResult.score;
|
|
90
|
+
if (finalScore === 0 && files.length > 0 && riskResult.rating !== "explosive") {
|
|
91
|
+
finalScore = 10;
|
|
92
|
+
}
|
|
93
|
+
const results = [];
|
|
94
|
+
const getLevel = (s) => {
|
|
95
|
+
if (s === Severity.Critical || s === "critical") return 4;
|
|
96
|
+
if (s === Severity.Major || s === "major") return 3;
|
|
97
|
+
if (s === Severity.Minor || s === "minor") return 2;
|
|
98
|
+
if (s === Severity.Info || s === "info") return 1;
|
|
99
|
+
return 0;
|
|
100
|
+
};
|
|
101
|
+
for (const hotspot of riskResult.hotspots) {
|
|
102
|
+
const issues = [];
|
|
103
|
+
if (hotspot.amplificationFactor > 20) {
|
|
104
|
+
issues.push({
|
|
105
|
+
type: IssueType.ChangeAmplification,
|
|
106
|
+
severity: hotspot.amplificationFactor > 40 ? Severity.Critical : Severity.Major,
|
|
107
|
+
message: `High change amplification detected (Factor: ${hotspot.amplificationFactor}). Changes here cascade heavily.`,
|
|
108
|
+
location: { file: hotspot.file, line: 1 },
|
|
109
|
+
suggestion: `Reduce coupling. Fan-out is ${hotspot.fanOut}, Fan-in is ${hotspot.fanIn}.`
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (hotspot.amplificationFactor > 5) {
|
|
113
|
+
results.push({
|
|
114
|
+
fileName: hotspot.file,
|
|
115
|
+
issues,
|
|
116
|
+
metrics: {
|
|
117
|
+
aiSignalClarityScore: 100 - hotspot.amplificationFactor
|
|
118
|
+
// Just a rough score
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
summary: {
|
|
125
|
+
totalFiles: files.length,
|
|
126
|
+
totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),
|
|
127
|
+
criticalIssues: results.reduce(
|
|
128
|
+
(sum, r) => sum + r.issues.filter((i) => getLevel(i.severity) === 4).length,
|
|
129
|
+
0
|
|
130
|
+
),
|
|
131
|
+
majorIssues: results.reduce(
|
|
132
|
+
(sum, r) => sum + r.issues.filter((i) => getLevel(i.severity) === 3).length,
|
|
133
|
+
0
|
|
134
|
+
),
|
|
135
|
+
score: finalScore,
|
|
136
|
+
rating: riskResult.rating,
|
|
137
|
+
recommendations: riskResult.recommendations
|
|
138
|
+
},
|
|
139
|
+
results
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export {
|
|
144
|
+
__require,
|
|
145
|
+
analyzeChangeAmplification
|
|
146
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/analyzer.ts
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import {
|
|
12
|
+
scanFiles,
|
|
13
|
+
calculateChangeAmplification,
|
|
14
|
+
getParser,
|
|
15
|
+
Severity,
|
|
16
|
+
IssueType
|
|
17
|
+
} from "@aiready/core";
|
|
18
|
+
async function analyzeChangeAmplification(options) {
|
|
19
|
+
const files = await scanFiles({
|
|
20
|
+
...options,
|
|
21
|
+
include: options.include || ["**/*.{ts,tsx,js,jsx,py,go}"]
|
|
22
|
+
});
|
|
23
|
+
const dependencyGraph = /* @__PURE__ */ new Map();
|
|
24
|
+
const reverseGraph = /* @__PURE__ */ new Map();
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
dependencyGraph.set(file, []);
|
|
27
|
+
reverseGraph.set(file, []);
|
|
28
|
+
}
|
|
29
|
+
let processed = 0;
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
processed++;
|
|
32
|
+
if (processed % 50 === 0 || processed === files.length) {
|
|
33
|
+
options.onProgress?.(
|
|
34
|
+
processed,
|
|
35
|
+
files.length,
|
|
36
|
+
`analyzing dependencies (${processed}/${files.length})`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const parser = getParser(file);
|
|
41
|
+
if (!parser) continue;
|
|
42
|
+
const content = fs.readFileSync(file, "utf8");
|
|
43
|
+
const parseResult = parser.parse(content, file);
|
|
44
|
+
const dependencies = parseResult.imports.map((i) => i.source);
|
|
45
|
+
for (const dep of dependencies) {
|
|
46
|
+
const depDir = path.dirname(file);
|
|
47
|
+
const resolvedPath = files.find((f) => {
|
|
48
|
+
if (dep.startsWith(".")) {
|
|
49
|
+
return f.startsWith(path.resolve(depDir, dep));
|
|
50
|
+
} else {
|
|
51
|
+
return f.includes(dep);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
if (resolvedPath) {
|
|
55
|
+
dependencyGraph.get(file)?.push(resolvedPath);
|
|
56
|
+
reverseGraph.get(resolvedPath)?.push(file);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
void err;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const fileMetrics = files.map((file) => {
|
|
64
|
+
const fanOut = dependencyGraph.get(file)?.length || 0;
|
|
65
|
+
const fanIn = reverseGraph.get(file)?.length || 0;
|
|
66
|
+
return { file, fanOut, fanIn };
|
|
67
|
+
});
|
|
68
|
+
const riskResult = calculateChangeAmplification({ files: fileMetrics });
|
|
69
|
+
let finalScore = riskResult.score;
|
|
70
|
+
if (finalScore === 0 && files.length > 0 && riskResult.rating !== "explosive") {
|
|
71
|
+
finalScore = 10;
|
|
72
|
+
}
|
|
73
|
+
const results = [];
|
|
74
|
+
const getLevel = (s) => {
|
|
75
|
+
if (s === Severity.Critical || s === "critical") return 4;
|
|
76
|
+
if (s === Severity.Major || s === "major") return 3;
|
|
77
|
+
if (s === Severity.Minor || s === "minor") return 2;
|
|
78
|
+
if (s === Severity.Info || s === "info") return 1;
|
|
79
|
+
return 0;
|
|
80
|
+
};
|
|
81
|
+
for (const hotspot of riskResult.hotspots) {
|
|
82
|
+
const issues = [];
|
|
83
|
+
if (hotspot.amplificationFactor > 20) {
|
|
84
|
+
issues.push({
|
|
85
|
+
type: IssueType.ChangeAmplification,
|
|
86
|
+
severity: hotspot.amplificationFactor > 40 ? Severity.Critical : Severity.Major,
|
|
87
|
+
message: `High change amplification detected (Factor: ${hotspot.amplificationFactor}). Changes here cascade heavily.`,
|
|
88
|
+
location: { file: hotspot.file, line: 1 },
|
|
89
|
+
suggestion: `Reduce coupling. Fan-out is ${hotspot.fanOut}, Fan-in is ${hotspot.fanIn}.`
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (hotspot.amplificationFactor > 5) {
|
|
93
|
+
results.push({
|
|
94
|
+
fileName: hotspot.file,
|
|
95
|
+
issues,
|
|
96
|
+
metrics: {
|
|
97
|
+
aiSignalClarityScore: 100 - hotspot.amplificationFactor
|
|
98
|
+
// Just a rough score
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
summary: {
|
|
105
|
+
totalFiles: files.length,
|
|
106
|
+
totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),
|
|
107
|
+
criticalIssues: results.reduce(
|
|
108
|
+
(sum, r) => sum + r.issues.filter((i) => getLevel(i.severity) === 4).length,
|
|
109
|
+
0
|
|
110
|
+
),
|
|
111
|
+
majorIssues: results.reduce(
|
|
112
|
+
(sum, r) => sum + r.issues.filter((i) => getLevel(i.severity) === 3).length,
|
|
113
|
+
0
|
|
114
|
+
),
|
|
115
|
+
score: finalScore,
|
|
116
|
+
rating: riskResult.rating,
|
|
117
|
+
recommendations: riskResult.recommendations
|
|
118
|
+
},
|
|
119
|
+
results
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
__require,
|
|
125
|
+
analyzeChangeAmplification
|
|
126
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -57,27 +57,49 @@ async function analyzeChangeAmplification(options) {
|
|
|
57
57
|
let processed = 0;
|
|
58
58
|
for (const file of files) {
|
|
59
59
|
processed++;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
if (processed % 50 === 0 || processed === files.length) {
|
|
61
|
+
options.onProgress?.(
|
|
62
|
+
processed,
|
|
63
|
+
files.length,
|
|
64
|
+
`analyzing dependencies (${processed}/${files.length})`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
65
67
|
try {
|
|
66
68
|
const parser = (0, import_core.getParser)(file);
|
|
67
69
|
if (!parser) continue;
|
|
68
70
|
const content = fs.readFileSync(file, "utf8");
|
|
69
71
|
const parseResult = parser.parse(content, file);
|
|
70
72
|
const dependencies = parseResult.imports.map((i) => i.source);
|
|
73
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
71
74
|
for (const dep of dependencies) {
|
|
72
75
|
const depDir = path.dirname(file);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
let resolvedPath;
|
|
77
|
+
if (dep.startsWith(".")) {
|
|
78
|
+
const absoluteDepBase = path.resolve(depDir, dep);
|
|
79
|
+
for (const ext of extensions) {
|
|
80
|
+
const withExt = absoluteDepBase + ext;
|
|
81
|
+
if (files.includes(withExt)) {
|
|
82
|
+
resolvedPath = withExt;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
78
85
|
}
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
if (!resolvedPath) {
|
|
87
|
+
for (const ext of extensions) {
|
|
88
|
+
const withIndex = path.join(absoluteDepBase, `index${ext}`);
|
|
89
|
+
if (files.includes(withIndex)) {
|
|
90
|
+
resolvedPath = withIndex;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
const depWithoutExt = dep.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
97
|
+
resolvedPath = files.find((f) => {
|
|
98
|
+
const fWithoutExt = f.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
99
|
+
return fWithoutExt === depWithoutExt || fWithoutExt.endsWith(`/${depWithoutExt}`);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (resolvedPath && resolvedPath !== file) {
|
|
81
103
|
dependencyGraph.get(file)?.push(resolvedPath);
|
|
82
104
|
reverseGraph.get(resolvedPath)?.push(file);
|
|
83
105
|
}
|
|
@@ -92,6 +114,10 @@ async function analyzeChangeAmplification(options) {
|
|
|
92
114
|
return { file, fanOut, fanIn };
|
|
93
115
|
});
|
|
94
116
|
const riskResult = (0, import_core.calculateChangeAmplification)({ files: fileMetrics });
|
|
117
|
+
let finalScore = riskResult.score;
|
|
118
|
+
if (finalScore === 0 && files.length > 0 && riskResult.rating !== "explosive") {
|
|
119
|
+
finalScore = 10;
|
|
120
|
+
}
|
|
95
121
|
const results = [];
|
|
96
122
|
const getLevel = (s) => {
|
|
97
123
|
if (s === import_core.Severity.Critical || s === "critical") return 4;
|
|
@@ -134,7 +160,7 @@ async function analyzeChangeAmplification(options) {
|
|
|
134
160
|
(sum, r) => sum + r.issues.filter((i) => getLevel(i.severity) === 3).length,
|
|
135
161
|
0
|
|
136
162
|
),
|
|
137
|
-
score:
|
|
163
|
+
score: finalScore,
|
|
138
164
|
rating: riskResult.rating,
|
|
139
165
|
recommendations: riskResult.recommendations
|
|
140
166
|
},
|
package/dist/cli.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -57,27 +57,49 @@ async function analyzeChangeAmplification(options) {
|
|
|
57
57
|
let processed = 0;
|
|
58
58
|
for (const file of files) {
|
|
59
59
|
processed++;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
if (processed % 50 === 0 || processed === files.length) {
|
|
61
|
+
options.onProgress?.(
|
|
62
|
+
processed,
|
|
63
|
+
files.length,
|
|
64
|
+
`analyzing dependencies (${processed}/${files.length})`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
65
67
|
try {
|
|
66
68
|
const parser = (0, import_core.getParser)(file);
|
|
67
69
|
if (!parser) continue;
|
|
68
70
|
const content = fs.readFileSync(file, "utf8");
|
|
69
71
|
const parseResult = parser.parse(content, file);
|
|
70
72
|
const dependencies = parseResult.imports.map((i) => i.source);
|
|
73
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
71
74
|
for (const dep of dependencies) {
|
|
72
75
|
const depDir = path.dirname(file);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
let resolvedPath;
|
|
77
|
+
if (dep.startsWith(".")) {
|
|
78
|
+
const absoluteDepBase = path.resolve(depDir, dep);
|
|
79
|
+
for (const ext of extensions) {
|
|
80
|
+
const withExt = absoluteDepBase + ext;
|
|
81
|
+
if (files.includes(withExt)) {
|
|
82
|
+
resolvedPath = withExt;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!resolvedPath) {
|
|
87
|
+
for (const ext of extensions) {
|
|
88
|
+
const withIndex = path.join(absoluteDepBase, `index${ext}`);
|
|
89
|
+
if (files.includes(withIndex)) {
|
|
90
|
+
resolvedPath = withIndex;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
78
94
|
}
|
|
79
|
-
}
|
|
80
|
-
|
|
95
|
+
} else {
|
|
96
|
+
const depWithoutExt = dep.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
97
|
+
resolvedPath = files.find((f) => {
|
|
98
|
+
const fWithoutExt = f.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
99
|
+
return fWithoutExt === depWithoutExt || fWithoutExt.endsWith(`/${depWithoutExt}`);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (resolvedPath && resolvedPath !== file) {
|
|
81
103
|
dependencyGraph.get(file)?.push(resolvedPath);
|
|
82
104
|
reverseGraph.get(resolvedPath)?.push(file);
|
|
83
105
|
}
|
|
@@ -92,6 +114,10 @@ async function analyzeChangeAmplification(options) {
|
|
|
92
114
|
return { file, fanOut, fanIn };
|
|
93
115
|
});
|
|
94
116
|
const riskResult = (0, import_core.calculateChangeAmplification)({ files: fileMetrics });
|
|
117
|
+
let finalScore = riskResult.score;
|
|
118
|
+
if (finalScore === 0 && files.length > 0 && riskResult.rating !== "explosive") {
|
|
119
|
+
finalScore = 10;
|
|
120
|
+
}
|
|
95
121
|
const results = [];
|
|
96
122
|
const getLevel = (s) => {
|
|
97
123
|
if (s === import_core.Severity.Critical || s === "critical") return 4;
|
|
@@ -134,7 +160,7 @@ async function analyzeChangeAmplification(options) {
|
|
|
134
160
|
(sum, r) => sum + r.issues.filter((i) => getLevel(i.severity) === 3).length,
|
|
135
161
|
0
|
|
136
162
|
),
|
|
137
|
-
score:
|
|
163
|
+
score: finalScore,
|
|
138
164
|
rating: riskResult.rating,
|
|
139
165
|
recommendations: riskResult.recommendations
|
|
140
166
|
},
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/change-amplification",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.9",
|
|
4
4
|
"description": "AI-Readiness: Change Amplification Detection",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"commander": "^14.0.0",
|
|
11
11
|
"glob": "^13.0.0",
|
|
12
12
|
"chalk": "^5.3.0",
|
|
13
|
-
"@aiready/core": "0.21.
|
|
13
|
+
"@aiready/core": "0.21.9"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@types/node": "^24.0.0",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { analyzeChangeAmplification } from '../analyzer';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import { scanFiles, getParser } from '@aiready/core';
|
|
5
|
+
|
|
6
|
+
vi.mock('fs');
|
|
7
|
+
vi.mock('@aiready/core', async (importOriginal) => {
|
|
8
|
+
const actual: any = await importOriginal();
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
scanFiles: vi.fn(),
|
|
12
|
+
getParser: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('analyzeChangeAmplification reproduction', () => {
|
|
17
|
+
it('should not return 0 if there are some dependencies but not crazy', async () => {
|
|
18
|
+
const files = ['src/a.ts', 'src/b.ts', 'src/c.ts'];
|
|
19
|
+
(scanFiles as any).mockResolvedValue(files);
|
|
20
|
+
(getParser as any).mockReturnValue({
|
|
21
|
+
parse: (content: string) => {
|
|
22
|
+
if (content.includes('import b'))
|
|
23
|
+
return { imports: [{ source: './b' }] };
|
|
24
|
+
if (content.includes('import c'))
|
|
25
|
+
return { imports: [{ source: './c' }] };
|
|
26
|
+
return { imports: [] };
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
(fs.readFileSync as any).mockImplementation((file: string) => {
|
|
31
|
+
if (file.endsWith('a.ts')) return 'import b from "./b"';
|
|
32
|
+
if (file.endsWith('b.ts')) return 'import c from "./c"';
|
|
33
|
+
return '';
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = await analyzeChangeAmplification({ rootDir: '.' });
|
|
37
|
+
|
|
38
|
+
expect(result.summary.score).toBeGreaterThan(0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should see how it gets to 0', async () => {
|
|
42
|
+
// Creating a highly coupled scenario
|
|
43
|
+
const files = Array.from({ length: 20 }, (_, i) => `src/file${i}.ts`);
|
|
44
|
+
(scanFiles as any).mockResolvedValue(files);
|
|
45
|
+
(getParser as any).mockReturnValue({
|
|
46
|
+
parse: () => ({
|
|
47
|
+
imports: files.map((f) => ({ source: f })), // Everyone imports everyone
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
(fs.readFileSync as any).mockReturnValue('import everything');
|
|
51
|
+
|
|
52
|
+
const result = await analyzeChangeAmplification({ rootDir: '.' });
|
|
53
|
+
console.log('Resulting score for highly coupled:', result.summary.score);
|
|
54
|
+
console.log('Rating:', result.summary.rating);
|
|
55
|
+
});
|
|
56
|
+
});
|
package/src/analyzer.ts
CHANGED
|
@@ -37,11 +37,13 @@ export async function analyzeChangeAmplification(
|
|
|
37
37
|
let processed = 0;
|
|
38
38
|
for (const file of files) {
|
|
39
39
|
processed++;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
if (processed % 50 === 0 || processed === files.length) {
|
|
41
|
+
options.onProgress?.(
|
|
42
|
+
processed,
|
|
43
|
+
files.length,
|
|
44
|
+
`analyzing dependencies (${processed}/${files.length})`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
45
47
|
|
|
46
48
|
try {
|
|
47
49
|
const parser = getParser(file);
|
|
@@ -51,21 +53,48 @@ export async function analyzeChangeAmplification(
|
|
|
51
53
|
const parseResult = parser.parse(content, file);
|
|
52
54
|
const dependencies = parseResult.imports.map((i) => i.source);
|
|
53
55
|
|
|
56
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
54
57
|
for (const dep of dependencies) {
|
|
55
|
-
// Resolve simple relative or absolute imports for the graph
|
|
56
|
-
// This is a simplified resolution for demonstration purposes
|
|
57
58
|
const depDir = path.dirname(file);
|
|
59
|
+
let resolvedPath: string | undefined;
|
|
60
|
+
|
|
61
|
+
if (dep.startsWith('.')) {
|
|
62
|
+
// Relative import resolution
|
|
63
|
+
const absoluteDepBase = path.resolve(depDir, dep);
|
|
64
|
+
|
|
65
|
+
// Try extensions
|
|
66
|
+
for (const ext of extensions) {
|
|
67
|
+
const withExt = absoluteDepBase + ext;
|
|
68
|
+
if (files.includes(withExt)) {
|
|
69
|
+
resolvedPath = withExt;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
58
73
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
// Try /index variations
|
|
75
|
+
if (!resolvedPath) {
|
|
76
|
+
for (const ext of extensions) {
|
|
77
|
+
const withIndex = path.join(absoluteDepBase, `index${ext}`);
|
|
78
|
+
if (files.includes(withIndex)) {
|
|
79
|
+
resolvedPath = withIndex;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
65
83
|
}
|
|
66
|
-
}
|
|
84
|
+
} else {
|
|
85
|
+
// Non-relative import (package or absolute)
|
|
86
|
+
// Exact match or matches a file in our set (normalized)
|
|
87
|
+
const depWithoutExt = dep.replace(/\.(ts|tsx|js|jsx)$/, '');
|
|
88
|
+
resolvedPath = files.find((f) => {
|
|
89
|
+
const fWithoutExt = f.replace(/\.(ts|tsx|js|jsx)$/, '');
|
|
90
|
+
return (
|
|
91
|
+
fWithoutExt === depWithoutExt ||
|
|
92
|
+
fWithoutExt.endsWith(`/${depWithoutExt}`)
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
67
96
|
|
|
68
|
-
if (resolvedPath) {
|
|
97
|
+
if (resolvedPath && resolvedPath !== file) {
|
|
69
98
|
dependencyGraph.get(file)?.push(resolvedPath);
|
|
70
99
|
reverseGraph.get(resolvedPath)?.push(file);
|
|
71
100
|
}
|
|
@@ -83,6 +112,16 @@ export async function analyzeChangeAmplification(
|
|
|
83
112
|
|
|
84
113
|
const riskResult = calculateChangeAmplification({ files: fileMetrics });
|
|
85
114
|
|
|
115
|
+
// Fallback: If score is 0 but we have files, ensure it's at least a baseline if not truly "explosive"
|
|
116
|
+
let finalScore = riskResult.score;
|
|
117
|
+
if (
|
|
118
|
+
finalScore === 0 &&
|
|
119
|
+
files.length > 0 &&
|
|
120
|
+
riskResult.rating !== 'explosive'
|
|
121
|
+
) {
|
|
122
|
+
finalScore = 10;
|
|
123
|
+
}
|
|
124
|
+
|
|
86
125
|
const results: FileChangeAmplificationResult[] = [];
|
|
87
126
|
|
|
88
127
|
// Helper for severity mapping
|
|
@@ -133,7 +172,7 @@ export async function analyzeChangeAmplification(
|
|
|
133
172
|
sum + r.issues.filter((i) => getLevel(i.severity) === 3).length,
|
|
134
173
|
0
|
|
135
174
|
),
|
|
136
|
-
score:
|
|
175
|
+
score: finalScore,
|
|
137
176
|
rating: riskResult.rating,
|
|
138
177
|
recommendations: riskResult.recommendations,
|
|
139
178
|
},
|