@aiready/change-amplification 0.11.3 → 0.11.8

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.2 build /Users/pengcao/projects/aiready/packages/change-amplification
3
+ > @aiready/change-amplification@0.11.7 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,16 +9,16 @@
9
9
  CLI Target: es2020
10
10
  CJS Build start
11
11
  ESM Build start
12
- ESM dist/index.mjs 1.25 KB
13
- ESM dist/chunk-GLHIV53G.mjs 3.79 KB
12
+ CJS dist/index.js 7.40 KB
13
+ CJS dist/cli.js 9.01 KB
14
+ CJS ⚡️ Build success in 74ms
14
15
  ESM dist/cli.mjs 2.78 KB
15
- ESM ⚡️ Build success in 76ms
16
- CJS dist/index.js 6.37 KB
17
- CJS dist/cli.js 7.98 KB
18
- CJS ⚡️ Build success in 76ms
19
- DTS Build start
20
- DTS ⚡️ Build success in 2997ms
21
- DTS dist/cli.d.ts 152.00 B
22
- DTS dist/index.d.ts 1.05 KB
23
- DTS dist/cli.d.mts 152.00 B
24
- DTS dist/index.d.mts 1.05 KB
16
+ ESM dist/index.mjs 1.25 KB
17
+ ESM dist/chunk-OBBL7HKE.mjs 4.82 KB
18
+ ESM ⚡️ Build success in 80ms
19
+ DTS Build start
20
+ DTS ⚡️ Build success in 1656ms
21
+ DTS dist/cli.d.ts 152.00 B
22
+ DTS dist/index.d.ts 1.05 KB
23
+ DTS dist/cli.d.mts 152.00 B
24
+ DTS dist/index.d.mts 1.05 KB
@@ -1,16 +1,21 @@
1
1
 
2
2
  
3
- > @aiready/change-amplification@0.11.2 test /Users/pengcao/projects/aiready/packages/change-amplification
3
+ > @aiready/change-amplification@0.11.7 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) 3ms
9
+ ✓ src/__tests__/dummy.test.ts (1 test) 1ms
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  10:56:10
14
-  Duration  883ms (transform 174ms, setup 0ms, import 235ms, tests 3ms, environment 0ms)
14
+ ✓ src/__tests__/analyzer.test.ts (2 tests) 113ms
15
+
16
+  Test Files  2 passed (2)
17
+  Tests  3 passed (3)
18
+  Start at  13:15:42
19
+  Duration  1.68s (transform 348ms, setup 0ms, import 1.16s, tests 114ms, 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.3",
3
+ "version": "0.11.8",
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.3"
13
+ "@aiready/core": "0.21.8"
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
  },