@aiready/change-amplification 0.1.4
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 +108 -0
- package/.turbo/turbo-test.log +16 -0
- package/README.md +65 -0
- package/dist/chunk-DS7GBUP2.mjs +100 -0
- package/dist/chunk-VW57ZQRN.mjs +103 -0
- package/dist/cli.d.mts +4 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +187 -0
- package/dist/cli.mjs +63 -0
- package/dist/index.d.mts +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +131 -0
- package/dist/index.mjs +6 -0
- package/package.json +45 -0
- package/src/__tests__/dummy.test.ts +7 -0
- package/src/analyzer.ts +116 -0
- package/src/cli.ts +71 -0
- package/src/index.ts +7 -0
- package/src/types.ts +30 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @aiready/change-amplification@0.1.4 build /Users/pengcao/projects/aiready/packages/change-amplification
|
|
4
|
+
> tsup src/index.ts src/cli.ts --format cjs,esm --dts
|
|
5
|
+
|
|
6
|
+
[34mCLI[39m Building entry: src/cli.ts, src/index.ts
|
|
7
|
+
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
8
|
+
[34mCLI[39m tsup v8.5.1
|
|
9
|
+
[34mCLI[39m Target: es2020
|
|
10
|
+
[34mCJS[39m Build start
|
|
11
|
+
[34mESM[39m Build start
|
|
12
|
+
|
|
13
|
+
[90m[[90m1:03:47 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
|
+
|
|
15
|
+
package.json:33:6:
|
|
16
|
+
[37m 33 ā [32m"types"[37m: "./dist/index.d.ts"
|
|
17
|
+
āµ [32m~~~~~~~[0m
|
|
18
|
+
|
|
19
|
+
The "import" condition comes earlier and will be used for all "import" statements:
|
|
20
|
+
|
|
21
|
+
package.json:31:6:
|
|
22
|
+
[37m 31 ā [32m"import"[37m: "./dist/index.mjs",
|
|
23
|
+
āµ [32m~~~~~~~~[0m
|
|
24
|
+
|
|
25
|
+
The "require" condition comes earlier and will be used for all "require" calls:
|
|
26
|
+
|
|
27
|
+
package.json:32:6:
|
|
28
|
+
[37m 32 ā [32m"require"[37m: "./dist/index.js",
|
|
29
|
+
āµ [32m~~~~~~~~~[0m
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
[90m[[90m1:03:47 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
|
+
|
|
36
|
+
package.json:38:6:
|
|
37
|
+
[37m 38 ā [32m"types"[37m: "./dist/cli.d.ts"
|
|
38
|
+
āµ [32m~~~~~~~[0m
|
|
39
|
+
|
|
40
|
+
The "import" condition comes earlier and will be used for all "import" statements:
|
|
41
|
+
|
|
42
|
+
package.json:36:6:
|
|
43
|
+
[37m 36 ā [32m"import"[37m: "./dist/cli.mjs",
|
|
44
|
+
āµ [32m~~~~~~~~[0m
|
|
45
|
+
|
|
46
|
+
The "require" condition comes earlier and will be used for all "require" calls:
|
|
47
|
+
|
|
48
|
+
package.json:37:6:
|
|
49
|
+
[37m 37 ā [32m"require"[37m: "./dist/cli.js",
|
|
50
|
+
āµ [32m~~~~~~~~~[0m
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
[90m[[90m1:03:47 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
|
+
|
|
57
|
+
package.json:33:6:
|
|
58
|
+
[37m 33 ā [32m"types"[37m: "./dist/index.d.ts"
|
|
59
|
+
āµ [32m~~~~~~~[0m
|
|
60
|
+
|
|
61
|
+
The "import" condition comes earlier and will be used for all "import" statements:
|
|
62
|
+
|
|
63
|
+
package.json:31:6:
|
|
64
|
+
[37m 31 ā [32m"import"[37m: "./dist/index.mjs",
|
|
65
|
+
āµ [32m~~~~~~~~[0m
|
|
66
|
+
|
|
67
|
+
The "require" condition comes earlier and will be used for all "require" calls:
|
|
68
|
+
|
|
69
|
+
package.json:32:6:
|
|
70
|
+
[37m 32 ā [32m"require"[37m: "./dist/index.js",
|
|
71
|
+
āµ [32m~~~~~~~~~[0m
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
[90m[[90m1:03:47 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
|
+
|
|
78
|
+
package.json:38:6:
|
|
79
|
+
[37m 38 ā [32m"types"[37m: "./dist/cli.d.ts"
|
|
80
|
+
āµ [32m~~~~~~~[0m
|
|
81
|
+
|
|
82
|
+
The "import" condition comes earlier and will be used for all "import" statements:
|
|
83
|
+
|
|
84
|
+
package.json:36:6:
|
|
85
|
+
[37m 36 ā [32m"import"[37m: "./dist/cli.mjs",
|
|
86
|
+
āµ [32m~~~~~~~~[0m
|
|
87
|
+
|
|
88
|
+
The "require" condition comes earlier and will be used for all "require" calls:
|
|
89
|
+
|
|
90
|
+
package.json:37:6:
|
|
91
|
+
[37m 37 ā [32m"require"[37m: "./dist/cli.js",
|
|
92
|
+
āµ [32m~~~~~~~~~[0m
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m110.00 B[39m
|
|
97
|
+
[32mESM[39m [1mdist/cli.mjs [22m[32m2.72 KB[39m
|
|
98
|
+
[32mESM[39m [1mdist/chunk-VW57ZQRN.mjs [22m[32m3.83 KB[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
|
|
103
|
+
DTS Build start
|
|
104
|
+
DTS ā”ļø Build success in 7604ms
|
|
105
|
+
DTS dist/cli.d.ts 152.00 B
|
|
106
|
+
DTS dist/index.d.ts 1013.00 B
|
|
107
|
+
DTS dist/cli.d.mts 152.00 B
|
|
108
|
+
DTS dist/index.d.mts 1013.00 B
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @aiready/change-amplification@0.1.4 test /Users/pengcao/projects/aiready/packages/change-amplification
|
|
4
|
+
> vitest run
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
[7m[1m[36m RUN [39m[22m[27m [36mv1.6.1[39m [90m/Users/pengcao/projects/aiready/packages/change-amplification[39m
|
|
8
|
+
|
|
9
|
+
[32mā[39m src/__tests__/dummy.test.ts [2m ([22m[2m1 test[22m[2m)[22m[90m 9[2mms[22m[39m
|
|
10
|
+
|
|
11
|
+
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
12
|
+
[2m Tests [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
13
|
+
[2m Start at [22m 13:04:26
|
|
14
|
+
[2m Duration [22m 2.33s[2m (transform 367ms, setup 0ms, collect 466ms, tests 9ms, environment 0ms, prepare 541ms)[22m
|
|
15
|
+
|
|
16
|
+
[?25h
|
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @aiready/change-amplification
|
|
2
|
+
|
|
3
|
+
> AIReady Spoke: Analyzes architectural coupling and graph metrics to predict how code changes "explode" across the codebase.
|
|
4
|
+
|
|
5
|
+
[](https://npmjs.com/package/@aiready/change-amplification)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
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.
|
|
11
|
+
|
|
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
|
+
|
|
14
|
+
## šļø Architecture
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
šÆ USER
|
|
18
|
+
ā
|
|
19
|
+
ā¼
|
|
20
|
+
šļø @aiready/cli (orchestrator)
|
|
21
|
+
ā ā ā ā ā ā ā ā ā
|
|
22
|
+
ā¼ ā¼ ā¼ ā¼ ā¼ ā¼ ā¼ ā¼ ā¼
|
|
23
|
+
[PAT] [CTX] [CON] [AMP] [DEP] [DOC] [SIG] [AGT] [TST]
|
|
24
|
+
ā ā ā ā ā ā ā ā ā
|
|
25
|
+
āāāāāāā“āāāāāā“āāāāāā“āāāāāā“āāāāāā“āāāāāā“āāāāāā“āāāāāā
|
|
26
|
+
ā
|
|
27
|
+
ā¼
|
|
28
|
+
š¢ @aiready/core
|
|
29
|
+
|
|
30
|
+
Legend:
|
|
31
|
+
PAT = pattern-detect CTX = context-analyzer
|
|
32
|
+
CON = consistency AMP = change-amplification ā
|
|
33
|
+
DEP = deps-health DOC = doc-drift
|
|
34
|
+
SIG = ai-signal-clarity AGT = agent-grounding
|
|
35
|
+
TST = testability ā
= YOU ARE HERE
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Fan-Out Analysis**: Measures how many dependencies a file has (impact of external changes on this file).
|
|
41
|
+
- **Fan-In Analysis**: Measures how many files depend on this one (impact of changes in this file on the system).
|
|
42
|
+
- **Amplification Factor**: A weighted metric predicting the "blast radius" of a single line change.
|
|
43
|
+
- **Hotspot Detection**: Automatically flags files that should be refactored to reduce system-wide fragility.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pnpm add @aiready/change-amplification
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
This tool is designed to be run through the unified AIReady CLI.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Scan for change amplification hotspots
|
|
57
|
+
aiready scan . --tools change-amplification
|
|
58
|
+
|
|
59
|
+
# Get specific results for a directory
|
|
60
|
+
aiready change-amplification ./src
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
|
@@ -0,0 +1,100 @@
|
|
|
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 { parseDependencies } 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, { cwd: dir, ignore: excludePatterns, absolute: true });
|
|
20
|
+
matchedFiles = matchedFiles.concat(files);
|
|
21
|
+
}
|
|
22
|
+
return [...new Set(matchedFiles)];
|
|
23
|
+
}
|
|
24
|
+
async function analyzeChangeAmplification(options) {
|
|
25
|
+
const rootDir = path.resolve(options.rootDir || ".");
|
|
26
|
+
const files = collectFiles(rootDir, options);
|
|
27
|
+
const dependencyGraph = /* @__PURE__ */ new Map();
|
|
28
|
+
const reverseGraph = /* @__PURE__ */ new Map();
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
dependencyGraph.set(file, []);
|
|
31
|
+
reverseGraph.set(file, []);
|
|
32
|
+
}
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
try {
|
|
35
|
+
const content = fs.readFileSync(file, "utf8");
|
|
36
|
+
const dependencies = parseDependencies(content);
|
|
37
|
+
for (const dep of dependencies) {
|
|
38
|
+
const depDir = path.dirname(file);
|
|
39
|
+
const resolvedPath = files.find((f) => {
|
|
40
|
+
if (dep.startsWith(".")) {
|
|
41
|
+
return f.startsWith(path.resolve(depDir, dep));
|
|
42
|
+
} else {
|
|
43
|
+
return f.includes(dep);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if (resolvedPath) {
|
|
47
|
+
dependencyGraph.get(file)?.push(resolvedPath);
|
|
48
|
+
reverseGraph.get(resolvedPath)?.push(file);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const fileMetrics = files.map((file) => {
|
|
55
|
+
const fanOut = dependencyGraph.get(file)?.length || 0;
|
|
56
|
+
const fanIn = reverseGraph.get(file)?.length || 0;
|
|
57
|
+
return { file, fanOut, fanIn };
|
|
58
|
+
});
|
|
59
|
+
const riskResult = calculateChangeAmplification({ files: fileMetrics });
|
|
60
|
+
const results = [];
|
|
61
|
+
for (const hotspot of riskResult.hotspots) {
|
|
62
|
+
const issues = [];
|
|
63
|
+
if (hotspot.amplificationFactor > 20) {
|
|
64
|
+
issues.push({
|
|
65
|
+
type: "change-amplification",
|
|
66
|
+
severity: hotspot.amplificationFactor > 40 ? "critical" : "major",
|
|
67
|
+
message: `High change amplification detected (Factor: ${hotspot.amplificationFactor}). Changes here cascade heavily.`,
|
|
68
|
+
location: { file: hotspot.file, line: 1 },
|
|
69
|
+
suggestion: `Reduce coupling. Fan-out is ${hotspot.fanOut}, Fan-in is ${hotspot.fanIn}.`
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (hotspot.amplificationFactor > 5) {
|
|
73
|
+
results.push({
|
|
74
|
+
fileName: hotspot.file,
|
|
75
|
+
issues,
|
|
76
|
+
metrics: {
|
|
77
|
+
aiSignalClarityScore: 100 - hotspot.amplificationFactor
|
|
78
|
+
// Just a rough score
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
summary: {
|
|
85
|
+
totalFiles: files.length,
|
|
86
|
+
totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),
|
|
87
|
+
criticalIssues: results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === "critical").length, 0),
|
|
88
|
+
majorIssues: results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === "major").length, 0),
|
|
89
|
+
score: riskResult.score,
|
|
90
|
+
rating: riskResult.rating,
|
|
91
|
+
recommendations: riskResult.recommendations
|
|
92
|
+
},
|
|
93
|
+
results
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
__require,
|
|
99
|
+
analyzeChangeAmplification
|
|
100
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
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, { cwd: dir, ignore: excludePatterns, absolute: true });
|
|
20
|
+
matchedFiles = matchedFiles.concat(files);
|
|
21
|
+
}
|
|
22
|
+
return [...new Set(matchedFiles)];
|
|
23
|
+
}
|
|
24
|
+
async function analyzeChangeAmplification(options) {
|
|
25
|
+
const rootDir = path.resolve(options.rootDir || ".");
|
|
26
|
+
const files = collectFiles(rootDir, options);
|
|
27
|
+
const dependencyGraph = /* @__PURE__ */ new Map();
|
|
28
|
+
const reverseGraph = /* @__PURE__ */ new Map();
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
dependencyGraph.set(file, []);
|
|
31
|
+
reverseGraph.set(file, []);
|
|
32
|
+
}
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
try {
|
|
35
|
+
const parser = getParser(file);
|
|
36
|
+
if (!parser) continue;
|
|
37
|
+
const content = fs.readFileSync(file, "utf8");
|
|
38
|
+
const parseResult = parser.parse(content, file);
|
|
39
|
+
const dependencies = parseResult.imports.map((i) => i.source);
|
|
40
|
+
for (const dep of dependencies) {
|
|
41
|
+
const depDir = path.dirname(file);
|
|
42
|
+
const resolvedPath = files.find((f) => {
|
|
43
|
+
if (dep.startsWith(".")) {
|
|
44
|
+
return f.startsWith(path.resolve(depDir, dep));
|
|
45
|
+
} else {
|
|
46
|
+
return f.includes(dep);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
if (resolvedPath) {
|
|
50
|
+
dependencyGraph.get(file)?.push(resolvedPath);
|
|
51
|
+
reverseGraph.get(resolvedPath)?.push(file);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const fileMetrics = files.map((file) => {
|
|
58
|
+
const fanOut = dependencyGraph.get(file)?.length || 0;
|
|
59
|
+
const fanIn = reverseGraph.get(file)?.length || 0;
|
|
60
|
+
return { file, fanOut, fanIn };
|
|
61
|
+
});
|
|
62
|
+
const riskResult = calculateChangeAmplification({ files: fileMetrics });
|
|
63
|
+
const results = [];
|
|
64
|
+
for (const hotspot of riskResult.hotspots) {
|
|
65
|
+
const issues = [];
|
|
66
|
+
if (hotspot.amplificationFactor > 20) {
|
|
67
|
+
issues.push({
|
|
68
|
+
type: "change-amplification",
|
|
69
|
+
severity: hotspot.amplificationFactor > 40 ? "critical" : "major",
|
|
70
|
+
message: `High change amplification detected (Factor: ${hotspot.amplificationFactor}). Changes here cascade heavily.`,
|
|
71
|
+
location: { file: hotspot.file, line: 1 },
|
|
72
|
+
suggestion: `Reduce coupling. Fan-out is ${hotspot.fanOut}, Fan-in is ${hotspot.fanIn}.`
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (hotspot.amplificationFactor > 5) {
|
|
76
|
+
results.push({
|
|
77
|
+
fileName: hotspot.file,
|
|
78
|
+
issues,
|
|
79
|
+
metrics: {
|
|
80
|
+
aiSignalClarityScore: 100 - hotspot.amplificationFactor
|
|
81
|
+
// Just a rough score
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
summary: {
|
|
88
|
+
totalFiles: files.length,
|
|
89
|
+
totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),
|
|
90
|
+
criticalIssues: results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === "critical").length, 0),
|
|
91
|
+
majorIssues: results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === "major").length, 0),
|
|
92
|
+
score: riskResult.score,
|
|
93
|
+
rating: riskResult.rating,
|
|
94
|
+
recommendations: riskResult.recommendations
|
|
95
|
+
},
|
|
96
|
+
results
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
__require,
|
|
102
|
+
analyzeChangeAmplification
|
|
103
|
+
};
|
package/dist/cli.d.mts
ADDED
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/cli.ts
|
|
32
|
+
var cli_exports = {};
|
|
33
|
+
__export(cli_exports, {
|
|
34
|
+
changeAmplificationAction: () => changeAmplificationAction
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(cli_exports);
|
|
37
|
+
var import_commander = require("commander");
|
|
38
|
+
var import_chalk = __toESM(require("chalk"));
|
|
39
|
+
var path2 = __toESM(require("path"));
|
|
40
|
+
var fs2 = __toESM(require("fs"));
|
|
41
|
+
|
|
42
|
+
// src/analyzer.ts
|
|
43
|
+
var fs = __toESM(require("fs"));
|
|
44
|
+
var path = __toESM(require("path"));
|
|
45
|
+
var import_glob = require("glob");
|
|
46
|
+
var import_core = require("@aiready/core");
|
|
47
|
+
var import_core2 = require("@aiready/core");
|
|
48
|
+
function collectFiles(dir, options) {
|
|
49
|
+
const includePatterns = options.include && options.include.length > 0 ? options.include : ["**/*.{ts,tsx,js,jsx,py,go}"];
|
|
50
|
+
const excludePatterns = options.exclude && options.exclude.length > 0 ? options.exclude : ["**/node_modules/**", "**/dist/**", "**/.git/**"];
|
|
51
|
+
let matchedFiles = [];
|
|
52
|
+
for (const pattern of includePatterns) {
|
|
53
|
+
const files = (0, import_glob.globSync)(pattern, { cwd: dir, ignore: excludePatterns, absolute: true });
|
|
54
|
+
matchedFiles = matchedFiles.concat(files);
|
|
55
|
+
}
|
|
56
|
+
return [...new Set(matchedFiles)];
|
|
57
|
+
}
|
|
58
|
+
async function analyzeChangeAmplification(options) {
|
|
59
|
+
const rootDir = path.resolve(options.rootDir || ".");
|
|
60
|
+
const files = collectFiles(rootDir, options);
|
|
61
|
+
const dependencyGraph = /* @__PURE__ */ new Map();
|
|
62
|
+
const reverseGraph = /* @__PURE__ */ new Map();
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
dependencyGraph.set(file, []);
|
|
65
|
+
reverseGraph.set(file, []);
|
|
66
|
+
}
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
try {
|
|
69
|
+
const parser = (0, import_core2.getParser)(file);
|
|
70
|
+
if (!parser) continue;
|
|
71
|
+
const content = fs.readFileSync(file, "utf8");
|
|
72
|
+
const parseResult = parser.parse(content, file);
|
|
73
|
+
const dependencies = parseResult.imports.map((i) => i.source);
|
|
74
|
+
for (const dep of dependencies) {
|
|
75
|
+
const depDir = path.dirname(file);
|
|
76
|
+
const resolvedPath = files.find((f) => {
|
|
77
|
+
if (dep.startsWith(".")) {
|
|
78
|
+
return f.startsWith(path.resolve(depDir, dep));
|
|
79
|
+
} else {
|
|
80
|
+
return f.includes(dep);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
if (resolvedPath) {
|
|
84
|
+
dependencyGraph.get(file)?.push(resolvedPath);
|
|
85
|
+
reverseGraph.get(resolvedPath)?.push(file);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const fileMetrics = files.map((file) => {
|
|
92
|
+
const fanOut = dependencyGraph.get(file)?.length || 0;
|
|
93
|
+
const fanIn = reverseGraph.get(file)?.length || 0;
|
|
94
|
+
return { file, fanOut, fanIn };
|
|
95
|
+
});
|
|
96
|
+
const riskResult = (0, import_core.calculateChangeAmplification)({ files: fileMetrics });
|
|
97
|
+
const results = [];
|
|
98
|
+
for (const hotspot of riskResult.hotspots) {
|
|
99
|
+
const issues = [];
|
|
100
|
+
if (hotspot.amplificationFactor > 20) {
|
|
101
|
+
issues.push({
|
|
102
|
+
type: "change-amplification",
|
|
103
|
+
severity: hotspot.amplificationFactor > 40 ? "critical" : "major",
|
|
104
|
+
message: `High change amplification detected (Factor: ${hotspot.amplificationFactor}). Changes here cascade heavily.`,
|
|
105
|
+
location: { file: hotspot.file, line: 1 },
|
|
106
|
+
suggestion: `Reduce coupling. Fan-out is ${hotspot.fanOut}, Fan-in is ${hotspot.fanIn}.`
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (hotspot.amplificationFactor > 5) {
|
|
110
|
+
results.push({
|
|
111
|
+
fileName: hotspot.file,
|
|
112
|
+
issues,
|
|
113
|
+
metrics: {
|
|
114
|
+
aiSignalClarityScore: 100 - hotspot.amplificationFactor
|
|
115
|
+
// Just a rough score
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
summary: {
|
|
122
|
+
totalFiles: files.length,
|
|
123
|
+
totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),
|
|
124
|
+
criticalIssues: results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === "critical").length, 0),
|
|
125
|
+
majorIssues: results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === "major").length, 0),
|
|
126
|
+
score: riskResult.score,
|
|
127
|
+
rating: riskResult.rating,
|
|
128
|
+
recommendations: riskResult.recommendations
|
|
129
|
+
},
|
|
130
|
+
results
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/cli.ts
|
|
135
|
+
var changeAmplificationAction = async (directory, options) => {
|
|
136
|
+
try {
|
|
137
|
+
const resolvedDir = path2.resolve(process.cwd(), directory);
|
|
138
|
+
const finalOptions = {
|
|
139
|
+
rootDir: resolvedDir,
|
|
140
|
+
include: options.include ? options.include.split(",") : void 0,
|
|
141
|
+
exclude: options.exclude ? options.exclude.split(",") : void 0
|
|
142
|
+
};
|
|
143
|
+
const report = await analyzeChangeAmplification(finalOptions);
|
|
144
|
+
if (options.output === "json") {
|
|
145
|
+
const outputPath = options.outputFile || `change-amplification-report-${Date.now()}.json`;
|
|
146
|
+
fs2.writeFileSync(outputPath, JSON.stringify(report, null, 2));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
console.log(import_chalk.default.bold("\n\u{1F310} Change Amplification Analysis\n"));
|
|
150
|
+
console.log(`Rating: ${import_chalk.default.bold(report.summary.rating)}`);
|
|
151
|
+
console.log(`Score: ${Math.round(report.summary.score)}/100`);
|
|
152
|
+
console.log(`Critical Issues: ${report.summary.criticalIssues}`);
|
|
153
|
+
console.log(`Major Issues: ${report.summary.majorIssues}`);
|
|
154
|
+
if (report.summary.recommendations.length > 0) {
|
|
155
|
+
console.log(import_chalk.default.bold("\nRecommendations:"));
|
|
156
|
+
for (const rec of report.summary.recommendations) {
|
|
157
|
+
console.log(import_chalk.default.cyan(`\u2022 ${rec}`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (report.results.length > 0) {
|
|
161
|
+
console.log(import_chalk.default.bold("\nHotspots:"));
|
|
162
|
+
for (const result of report.results) {
|
|
163
|
+
console.log(`
|
|
164
|
+
\u{1F4C4} ${import_chalk.default.cyan(result.fileName)}`);
|
|
165
|
+
for (const issue of result.issues) {
|
|
166
|
+
const color = issue.severity === "critical" ? import_chalk.default.red : import_chalk.default.yellow;
|
|
167
|
+
console.log(` ${color("\u25A0")} ${issue.message}`);
|
|
168
|
+
console.log(` ${import_chalk.default.dim("Suggestion: " + issue.suggestion)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
console.log(import_chalk.default.green("\n\u2728 No change amplification issues found. Architecture is well contained."));
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error(import_chalk.default.red("Error during change amplification analysis:"), error);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var program = new import_commander.Command();
|
|
180
|
+
program.name("aiready-change-amplification").description("Analyze graph metrics for change amplification").argument("[directory]", "Directory to analyze", ".").option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-o, --output <format>", "Output format: console, json", "console").option("--output-file <path>", "Output file path (for json)").action(changeAmplificationAction);
|
|
181
|
+
if (require.main === module) {
|
|
182
|
+
program.parse();
|
|
183
|
+
}
|
|
184
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
185
|
+
0 && (module.exports = {
|
|
186
|
+
changeAmplificationAction
|
|
187
|
+
});
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
__require,
|
|
4
|
+
analyzeChangeAmplification
|
|
5
|
+
} from "./chunk-VW57ZQRN.mjs";
|
|
6
|
+
|
|
7
|
+
// src/cli.ts
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
var changeAmplificationAction = async (directory, options) => {
|
|
13
|
+
try {
|
|
14
|
+
const resolvedDir = path.resolve(process.cwd(), directory);
|
|
15
|
+
const finalOptions = {
|
|
16
|
+
rootDir: resolvedDir,
|
|
17
|
+
include: options.include ? options.include.split(",") : void 0,
|
|
18
|
+
exclude: options.exclude ? options.exclude.split(",") : void 0
|
|
19
|
+
};
|
|
20
|
+
const report = await analyzeChangeAmplification(finalOptions);
|
|
21
|
+
if (options.output === "json") {
|
|
22
|
+
const outputPath = options.outputFile || `change-amplification-report-${Date.now()}.json`;
|
|
23
|
+
fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
console.log(chalk.bold("\n\u{1F310} Change Amplification Analysis\n"));
|
|
27
|
+
console.log(`Rating: ${chalk.bold(report.summary.rating)}`);
|
|
28
|
+
console.log(`Score: ${Math.round(report.summary.score)}/100`);
|
|
29
|
+
console.log(`Critical Issues: ${report.summary.criticalIssues}`);
|
|
30
|
+
console.log(`Major Issues: ${report.summary.majorIssues}`);
|
|
31
|
+
if (report.summary.recommendations.length > 0) {
|
|
32
|
+
console.log(chalk.bold("\nRecommendations:"));
|
|
33
|
+
for (const rec of report.summary.recommendations) {
|
|
34
|
+
console.log(chalk.cyan(`\u2022 ${rec}`));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (report.results.length > 0) {
|
|
38
|
+
console.log(chalk.bold("\nHotspots:"));
|
|
39
|
+
for (const result of report.results) {
|
|
40
|
+
console.log(`
|
|
41
|
+
\u{1F4C4} ${chalk.cyan(result.fileName)}`);
|
|
42
|
+
for (const issue of result.issues) {
|
|
43
|
+
const color = issue.severity === "critical" ? chalk.red : chalk.yellow;
|
|
44
|
+
console.log(` ${color("\u25A0")} ${issue.message}`);
|
|
45
|
+
console.log(` ${chalk.dim("Suggestion: " + issue.suggestion)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
console.log(chalk.green("\n\u2728 No change amplification issues found. Architecture is well contained."));
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(chalk.red("Error during change amplification analysis:"), error);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var program = new Command();
|
|
57
|
+
program.name("aiready-change-amplification").description("Analyze graph metrics for change amplification").argument("[directory]", "Directory to analyze", ".").option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-o, --output <format>", "Output format: console, json", "console").option("--output-file <path>", "Output file path (for json)").action(changeAmplificationAction);
|
|
58
|
+
if (__require.main === module) {
|
|
59
|
+
program.parse();
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
changeAmplificationAction
|
|
63
|
+
};
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Issue, ScanOptions } from '@aiready/core';
|
|
2
|
+
|
|
3
|
+
interface ChangeAmplificationOptions extends ScanOptions {
|
|
4
|
+
}
|
|
5
|
+
interface ChangeAmplificationIssue extends Issue {
|
|
6
|
+
type: 'change-amplification';
|
|
7
|
+
}
|
|
8
|
+
interface FileChangeAmplificationResult {
|
|
9
|
+
fileName: string;
|
|
10
|
+
issues: ChangeAmplificationIssue[];
|
|
11
|
+
metrics: {
|
|
12
|
+
aiSignalClarityScore?: number;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
interface ChangeAmplificationReport {
|
|
16
|
+
summary: {
|
|
17
|
+
totalFiles: number;
|
|
18
|
+
totalIssues: number;
|
|
19
|
+
criticalIssues: number;
|
|
20
|
+
majorIssues: number;
|
|
21
|
+
score: number;
|
|
22
|
+
rating: 'isolated' | 'contained' | 'amplified' | 'explosive';
|
|
23
|
+
recommendations: string[];
|
|
24
|
+
};
|
|
25
|
+
results: FileChangeAmplificationResult[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare function analyzeChangeAmplification(options: ChangeAmplificationOptions): Promise<ChangeAmplificationReport>;
|
|
29
|
+
|
|
30
|
+
export { type ChangeAmplificationIssue, type ChangeAmplificationOptions, type ChangeAmplificationReport, type FileChangeAmplificationResult, analyzeChangeAmplification };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Issue, ScanOptions } from '@aiready/core';
|
|
2
|
+
|
|
3
|
+
interface ChangeAmplificationOptions extends ScanOptions {
|
|
4
|
+
}
|
|
5
|
+
interface ChangeAmplificationIssue extends Issue {
|
|
6
|
+
type: 'change-amplification';
|
|
7
|
+
}
|
|
8
|
+
interface FileChangeAmplificationResult {
|
|
9
|
+
fileName: string;
|
|
10
|
+
issues: ChangeAmplificationIssue[];
|
|
11
|
+
metrics: {
|
|
12
|
+
aiSignalClarityScore?: number;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
interface ChangeAmplificationReport {
|
|
16
|
+
summary: {
|
|
17
|
+
totalFiles: number;
|
|
18
|
+
totalIssues: number;
|
|
19
|
+
criticalIssues: number;
|
|
20
|
+
majorIssues: number;
|
|
21
|
+
score: number;
|
|
22
|
+
rating: 'isolated' | 'contained' | 'amplified' | 'explosive';
|
|
23
|
+
recommendations: string[];
|
|
24
|
+
};
|
|
25
|
+
results: FileChangeAmplificationResult[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare function analyzeChangeAmplification(options: ChangeAmplificationOptions): Promise<ChangeAmplificationReport>;
|
|
29
|
+
|
|
30
|
+
export { type ChangeAmplificationIssue, type ChangeAmplificationOptions, type ChangeAmplificationReport, type FileChangeAmplificationResult, analyzeChangeAmplification };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
analyzeChangeAmplification: () => analyzeChangeAmplification
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/analyzer.ts
|
|
38
|
+
var fs = __toESM(require("fs"));
|
|
39
|
+
var path = __toESM(require("path"));
|
|
40
|
+
var import_glob = require("glob");
|
|
41
|
+
var import_core = require("@aiready/core");
|
|
42
|
+
var import_core2 = require("@aiready/core");
|
|
43
|
+
function collectFiles(dir, options) {
|
|
44
|
+
const includePatterns = options.include && options.include.length > 0 ? options.include : ["**/*.{ts,tsx,js,jsx,py,go}"];
|
|
45
|
+
const excludePatterns = options.exclude && options.exclude.length > 0 ? options.exclude : ["**/node_modules/**", "**/dist/**", "**/.git/**"];
|
|
46
|
+
let matchedFiles = [];
|
|
47
|
+
for (const pattern of includePatterns) {
|
|
48
|
+
const files = (0, import_glob.globSync)(pattern, { cwd: dir, ignore: excludePatterns, absolute: true });
|
|
49
|
+
matchedFiles = matchedFiles.concat(files);
|
|
50
|
+
}
|
|
51
|
+
return [...new Set(matchedFiles)];
|
|
52
|
+
}
|
|
53
|
+
async function analyzeChangeAmplification(options) {
|
|
54
|
+
const rootDir = path.resolve(options.rootDir || ".");
|
|
55
|
+
const files = collectFiles(rootDir, options);
|
|
56
|
+
const dependencyGraph = /* @__PURE__ */ new Map();
|
|
57
|
+
const reverseGraph = /* @__PURE__ */ new Map();
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
dependencyGraph.set(file, []);
|
|
60
|
+
reverseGraph.set(file, []);
|
|
61
|
+
}
|
|
62
|
+
for (const file of files) {
|
|
63
|
+
try {
|
|
64
|
+
const parser = (0, import_core2.getParser)(file);
|
|
65
|
+
if (!parser) continue;
|
|
66
|
+
const content = fs.readFileSync(file, "utf8");
|
|
67
|
+
const parseResult = parser.parse(content, file);
|
|
68
|
+
const dependencies = parseResult.imports.map((i) => i.source);
|
|
69
|
+
for (const dep of dependencies) {
|
|
70
|
+
const depDir = path.dirname(file);
|
|
71
|
+
const resolvedPath = files.find((f) => {
|
|
72
|
+
if (dep.startsWith(".")) {
|
|
73
|
+
return f.startsWith(path.resolve(depDir, dep));
|
|
74
|
+
} else {
|
|
75
|
+
return f.includes(dep);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
if (resolvedPath) {
|
|
79
|
+
dependencyGraph.get(file)?.push(resolvedPath);
|
|
80
|
+
reverseGraph.get(resolvedPath)?.push(file);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const fileMetrics = files.map((file) => {
|
|
87
|
+
const fanOut = dependencyGraph.get(file)?.length || 0;
|
|
88
|
+
const fanIn = reverseGraph.get(file)?.length || 0;
|
|
89
|
+
return { file, fanOut, fanIn };
|
|
90
|
+
});
|
|
91
|
+
const riskResult = (0, import_core.calculateChangeAmplification)({ files: fileMetrics });
|
|
92
|
+
const results = [];
|
|
93
|
+
for (const hotspot of riskResult.hotspots) {
|
|
94
|
+
const issues = [];
|
|
95
|
+
if (hotspot.amplificationFactor > 20) {
|
|
96
|
+
issues.push({
|
|
97
|
+
type: "change-amplification",
|
|
98
|
+
severity: hotspot.amplificationFactor > 40 ? "critical" : "major",
|
|
99
|
+
message: `High change amplification detected (Factor: ${hotspot.amplificationFactor}). Changes here cascade heavily.`,
|
|
100
|
+
location: { file: hotspot.file, line: 1 },
|
|
101
|
+
suggestion: `Reduce coupling. Fan-out is ${hotspot.fanOut}, Fan-in is ${hotspot.fanIn}.`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (hotspot.amplificationFactor > 5) {
|
|
105
|
+
results.push({
|
|
106
|
+
fileName: hotspot.file,
|
|
107
|
+
issues,
|
|
108
|
+
metrics: {
|
|
109
|
+
aiSignalClarityScore: 100 - hotspot.amplificationFactor
|
|
110
|
+
// Just a rough score
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
summary: {
|
|
117
|
+
totalFiles: files.length,
|
|
118
|
+
totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),
|
|
119
|
+
criticalIssues: results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === "critical").length, 0),
|
|
120
|
+
majorIssues: results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === "major").length, 0),
|
|
121
|
+
score: riskResult.score,
|
|
122
|
+
rating: riskResult.rating,
|
|
123
|
+
recommendations: riskResult.recommendations
|
|
124
|
+
},
|
|
125
|
+
results
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
129
|
+
0 && (module.exports = {
|
|
130
|
+
analyzeChangeAmplification
|
|
131
|
+
});
|
package/dist/index.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aiready/change-amplification",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "AI-Readiness: Change Amplification Detection",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@typescript-eslint/typescript-estree": "^7.8.0",
|
|
10
|
+
"commander": "^12.0.0",
|
|
11
|
+
"glob": "^10.3.12",
|
|
12
|
+
"chalk": "^5.3.0",
|
|
13
|
+
"@aiready/core": "0.9.31"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/node": "^20.12.7",
|
|
17
|
+
"@typescript-eslint/types": "^7.8.0",
|
|
18
|
+
"tsup": "^8.0.2",
|
|
19
|
+
"typescript": "^5.4.5",
|
|
20
|
+
"vitest": "^1.6.0"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"import": "./dist/index.mjs",
|
|
25
|
+
"require": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts"
|
|
27
|
+
},
|
|
28
|
+
"./cli": {
|
|
29
|
+
"import": "./dist/cli.mjs",
|
|
30
|
+
"require": "./dist/cli.js",
|
|
31
|
+
"types": "./dist/cli.d.ts"
|
|
32
|
+
},
|
|
33
|
+
"./dist/cli.js": {
|
|
34
|
+
"require": "./dist/cli.js",
|
|
35
|
+
"import": "./dist/cli.mjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup src/index.ts src/cli.ts --format cjs,esm --dts",
|
|
40
|
+
"dev": "tsup src/index.ts src/cli.ts --format cjs,esm --watch",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest",
|
|
43
|
+
"lint": "eslint src --ext .ts"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/analyzer.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { globSync } from 'glob';
|
|
4
|
+
import { calculateChangeAmplification } from '@aiready/core';
|
|
5
|
+
import type { ChangeAmplificationOptions, ChangeAmplificationReport, FileChangeAmplificationResult, ChangeAmplificationIssue } from './types';
|
|
6
|
+
import { getParser } from '@aiready/core';
|
|
7
|
+
|
|
8
|
+
function collectFiles(dir: string, options: ChangeAmplificationOptions): string[] {
|
|
9
|
+
const includePatterns = options.include && options.include.length > 0 ? options.include : ['**/*.{ts,tsx,js,jsx,py,go}'];
|
|
10
|
+
const excludePatterns = options.exclude && options.exclude.length > 0 ? options.exclude : ['**/node_modules/**', '**/dist/**', '**/.git/**'];
|
|
11
|
+
|
|
12
|
+
let matchedFiles: string[] = [];
|
|
13
|
+
for (const pattern of includePatterns) {
|
|
14
|
+
const files = globSync(pattern, { cwd: dir, ignore: excludePatterns, absolute: true });
|
|
15
|
+
matchedFiles = matchedFiles.concat(files);
|
|
16
|
+
}
|
|
17
|
+
return [...new Set(matchedFiles)];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function analyzeChangeAmplification(
|
|
21
|
+
options: ChangeAmplificationOptions,
|
|
22
|
+
): Promise<ChangeAmplificationReport> {
|
|
23
|
+
const rootDir = path.resolve(options.rootDir || '.');
|
|
24
|
+
const files = collectFiles(rootDir, options);
|
|
25
|
+
|
|
26
|
+
// Compute graph metrics: fanIn and fanOut
|
|
27
|
+
const dependencyGraph = new Map<string, string[]>(); // key: file, value: imported files
|
|
28
|
+
const reverseGraph = new Map<string, string[]>(); // key: file, value: files that import it
|
|
29
|
+
|
|
30
|
+
// Initialize graph
|
|
31
|
+
for (const file of files) {
|
|
32
|
+
dependencyGraph.set(file, []);
|
|
33
|
+
reverseGraph.set(file, []);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Parse files to build dependency graph
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
try {
|
|
39
|
+
const parser = getParser(file);
|
|
40
|
+
if (!parser) continue;
|
|
41
|
+
|
|
42
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
43
|
+
const parseResult = parser.parse(content, file);
|
|
44
|
+
const dependencies = parseResult.imports.map(i => i.source);
|
|
45
|
+
|
|
46
|
+
for (const dep of dependencies) {
|
|
47
|
+
// Resolve simple relative or absolute imports for the graph
|
|
48
|
+
// This is a simplified resolution for demonstration purposes
|
|
49
|
+
const depDir = path.dirname(file);
|
|
50
|
+
|
|
51
|
+
// Find if this dependency resolves to one of our mapped files
|
|
52
|
+
const resolvedPath = files.find(f => {
|
|
53
|
+
if (dep.startsWith('.')) {
|
|
54
|
+
return f.startsWith(path.resolve(depDir, dep));
|
|
55
|
+
} else {
|
|
56
|
+
return f.includes(dep);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (resolvedPath) {
|
|
61
|
+
dependencyGraph.get(file)?.push(resolvedPath);
|
|
62
|
+
reverseGraph.get(resolvedPath)?.push(file);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
// Ignore parse errors silently
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const fileMetrics = files.map(file => {
|
|
71
|
+
const fanOut = dependencyGraph.get(file)?.length || 0;
|
|
72
|
+
const fanIn = reverseGraph.get(file)?.length || 0;
|
|
73
|
+
return { file, fanOut, fanIn };
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const riskResult = calculateChangeAmplification({ files: fileMetrics });
|
|
77
|
+
|
|
78
|
+
const results: FileChangeAmplificationResult[] = [];
|
|
79
|
+
|
|
80
|
+
for (const hotspot of riskResult.hotspots) {
|
|
81
|
+
const issues: ChangeAmplificationIssue[] = [];
|
|
82
|
+
if (hotspot.amplificationFactor > 20) {
|
|
83
|
+
issues.push({
|
|
84
|
+
type: 'change-amplification',
|
|
85
|
+
severity: hotspot.amplificationFactor > 40 ? 'critical' : 'major',
|
|
86
|
+
message: `High change amplification detected (Factor: ${hotspot.amplificationFactor}). Changes here cascade heavily.`,
|
|
87
|
+
location: { file: hotspot.file, line: 1 },
|
|
88
|
+
suggestion: `Reduce coupling. Fan-out is ${hotspot.fanOut}, Fan-in is ${hotspot.fanIn}.`
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// We only push results for files that have either high fan-in or fan-out
|
|
93
|
+
if (hotspot.amplificationFactor > 5) {
|
|
94
|
+
results.push({
|
|
95
|
+
fileName: hotspot.file,
|
|
96
|
+
issues,
|
|
97
|
+
metrics: {
|
|
98
|
+
aiSignalClarityScore: 100 - hotspot.amplificationFactor, // Just a rough score
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
summary: {
|
|
106
|
+
totalFiles: files.length,
|
|
107
|
+
totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),
|
|
108
|
+
criticalIssues: results.reduce((sum, r) => sum + r.issues.filter(i => i.severity === 'critical').length, 0),
|
|
109
|
+
majorIssues: results.reduce((sum, r) => sum + r.issues.filter(i => i.severity === 'major').length, 0),
|
|
110
|
+
score: riskResult.score,
|
|
111
|
+
rating: riskResult.rating,
|
|
112
|
+
recommendations: riskResult.recommendations,
|
|
113
|
+
},
|
|
114
|
+
results,
|
|
115
|
+
};
|
|
116
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import { analyzeChangeAmplification } from './analyzer';
|
|
7
|
+
import type { ChangeAmplificationOptions } from './types';
|
|
8
|
+
|
|
9
|
+
export const changeAmplificationAction = async (directory: string, options: any) => {
|
|
10
|
+
try {
|
|
11
|
+
const resolvedDir = path.resolve(process.cwd(), directory);
|
|
12
|
+
const finalOptions: ChangeAmplificationOptions = {
|
|
13
|
+
rootDir: resolvedDir,
|
|
14
|
+
include: options.include ? options.include.split(',') : undefined,
|
|
15
|
+
exclude: options.exclude ? options.exclude.split(',') : undefined,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const report = await analyzeChangeAmplification(finalOptions);
|
|
19
|
+
|
|
20
|
+
if (options.output === 'json') {
|
|
21
|
+
const outputPath = options.outputFile || `change-amplification-report-${Date.now()}.json`;
|
|
22
|
+
fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(chalk.bold('\nš Change Amplification Analysis\n'));
|
|
27
|
+
console.log(`Rating: ${chalk.bold(report.summary.rating)}`);
|
|
28
|
+
console.log(`Score: ${Math.round(report.summary.score)}/100`);
|
|
29
|
+
console.log(`Critical Issues: ${report.summary.criticalIssues}`);
|
|
30
|
+
console.log(`Major Issues: ${report.summary.majorIssues}`);
|
|
31
|
+
|
|
32
|
+
if (report.summary.recommendations.length > 0) {
|
|
33
|
+
console.log(chalk.bold('\nRecommendations:'));
|
|
34
|
+
for (const rec of report.summary.recommendations) {
|
|
35
|
+
console.log(chalk.cyan(`⢠${rec}`));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (report.results.length > 0) {
|
|
40
|
+
console.log(chalk.bold('\nHotspots:'));
|
|
41
|
+
for (const result of report.results) {
|
|
42
|
+
console.log(`\nš ${chalk.cyan(result.fileName)}`);
|
|
43
|
+
for (const issue of result.issues) {
|
|
44
|
+
const color = issue.severity === 'critical' ? chalk.red : chalk.yellow;
|
|
45
|
+
console.log(` ${color('ā ')} ${issue.message}`);
|
|
46
|
+
console.log(` ${chalk.dim('Suggestion: ' + issue.suggestion)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
console.log(chalk.green('\n⨠No change amplification issues found. Architecture is well contained.'));
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(chalk.red('Error during change amplification analysis:'), error);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const program = new Command();
|
|
59
|
+
program
|
|
60
|
+
.name('aiready-change-amplification')
|
|
61
|
+
.description('Analyze graph metrics for change amplification')
|
|
62
|
+
.argument('[directory]', 'Directory to analyze', '.')
|
|
63
|
+
.option('--include <patterns>', 'File patterns to include (comma-separated)')
|
|
64
|
+
.option('--exclude <patterns>', 'File patterns to exclude (comma-separated)')
|
|
65
|
+
.option('-o, --output <format>', 'Output format: console, json', 'console')
|
|
66
|
+
.option('--output-file <path>', 'Output file path (for json)')
|
|
67
|
+
.action(changeAmplificationAction);
|
|
68
|
+
|
|
69
|
+
if (require.main === module) {
|
|
70
|
+
program.parse();
|
|
71
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ScanOptions, Issue } from '@aiready/core';
|
|
2
|
+
|
|
3
|
+
export interface ChangeAmplificationOptions extends ScanOptions {
|
|
4
|
+
// Add any specific configurations needed for change amplification here
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ChangeAmplificationIssue extends Issue {
|
|
8
|
+
type: 'change-amplification';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FileChangeAmplificationResult {
|
|
12
|
+
fileName: string;
|
|
13
|
+
issues: ChangeAmplificationIssue[];
|
|
14
|
+
metrics: {
|
|
15
|
+
aiSignalClarityScore?: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ChangeAmplificationReport {
|
|
20
|
+
summary: {
|
|
21
|
+
totalFiles: number;
|
|
22
|
+
totalIssues: number;
|
|
23
|
+
criticalIssues: number;
|
|
24
|
+
majorIssues: number;
|
|
25
|
+
score: number;
|
|
26
|
+
rating: 'isolated' | 'contained' | 'amplified' | 'explosive';
|
|
27
|
+
recommendations: string[];
|
|
28
|
+
};
|
|
29
|
+
results: FileChangeAmplificationResult[];
|
|
30
|
+
}
|