@aiready/change-amplification 0.1.4 → 0.1.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.
- package/.turbo/turbo-build.log +14 -14
- package/.turbo/turbo-test.log +6 -6
- package/README.md +1 -1
- package/dist/chunk-A5ACPWWO.mjs +114 -0
- package/dist/cli.js +23 -5
- package/dist/cli.mjs +10 -3
- package/dist/index.d.mts +1 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +14 -3
- package/dist/index.mjs +1 -1
- package/package.json +8 -8
- package/src/analyzer.ts +40 -15
- package/src/cli.ts +17 -5
- package/src/types.ts +1 -3
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/change-amplification@0.1.
|
|
3
|
+
> @aiready/change-amplification@0.1.6 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
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[34mCJS[39m Build start
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
|
|
13
|
-
[90m[[
|
|
13
|
+
[90m[[90m10:18:49 PM[90m][39m [43m[30m WARN [39m[49m [33m▲ [43;33m[[43;30mWARNING[43;33m][0m [1mThe condition "types" here will never be used as it comes after both "import" and "require"[0m [package.json]
|
|
14
14
|
|
|
15
15
|
package.json:33:6:
|
|
16
16
|
[37m 33 │ [32m"types"[37m: "./dist/index.d.ts"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
[90m[[
|
|
34
|
+
[90m[[90m10:18:49 PM[90m][39m [43m[30m WARN [39m[49m [33m▲ [43;33m[[43;30mWARNING[43;33m][0m [1mThe condition "types" here will never be used as it comes after both "import" and "require"[0m [package.json]
|
|
35
35
|
|
|
36
36
|
package.json:38:6:
|
|
37
37
|
[37m 38 │ [32m"types"[37m: "./dist/cli.d.ts"
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
[90m[[
|
|
55
|
+
[90m[[90m10:18:49 PM[90m][39m [43m[30m WARN [39m[49m [33m▲ [43;33m[[43;30mWARNING[43;33m][0m [1mThe condition "types" here will never be used as it comes after both "import" and "require"[0m [package.json]
|
|
56
56
|
|
|
57
57
|
package.json:33:6:
|
|
58
58
|
[37m 33 │ [32m"types"[37m: "./dist/index.d.ts"
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
[90m[[
|
|
76
|
+
[90m[[90m10:18:49 PM[90m][39m [43m[30m WARN [39m[49m [33m▲ [43;33m[[43;30mWARNING[43;33m][0m [1mThe condition "types" here will never be used as it comes after both "import" and "require"[0m [package.json]
|
|
77
77
|
|
|
78
78
|
package.json:38:6:
|
|
79
79
|
[37m 38 │ [32m"types"[37m: "./dist/cli.d.ts"
|
|
@@ -93,16 +93,16 @@
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
[32mCJS[39m [1mdist/cli.js [22m[32m8.08 KB[39m
|
|
97
|
+
[32mCJS[39m [1mdist/index.js [22m[32m5.21 KB[39m
|
|
98
|
+
[32mCJS[39m ⚡️ Build success in 145ms
|
|
99
|
+
[32mESM[39m [1mdist/cli.mjs [22m[32m2.78 KB[39m
|
|
96
100
|
[32mESM[39m [1mdist/index.mjs [22m[32m110.00 B[39m
|
|
97
|
-
[32mESM[39m [1mdist/
|
|
98
|
-
[32mESM[39m
|
|
99
|
-
[32mESM[39m ⚡️ Build success in 66ms
|
|
100
|
-
[32mCJS[39m [1mdist/index.js [22m[32m5.13 KB[39m
|
|
101
|
-
[32mCJS[39m [1mdist/cli.js [22m[32m7.94 KB[39m
|
|
102
|
-
[32mCJS[39m ⚡️ Build success in 67ms
|
|
101
|
+
[32mESM[39m [1mdist/chunk-A5ACPWWO.mjs [22m[32m3.92 KB[39m
|
|
102
|
+
[32mESM[39m ⚡️ Build success in 146ms
|
|
103
103
|
DTS Build start
|
|
104
|
-
DTS ⚡️ Build success in
|
|
104
|
+
DTS ⚡️ Build success in 4081ms
|
|
105
105
|
DTS dist/cli.d.ts 152.00 B
|
|
106
|
-
DTS dist/index.d.ts
|
|
106
|
+
DTS dist/index.d.ts 999.00 B
|
|
107
107
|
DTS dist/cli.d.mts 152.00 B
|
|
108
|
-
DTS dist/index.d.mts
|
|
108
|
+
DTS dist/index.d.mts 999.00 B
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/change-amplification@0.1.
|
|
3
|
+
> @aiready/change-amplification@0.1.6 test /Users/pengcao/projects/aiready/packages/change-amplification
|
|
4
4
|
> vitest run
|
|
5
5
|
|
|
6
|
+
[?25l
|
|
7
|
+
[1m[46m RUN [49m[22m [36mv4.0.18 [39m[90m/Users/pengcao/projects/aiready/packages/change-amplification[39m
|
|
6
8
|
|
|
7
|
-
[
|
|
8
|
-
|
|
9
|
-
[32m✓[39m src/__tests__/dummy.test.ts [2m ([22m[2m1 test[22m[2m)[22m[90m 9[2mms[22m[39m
|
|
9
|
+
[32m✓[39m src/__tests__/dummy.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 2[2mms[22m[39m
|
|
10
10
|
|
|
11
11
|
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
12
12
|
[2m Tests [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
13
|
-
[2m Start at [22m
|
|
14
|
-
[2m Duration [22m
|
|
13
|
+
[2m Start at [22m 22:19:16
|
|
14
|
+
[2m Duration [22m 285ms[2m (transform 44ms, setup 0ms, import 97ms, tests 2ms, environment 0ms)[22m
|
|
15
15
|
|
|
16
16
|
[?25h
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
## Overview
|
|
9
9
|
|
|
10
|
-
High architectural coupling is one of the leading causes of AI agent failure. When an agent modifies a "bottleneck" file with dozens of dependents, the resulting cascade of breakages often exceeds the agent's context window or reasoning capacity.
|
|
10
|
+
High architectural coupling is one of the leading causes of AI agent failure. When an agent modifies a "bottleneck" file with dozens of dependents, the resulting cascade of breakages often exceeds the agent's context window or reasoning capacity.
|
|
11
11
|
|
|
12
12
|
The **Change Amplification** analyzer computes graph metrics (fan-in, fan-out, and centrality) to identify these high-risk areas before they cause an "edit explosion."
|
|
13
13
|
|
|
@@ -0,0 +1,114 @@
|
|
|
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 { globSync } from "glob";
|
|
12
|
+
import { calculateChangeAmplification } from "@aiready/core";
|
|
13
|
+
import { getParser } from "@aiready/core";
|
|
14
|
+
function collectFiles(dir, options) {
|
|
15
|
+
const includePatterns = options.include && options.include.length > 0 ? options.include : ["**/*.{ts,tsx,js,jsx,py,go}"];
|
|
16
|
+
const excludePatterns = options.exclude && options.exclude.length > 0 ? options.exclude : ["**/node_modules/**", "**/dist/**", "**/.git/**"];
|
|
17
|
+
let matchedFiles = [];
|
|
18
|
+
for (const pattern of includePatterns) {
|
|
19
|
+
const files = globSync(pattern, {
|
|
20
|
+
cwd: dir,
|
|
21
|
+
ignore: excludePatterns,
|
|
22
|
+
absolute: true
|
|
23
|
+
});
|
|
24
|
+
matchedFiles = matchedFiles.concat(files);
|
|
25
|
+
}
|
|
26
|
+
return [...new Set(matchedFiles)];
|
|
27
|
+
}
|
|
28
|
+
async function analyzeChangeAmplification(options) {
|
|
29
|
+
const rootDir = path.resolve(options.rootDir || ".");
|
|
30
|
+
const files = collectFiles(rootDir, options);
|
|
31
|
+
const dependencyGraph = /* @__PURE__ */ new Map();
|
|
32
|
+
const reverseGraph = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
dependencyGraph.set(file, []);
|
|
35
|
+
reverseGraph.set(file, []);
|
|
36
|
+
}
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
try {
|
|
39
|
+
const parser = getParser(file);
|
|
40
|
+
if (!parser) continue;
|
|
41
|
+
const content = fs.readFileSync(file, "utf8");
|
|
42
|
+
const parseResult = parser.parse(content, file);
|
|
43
|
+
const dependencies = parseResult.imports.map((i) => i.source);
|
|
44
|
+
for (const dep of dependencies) {
|
|
45
|
+
const depDir = path.dirname(file);
|
|
46
|
+
const resolvedPath = files.find((f) => {
|
|
47
|
+
if (dep.startsWith(".")) {
|
|
48
|
+
return f.startsWith(path.resolve(depDir, dep));
|
|
49
|
+
} else {
|
|
50
|
+
return f.includes(dep);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
if (resolvedPath) {
|
|
54
|
+
dependencyGraph.get(file)?.push(resolvedPath);
|
|
55
|
+
reverseGraph.get(resolvedPath)?.push(file);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
void err;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const fileMetrics = files.map((file) => {
|
|
63
|
+
const fanOut = dependencyGraph.get(file)?.length || 0;
|
|
64
|
+
const fanIn = reverseGraph.get(file)?.length || 0;
|
|
65
|
+
return { file, fanOut, fanIn };
|
|
66
|
+
});
|
|
67
|
+
const riskResult = calculateChangeAmplification({ files: fileMetrics });
|
|
68
|
+
const results = [];
|
|
69
|
+
for (const hotspot of riskResult.hotspots) {
|
|
70
|
+
const issues = [];
|
|
71
|
+
if (hotspot.amplificationFactor > 20) {
|
|
72
|
+
issues.push({
|
|
73
|
+
type: "change-amplification",
|
|
74
|
+
severity: hotspot.amplificationFactor > 40 ? "critical" : "major",
|
|
75
|
+
message: `High change amplification detected (Factor: ${hotspot.amplificationFactor}). Changes here cascade heavily.`,
|
|
76
|
+
location: { file: hotspot.file, line: 1 },
|
|
77
|
+
suggestion: `Reduce coupling. Fan-out is ${hotspot.fanOut}, Fan-in is ${hotspot.fanIn}.`
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (hotspot.amplificationFactor > 5) {
|
|
81
|
+
results.push({
|
|
82
|
+
fileName: hotspot.file,
|
|
83
|
+
issues,
|
|
84
|
+
metrics: {
|
|
85
|
+
aiSignalClarityScore: 100 - hotspot.amplificationFactor
|
|
86
|
+
// Just a rough score
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
summary: {
|
|
93
|
+
totalFiles: files.length,
|
|
94
|
+
totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),
|
|
95
|
+
criticalIssues: results.reduce(
|
|
96
|
+
(sum, r) => sum + r.issues.filter((i) => i.severity === "critical").length,
|
|
97
|
+
0
|
|
98
|
+
),
|
|
99
|
+
majorIssues: results.reduce(
|
|
100
|
+
(sum, r) => sum + r.issues.filter((i) => i.severity === "major").length,
|
|
101
|
+
0
|
|
102
|
+
),
|
|
103
|
+
score: riskResult.score,
|
|
104
|
+
rating: riskResult.rating,
|
|
105
|
+
recommendations: riskResult.recommendations
|
|
106
|
+
},
|
|
107
|
+
results
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export {
|
|
112
|
+
__require,
|
|
113
|
+
analyzeChangeAmplification
|
|
114
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -50,7 +50,11 @@ function collectFiles(dir, options) {
|
|
|
50
50
|
const excludePatterns = options.exclude && options.exclude.length > 0 ? options.exclude : ["**/node_modules/**", "**/dist/**", "**/.git/**"];
|
|
51
51
|
let matchedFiles = [];
|
|
52
52
|
for (const pattern of includePatterns) {
|
|
53
|
-
const files = (0, import_glob.globSync)(pattern, {
|
|
53
|
+
const files = (0, import_glob.globSync)(pattern, {
|
|
54
|
+
cwd: dir,
|
|
55
|
+
ignore: excludePatterns,
|
|
56
|
+
absolute: true
|
|
57
|
+
});
|
|
54
58
|
matchedFiles = matchedFiles.concat(files);
|
|
55
59
|
}
|
|
56
60
|
return [...new Set(matchedFiles)];
|
|
@@ -86,6 +90,7 @@ async function analyzeChangeAmplification(options) {
|
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
92
|
} catch (err) {
|
|
93
|
+
void err;
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
const fileMetrics = files.map((file) => {
|
|
@@ -121,8 +126,14 @@ async function analyzeChangeAmplification(options) {
|
|
|
121
126
|
summary: {
|
|
122
127
|
totalFiles: files.length,
|
|
123
128
|
totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),
|
|
124
|
-
criticalIssues: results.reduce(
|
|
125
|
-
|
|
129
|
+
criticalIssues: results.reduce(
|
|
130
|
+
(sum, r) => sum + r.issues.filter((i) => i.severity === "critical").length,
|
|
131
|
+
0
|
|
132
|
+
),
|
|
133
|
+
majorIssues: results.reduce(
|
|
134
|
+
(sum, r) => sum + r.issues.filter((i) => i.severity === "major").length,
|
|
135
|
+
0
|
|
136
|
+
),
|
|
126
137
|
score: riskResult.score,
|
|
127
138
|
rating: riskResult.rating,
|
|
128
139
|
recommendations: riskResult.recommendations
|
|
@@ -169,10 +180,17 @@ var changeAmplificationAction = async (directory, options) => {
|
|
|
169
180
|
}
|
|
170
181
|
}
|
|
171
182
|
} else {
|
|
172
|
-
console.log(
|
|
183
|
+
console.log(
|
|
184
|
+
import_chalk.default.green(
|
|
185
|
+
"\n\u2728 No change amplification issues found. Architecture is well contained."
|
|
186
|
+
)
|
|
187
|
+
);
|
|
173
188
|
}
|
|
174
189
|
} catch (error) {
|
|
175
|
-
console.error(
|
|
190
|
+
console.error(
|
|
191
|
+
import_chalk.default.red("Error during change amplification analysis:"),
|
|
192
|
+
error
|
|
193
|
+
);
|
|
176
194
|
process.exit(1);
|
|
177
195
|
}
|
|
178
196
|
};
|
package/dist/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
__require,
|
|
4
4
|
analyzeChangeAmplification
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-A5ACPWWO.mjs";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
8
|
import { Command } from "commander";
|
|
@@ -46,10 +46,17 @@ var changeAmplificationAction = async (directory, options) => {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
} else {
|
|
49
|
-
console.log(
|
|
49
|
+
console.log(
|
|
50
|
+
chalk.green(
|
|
51
|
+
"\n\u2728 No change amplification issues found. Architecture is well contained."
|
|
52
|
+
)
|
|
53
|
+
);
|
|
50
54
|
}
|
|
51
55
|
} catch (error) {
|
|
52
|
-
console.error(
|
|
56
|
+
console.error(
|
|
57
|
+
chalk.red("Error during change amplification analysis:"),
|
|
58
|
+
error
|
|
59
|
+
);
|
|
53
60
|
process.exit(1);
|
|
54
61
|
}
|
|
55
62
|
};
|
package/dist/index.d.mts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -45,7 +45,11 @@ function collectFiles(dir, options) {
|
|
|
45
45
|
const excludePatterns = options.exclude && options.exclude.length > 0 ? options.exclude : ["**/node_modules/**", "**/dist/**", "**/.git/**"];
|
|
46
46
|
let matchedFiles = [];
|
|
47
47
|
for (const pattern of includePatterns) {
|
|
48
|
-
const files = (0, import_glob.globSync)(pattern, {
|
|
48
|
+
const files = (0, import_glob.globSync)(pattern, {
|
|
49
|
+
cwd: dir,
|
|
50
|
+
ignore: excludePatterns,
|
|
51
|
+
absolute: true
|
|
52
|
+
});
|
|
49
53
|
matchedFiles = matchedFiles.concat(files);
|
|
50
54
|
}
|
|
51
55
|
return [...new Set(matchedFiles)];
|
|
@@ -81,6 +85,7 @@ async function analyzeChangeAmplification(options) {
|
|
|
81
85
|
}
|
|
82
86
|
}
|
|
83
87
|
} catch (err) {
|
|
88
|
+
void err;
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
const fileMetrics = files.map((file) => {
|
|
@@ -116,8 +121,14 @@ async function analyzeChangeAmplification(options) {
|
|
|
116
121
|
summary: {
|
|
117
122
|
totalFiles: files.length,
|
|
118
123
|
totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),
|
|
119
|
-
criticalIssues: results.reduce(
|
|
120
|
-
|
|
124
|
+
criticalIssues: results.reduce(
|
|
125
|
+
(sum, r) => sum + r.issues.filter((i) => i.severity === "critical").length,
|
|
126
|
+
0
|
|
127
|
+
),
|
|
128
|
+
majorIssues: results.reduce(
|
|
129
|
+
(sum, r) => sum + r.issues.filter((i) => i.severity === "major").length,
|
|
130
|
+
0
|
|
131
|
+
),
|
|
121
132
|
score: riskResult.score,
|
|
122
133
|
rating: riskResult.rating,
|
|
123
134
|
recommendations: riskResult.recommendations
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/change-amplification",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "AI-Readiness: Change Amplification Detection",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@typescript-eslint/typescript-estree": "^
|
|
10
|
-
"commander": "^
|
|
11
|
-
"glob": "^
|
|
9
|
+
"@typescript-eslint/typescript-estree": "^8.0.0",
|
|
10
|
+
"commander": "^14.0.0",
|
|
11
|
+
"glob": "^13.0.0",
|
|
12
12
|
"chalk": "^5.3.0",
|
|
13
|
-
"@aiready/core": "0.9.
|
|
13
|
+
"@aiready/core": "0.9.33"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@types/node": "^
|
|
17
|
-
"@typescript-eslint/types": "^
|
|
16
|
+
"@types/node": "^24.0.0",
|
|
17
|
+
"@typescript-eslint/types": "^8.0.0",
|
|
18
18
|
"tsup": "^8.0.2",
|
|
19
19
|
"typescript": "^5.4.5",
|
|
20
|
-
"vitest": "^
|
|
20
|
+
"vitest": "^4.0.0"
|
|
21
21
|
},
|
|
22
22
|
"exports": {
|
|
23
23
|
".": {
|
package/src/analyzer.ts
CHANGED
|
@@ -2,30 +2,48 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { globSync } from 'glob';
|
|
4
4
|
import { calculateChangeAmplification } from '@aiready/core';
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
ChangeAmplificationOptions,
|
|
7
|
+
ChangeAmplificationReport,
|
|
8
|
+
FileChangeAmplificationResult,
|
|
9
|
+
ChangeAmplificationIssue,
|
|
10
|
+
} from './types';
|
|
6
11
|
import { getParser } from '@aiready/core';
|
|
7
12
|
|
|
8
|
-
function collectFiles(
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
function collectFiles(
|
|
14
|
+
dir: string,
|
|
15
|
+
options: ChangeAmplificationOptions
|
|
16
|
+
): string[] {
|
|
17
|
+
const includePatterns =
|
|
18
|
+
options.include && options.include.length > 0
|
|
19
|
+
? options.include
|
|
20
|
+
: ['**/*.{ts,tsx,js,jsx,py,go}'];
|
|
21
|
+
const excludePatterns =
|
|
22
|
+
options.exclude && options.exclude.length > 0
|
|
23
|
+
? options.exclude
|
|
24
|
+
: ['**/node_modules/**', '**/dist/**', '**/.git/**'];
|
|
11
25
|
|
|
12
26
|
let matchedFiles: string[] = [];
|
|
13
27
|
for (const pattern of includePatterns) {
|
|
14
|
-
const files = globSync(pattern, {
|
|
28
|
+
const files = globSync(pattern, {
|
|
29
|
+
cwd: dir,
|
|
30
|
+
ignore: excludePatterns,
|
|
31
|
+
absolute: true,
|
|
32
|
+
});
|
|
15
33
|
matchedFiles = matchedFiles.concat(files);
|
|
16
34
|
}
|
|
17
35
|
return [...new Set(matchedFiles)];
|
|
18
36
|
}
|
|
19
37
|
|
|
20
38
|
export async function analyzeChangeAmplification(
|
|
21
|
-
options: ChangeAmplificationOptions
|
|
39
|
+
options: ChangeAmplificationOptions
|
|
22
40
|
): Promise<ChangeAmplificationReport> {
|
|
23
41
|
const rootDir = path.resolve(options.rootDir || '.');
|
|
24
42
|
const files = collectFiles(rootDir, options);
|
|
25
43
|
|
|
26
44
|
// Compute graph metrics: fanIn and fanOut
|
|
27
45
|
const dependencyGraph = new Map<string, string[]>(); // key: file, value: imported files
|
|
28
|
-
const reverseGraph = new Map<string, string[]>();
|
|
46
|
+
const reverseGraph = new Map<string, string[]>(); // key: file, value: files that import it
|
|
29
47
|
|
|
30
48
|
// Initialize graph
|
|
31
49
|
for (const file of files) {
|
|
@@ -41,7 +59,7 @@ export async function analyzeChangeAmplification(
|
|
|
41
59
|
|
|
42
60
|
const content = fs.readFileSync(file, 'utf8');
|
|
43
61
|
const parseResult = parser.parse(content, file);
|
|
44
|
-
const dependencies = parseResult.imports.map(i => i.source);
|
|
62
|
+
const dependencies = parseResult.imports.map((i) => i.source);
|
|
45
63
|
|
|
46
64
|
for (const dep of dependencies) {
|
|
47
65
|
// Resolve simple relative or absolute imports for the graph
|
|
@@ -49,7 +67,7 @@ export async function analyzeChangeAmplification(
|
|
|
49
67
|
const depDir = path.dirname(file);
|
|
50
68
|
|
|
51
69
|
// Find if this dependency resolves to one of our mapped files
|
|
52
|
-
const resolvedPath = files.find(f => {
|
|
70
|
+
const resolvedPath = files.find((f) => {
|
|
53
71
|
if (dep.startsWith('.')) {
|
|
54
72
|
return f.startsWith(path.resolve(depDir, dep));
|
|
55
73
|
} else {
|
|
@@ -63,11 +81,11 @@ export async function analyzeChangeAmplification(
|
|
|
63
81
|
}
|
|
64
82
|
}
|
|
65
83
|
} catch (err) {
|
|
66
|
-
|
|
84
|
+
void err;
|
|
67
85
|
}
|
|
68
86
|
}
|
|
69
87
|
|
|
70
|
-
const fileMetrics = files.map(file => {
|
|
88
|
+
const fileMetrics = files.map((file) => {
|
|
71
89
|
const fanOut = dependencyGraph.get(file)?.length || 0;
|
|
72
90
|
const fanIn = reverseGraph.get(file)?.length || 0;
|
|
73
91
|
return { file, fanOut, fanIn };
|
|
@@ -85,7 +103,7 @@ export async function analyzeChangeAmplification(
|
|
|
85
103
|
severity: hotspot.amplificationFactor > 40 ? 'critical' : 'major',
|
|
86
104
|
message: `High change amplification detected (Factor: ${hotspot.amplificationFactor}). Changes here cascade heavily.`,
|
|
87
105
|
location: { file: hotspot.file, line: 1 },
|
|
88
|
-
suggestion: `Reduce coupling. Fan-out is ${hotspot.fanOut}, Fan-in is ${hotspot.fanIn}
|
|
106
|
+
suggestion: `Reduce coupling. Fan-out is ${hotspot.fanOut}, Fan-in is ${hotspot.fanIn}.`,
|
|
89
107
|
});
|
|
90
108
|
}
|
|
91
109
|
|
|
@@ -96,7 +114,7 @@ export async function analyzeChangeAmplification(
|
|
|
96
114
|
issues,
|
|
97
115
|
metrics: {
|
|
98
116
|
aiSignalClarityScore: 100 - hotspot.amplificationFactor, // Just a rough score
|
|
99
|
-
}
|
|
117
|
+
},
|
|
100
118
|
});
|
|
101
119
|
}
|
|
102
120
|
}
|
|
@@ -105,8 +123,15 @@ export async function analyzeChangeAmplification(
|
|
|
105
123
|
summary: {
|
|
106
124
|
totalFiles: files.length,
|
|
107
125
|
totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),
|
|
108
|
-
criticalIssues: results.reduce(
|
|
109
|
-
|
|
126
|
+
criticalIssues: results.reduce(
|
|
127
|
+
(sum, r) =>
|
|
128
|
+
sum + r.issues.filter((i) => i.severity === 'critical').length,
|
|
129
|
+
0
|
|
130
|
+
),
|
|
131
|
+
majorIssues: results.reduce(
|
|
132
|
+
(sum, r) => sum + r.issues.filter((i) => i.severity === 'major').length,
|
|
133
|
+
0
|
|
134
|
+
),
|
|
110
135
|
score: riskResult.score,
|
|
111
136
|
rating: riskResult.rating,
|
|
112
137
|
recommendations: riskResult.recommendations,
|
package/src/cli.ts
CHANGED
|
@@ -6,7 +6,10 @@ import * as fs from 'fs';
|
|
|
6
6
|
import { analyzeChangeAmplification } from './analyzer';
|
|
7
7
|
import type { ChangeAmplificationOptions } from './types';
|
|
8
8
|
|
|
9
|
-
export const changeAmplificationAction = async (
|
|
9
|
+
export const changeAmplificationAction = async (
|
|
10
|
+
directory: string,
|
|
11
|
+
options: any
|
|
12
|
+
) => {
|
|
10
13
|
try {
|
|
11
14
|
const resolvedDir = path.resolve(process.cwd(), directory);
|
|
12
15
|
const finalOptions: ChangeAmplificationOptions = {
|
|
@@ -18,7 +21,8 @@ export const changeAmplificationAction = async (directory: string, options: any)
|
|
|
18
21
|
const report = await analyzeChangeAmplification(finalOptions);
|
|
19
22
|
|
|
20
23
|
if (options.output === 'json') {
|
|
21
|
-
const outputPath =
|
|
24
|
+
const outputPath =
|
|
25
|
+
options.outputFile || `change-amplification-report-${Date.now()}.json`;
|
|
22
26
|
fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));
|
|
23
27
|
return;
|
|
24
28
|
}
|
|
@@ -41,16 +45,24 @@ export const changeAmplificationAction = async (directory: string, options: any)
|
|
|
41
45
|
for (const result of report.results) {
|
|
42
46
|
console.log(`\n📄 ${chalk.cyan(result.fileName)}`);
|
|
43
47
|
for (const issue of result.issues) {
|
|
44
|
-
const color =
|
|
48
|
+
const color =
|
|
49
|
+
issue.severity === 'critical' ? chalk.red : chalk.yellow;
|
|
45
50
|
console.log(` ${color('■')} ${issue.message}`);
|
|
46
51
|
console.log(` ${chalk.dim('Suggestion: ' + issue.suggestion)}`);
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
} else {
|
|
50
|
-
console.log(
|
|
55
|
+
console.log(
|
|
56
|
+
chalk.green(
|
|
57
|
+
'\n✨ No change amplification issues found. Architecture is well contained.'
|
|
58
|
+
)
|
|
59
|
+
);
|
|
51
60
|
}
|
|
52
61
|
} catch (error) {
|
|
53
|
-
console.error(
|
|
62
|
+
console.error(
|
|
63
|
+
chalk.red('Error during change amplification analysis:'),
|
|
64
|
+
error
|
|
65
|
+
);
|
|
54
66
|
process.exit(1);
|
|
55
67
|
}
|
|
56
68
|
};
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { ScanOptions, Issue } from '@aiready/core';
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
// Add any specific configurations needed for change amplification here
|
|
5
|
-
}
|
|
3
|
+
export type ChangeAmplificationOptions = ScanOptions;
|
|
6
4
|
|
|
7
5
|
export interface ChangeAmplificationIssue extends Issue {
|
|
8
6
|
type: 'change-amplification';
|