@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.
@@ -1,6 +1,6 @@
1
1
 
2
2
  
3
- > @aiready/change-amplification@0.11.3 build /Users/pengcao/projects/aiready/packages/change-amplification
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
  CLI Building entry: src/cli.ts, src/index.ts
@@ -9,15 +9,15 @@
9
9
  CLI Target: es2020
10
10
  CJS Build start
11
11
  ESM Build start
12
- CJS dist/cli.js 7.98 KB
13
- CJS dist/index.js 6.37 KB
14
- CJS ⚡️ Build success in 81ms
15
12
  ESM dist/index.mjs 1.25 KB
16
- ESM dist/chunk-GLHIV53G.mjs 3.79 KB
13
+ ESM dist/chunk-OBBL7HKE.mjs 4.82 KB
17
14
  ESM dist/cli.mjs 2.78 KB
18
- ESM ⚡️ Build success in 85ms
15
+ ESM ⚡️ Build success in 125ms
16
+ CJS dist/cli.js 9.01 KB
17
+ CJS dist/index.js 7.40 KB
18
+ CJS ⚡️ Build success in 125ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 4176ms
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
@@ -1,16 +1,21 @@
1
1
 
2
2
  
3
- > @aiready/change-amplification@0.11.3 test /Users/pengcao/projects/aiready/packages/change-amplification
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
   RUN  v4.0.18 /Users/pengcao/projects/aiready/packages/change-amplification
8
8
 
9
- ✓ src/__tests__/dummy.test.ts (1 test) 1ms
9
+ ✓ src/__tests__/dummy.test.ts (1 test) 16ms
10
+ stdout | src/__tests__/analyzer.test.ts > analyzeChangeAmplification reproduction > should see how it gets to 0
11
+ Resulting score for highly coupled: 27
12
+ Rating: explosive
10
13
 
11
-  Test Files  1 passed (1)
12
-  Tests  1 passed (1)
13
-  Start at  11:12:32
14
-  Duration  276ms (transform 35ms, setup 0ms, import 44ms, tests 1ms, environment 0ms)
14
+ ✓ src/__tests__/analyzer.test.ts (2 tests) 5ms
15
+
16
+  Test Files  2 passed (2)
17
+  Tests  3 passed (3)
18
+  Start at  13:29:31
19
+  Duration  2.10s (transform 234ms, setup 0ms, import 1.65s, tests 21ms, environment 0ms)
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
- options.onProgress?.(
61
- processed,
62
- files.length,
63
- `change-amplification: analyzing files`
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
- const resolvedPath = files.find((f) => {
74
- if (dep.startsWith(".")) {
75
- return f.startsWith(path.resolve(depDir, dep));
76
- } else {
77
- return f.includes(dep);
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
- if (resolvedPath) {
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: riskResult.score,
163
+ score: finalScore,
138
164
  rating: riskResult.rating,
139
165
  recommendations: riskResult.recommendations
140
166
  },
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  __require,
4
4
  analyzeChangeAmplification
5
- } from "./chunk-GLHIV53G.mjs";
5
+ } from "./chunk-OBBL7HKE.mjs";
6
6
 
7
7
  // src/cli.ts
8
8
  import { Command } from "commander";
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
- options.onProgress?.(
61
- processed,
62
- files.length,
63
- `change-amplification: analyzing files`
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
- const resolvedPath = files.find((f) => {
74
- if (dep.startsWith(".")) {
75
- return f.startsWith(path.resolve(depDir, dep));
76
- } else {
77
- return f.includes(dep);
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
- if (resolvedPath) {
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: riskResult.score,
163
+ score: finalScore,
138
164
  rating: riskResult.rating,
139
165
  recommendations: riskResult.recommendations
140
166
  },
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  analyzeChangeAmplification
3
- } from "./chunk-GLHIV53G.mjs";
3
+ } from "./chunk-OBBL7HKE.mjs";
4
4
 
5
5
  // src/index.ts
6
6
  import { ToolRegistry } from "@aiready/core";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/change-amplification",
3
- "version": "0.11.4",
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.4"
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
- options.onProgress?.(
41
- processed,
42
- files.length,
43
- `change-amplification: analyzing files`
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
- // Find if this dependency resolves to one of our mapped files
60
- const resolvedPath = files.find((f) => {
61
- if (dep.startsWith('.')) {
62
- return f.startsWith(path.resolve(depDir, dep));
63
- } else {
64
- return f.includes(dep);
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: riskResult.score,
175
+ score: finalScore,
137
176
  rating: riskResult.rating,
138
177
  recommendations: riskResult.recommendations,
139
178
  },