@aiready/change-amplification 0.14.15 → 0.14.17

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.14.14 build /Users/pengcao/projects/aiready/packages/change-amplification
3
+ > @aiready/change-amplification@0.14.16 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/index.js 7.04 KB
13
- CJS dist/cli.js 8.51 KB
14
- CJS ⚡️ Build success in 122ms
12
+ CJS dist/index.js 7.53 KB
13
+ CJS dist/cli.js 8.99 KB
14
+ CJS ⚡️ Build success in 69ms
15
15
  ESM dist/index.mjs 916.00 B
16
+ ESM dist/chunk-KUIEB4UN.mjs 5.32 KB
16
17
  ESM dist/cli.mjs 2.41 KB
17
- ESM dist/chunk-SPXGOPNW.mjs 4.83 KB
18
- ESM ⚡️ Build success in 122ms
18
+ ESM ⚡️ Build success in 69ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 5497ms
20
+ DTS ⚡️ Build success in 2867ms
21
21
  DTS dist/cli.d.ts 266.00 B
22
22
  DTS dist/index.d.ts 937.00 B
23
23
  DTS dist/cli.d.mts 266.00 B
@@ -1,7 +1,7 @@
1
1
 
2
2
  
3
- > @aiready/change-amplification@0.14.14 format-check /Users/pengcao/projects/aiready/packages/change-amplification
3
+ > @aiready/change-amplification@0.14.16 format-check /Users/pengcao/projects/aiready/packages/change-amplification
4
4
  > prettier --check . --ignore-path ../../.prettierignore
5
5
 
6
6
  Checking formatting...
7
- package.jsonREADME.mdsrc/__tests__/analyzer.test.tssrc/__tests__/cli.test.tssrc/__tests__/dummy.test.tssrc/__tests__/provider.test.tssrc/__tests__/scoring.test.tssrc/analyzer.tssrc/cli.tssrc/index.tssrc/provider.tssrc/scoring.tssrc/types.tstsconfig.jsonAll matched files use Prettier code style!
7
+ package.jsonREADME.mdsrc/__tests__/analyzer.test.tssrc/__tests__/cli.test.tssrc/__tests__/contract.test.tssrc/__tests__/dummy.test.tssrc/__tests__/provider.test.tssrc/__tests__/scoring.test.tssrc/analyzer.tssrc/cli.tssrc/index.tssrc/provider.tssrc/scoring.tssrc/types.tstsconfig.jsonAll matched files use Prettier code style!
@@ -1,5 +1,5 @@
1
1
 
2
2
  
3
- > @aiready/change-amplification@0.14.14 lint /Users/pengcao/projects/aiready/packages/change-amplification
3
+ > @aiready/change-amplification@0.14.16 lint /Users/pengcao/projects/aiready/packages/change-amplification
4
4
  > eslint src --ext .ts
5
5
 
@@ -1,24 +1,54 @@
1
1
 
2
2
  
3
- > @aiready/change-amplification@0.14.14 test /Users/pengcao/projects/aiready/packages/change-amplification
3
+ > @aiready/change-amplification@0.14.16 test /Users/pengcao/projects/aiready/packages/change-amplification
4
4
  > vitest run
5
5
 
6
6
  [?25l
7
-  RUN  v4.0.18 /Users/pengcao/projects/aiready/packages/change-amplification
7
+  RUN  v4.1.2 /Users/pengcao/projects/aiready/packages/change-amplification
8
8
 
9
- ✓ src/__tests__/dummy.test.ts (1 test) 3ms
10
- ✓ src/__tests__/cli.test.ts (2 tests) 6ms
11
- ✓ src/__tests__/provider.test.ts (2 tests) 7ms
12
- ✓ src/__tests__/scoring.test.ts (2 tests) 33ms
9
+ [?2026h
10
+  src/__tests__/analyzer.test.ts [queued]
11
+
12
+  Test Files 0 passed (6)
13
+  Tests 0 passed (0)
14
+  Start at 19:19:04
15
+  Duration 102ms
16
+ [?2026l[?2026h ✓ src/__tests__/dummy.test.ts (1 test) 2ms
17
+
18
+  ❯ src/__tests__/analyzer.test.ts [queued]
19
+  ❯ src/__tests__/cli.test.ts [queued]
20
+  ❯ src/__tests__/contract.test.ts [queued]
21
+  ❯ src/__tests__/provider.test.ts [queued]
22
+  ❯ src/__tests__/scoring.test.ts [queued]
23
+
24
+  Test Files 1 passed (6)
25
+  Tests 1 passed (1)
26
+  Start at 19:19:04
27
+  Duration 959ms
28
+ [?2026l[?2026h
29
+  ❯ src/__tests__/analyzer.test.ts [queued]
30
+  ❯ src/__tests__/cli.test.ts 0/2
31
+  ❯ src/__tests__/contract.test.ts [queued]
32
+  ❯ src/__tests__/provider.test.ts [queued]
33
+  ❯ src/__tests__/scoring.test.ts [queued]
34
+
35
+  Test Files 1 passed (6)
36
+  Tests 1 passed (3)
37
+  Start at 19:19:04
38
+  Duration 1.16s
39
+ [?2026l ✓ src/__tests__/cli.test.ts (2 tests) 2ms
40
+ ✓ src/__tests__/scoring.test.ts (2 tests) 2ms
41
+ ✓ src/__tests__/provider.test.ts (2 tests) 3ms
42
+ ✓ src/__tests__/contract.test.ts (1 test) 23ms
13
43
  stdout | src/__tests__/analyzer.test.ts > analyzeChangeAmplification reproduction > should see how it gets to 0
14
44
  Resulting score for highly coupled: 27
15
45
  Rating: explosive
16
46
 
17
- ✓ src/__tests__/analyzer.test.ts (2 tests) 8ms
47
+ ✓ src/__tests__/analyzer.test.ts (2 tests) 9ms
18
48
 
19
-  Test Files  5 passed (5)
20
-  Tests  9 passed (9)
21
-  Start at  01:04:43
22
-  Duration  4.63s (transform 5.03s, setup 0ms, import 14.86s, tests 57ms, environment 0ms)
49
+  Test Files  6 passed (6)
50
+  Tests  10 passed (10)
51
+  Start at  19:19:04
52
+  Duration  1.24s (transform 1.02s, setup 0ms, import 5.00s, tests 42ms, environment 1ms)
23
53
 
24
54
  [?25h
@@ -1,5 +1,5 @@
1
1
 
2
2
  
3
- > @aiready/change-amplification@0.14.14 type-check /Users/pengcao/projects/aiready/packages/change-amplification
3
+ > @aiready/change-amplification@0.14.16 type-check /Users/pengcao/projects/aiready/packages/change-amplification
4
4
  > tsc --noEmit
5
5
 
@@ -0,0 +1,148 @@
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 = await 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
+ const fileName = path.basename(hotspot.file);
104
+ const isBarrelFile = fileName === "index.ts" || fileName === "index.js" || fileName === "index.tsx" || fileName === "index.jsx" || fileName.startsWith("all-") || fileName.endsWith(".meta.ts") || fileName.endsWith(".meta.js");
105
+ if (hotspot.amplificationFactor > 20 && !isBarrelFile) {
106
+ issues.push({
107
+ type: IssueType.ChangeAmplification,
108
+ severity: hotspot.amplificationFactor > 40 ? Severity.Critical : Severity.Major,
109
+ message: `High change amplification detected (Factor: ${hotspot.amplificationFactor}). Changes here cascade heavily.`,
110
+ location: { file: hotspot.file, line: 1 },
111
+ suggestion: `Reduce coupling. Fan-out is ${hotspot.fanOut}, Fan-in is ${hotspot.fanIn}.`
112
+ });
113
+ }
114
+ if (hotspot.amplificationFactor > 5 && !isBarrelFile) {
115
+ results.push({
116
+ fileName: hotspot.file,
117
+ issues,
118
+ metrics: {
119
+ aiSignalClarityScore: 100 - hotspot.amplificationFactor
120
+ // Just a rough score
121
+ }
122
+ });
123
+ }
124
+ }
125
+ return {
126
+ summary: {
127
+ totalFiles: files.length,
128
+ totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),
129
+ criticalIssues: results.reduce(
130
+ (sum, r) => sum + r.issues.filter((i) => getLevel(i.severity) === 4).length,
131
+ 0
132
+ ),
133
+ majorIssues: results.reduce(
134
+ (sum, r) => sum + r.issues.filter((i) => getLevel(i.severity) === 3).length,
135
+ 0
136
+ ),
137
+ score: finalScore,
138
+ rating: riskResult.rating,
139
+ recommendations: riskResult.recommendations
140
+ },
141
+ results
142
+ };
143
+ }
144
+
145
+ export {
146
+ __require,
147
+ analyzeChangeAmplification
148
+ };
@@ -0,0 +1,148 @@
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 = await 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
+ const fileName = path.basename(hotspot.file).toLowerCase();
104
+ const isSharedUtility = fileName === "index.ts" || fileName === "index.js" || fileName === "index.tsx" || fileName === "index.jsx" || fileName.includes("logger") || fileName.includes("log.ts") || fileName.includes("constants") || fileName.includes("types.ts") || fileName.includes("enums.ts") || fileName.startsWith("all-") || fileName.endsWith(".meta.ts") || fileName.endsWith(".meta.js");
105
+ if (hotspot.amplificationFactor > 20 && !isSharedUtility) {
106
+ issues.push({
107
+ type: IssueType.ChangeAmplification,
108
+ severity: hotspot.amplificationFactor > 40 ? Severity.Critical : Severity.Major,
109
+ message: `High change amplification detected (Factor: ${hotspot.amplificationFactor}). Changes here cascade heavily.`,
110
+ location: { file: hotspot.file, line: 1 },
111
+ suggestion: `Reduce coupling. Fan-out is ${hotspot.fanOut}, Fan-in is ${hotspot.fanIn}.`
112
+ });
113
+ }
114
+ if (hotspot.amplificationFactor > 5 && !isSharedUtility) {
115
+ results.push({
116
+ fileName: hotspot.file,
117
+ issues,
118
+ metrics: {
119
+ aiSignalClarityScore: 100 - hotspot.amplificationFactor
120
+ // Just a rough score
121
+ }
122
+ });
123
+ }
124
+ }
125
+ return {
126
+ summary: {
127
+ totalFiles: files.length,
128
+ totalIssues: results.reduce((sum, r) => sum + r.issues.length, 0),
129
+ criticalIssues: results.reduce(
130
+ (sum, r) => sum + r.issues.filter((i) => getLevel(i.severity) === 4).length,
131
+ 0
132
+ ),
133
+ majorIssues: results.reduce(
134
+ (sum, r) => sum + r.issues.filter((i) => getLevel(i.severity) === 3).length,
135
+ 0
136
+ ),
137
+ score: finalScore,
138
+ rating: riskResult.rating,
139
+ recommendations: riskResult.recommendations
140
+ },
141
+ results
142
+ };
143
+ }
144
+
145
+ export {
146
+ __require,
147
+ analyzeChangeAmplification
148
+ };
package/dist/cli.js CHANGED
@@ -128,7 +128,9 @@ async function analyzeChangeAmplification(options) {
128
128
  };
129
129
  for (const hotspot of riskResult.hotspots) {
130
130
  const issues = [];
131
- if (hotspot.amplificationFactor > 20) {
131
+ const fileName = path.basename(hotspot.file).toLowerCase();
132
+ const isSharedUtility = fileName === "index.ts" || fileName === "index.js" || fileName === "index.tsx" || fileName === "index.jsx" || fileName.includes("logger") || fileName.includes("log.ts") || fileName.includes("constants") || fileName.includes("types.ts") || fileName.includes("enums.ts") || fileName.startsWith("all-") || fileName.endsWith(".meta.ts") || fileName.endsWith(".meta.js");
133
+ if (hotspot.amplificationFactor > 20 && !isSharedUtility) {
132
134
  issues.push({
133
135
  type: import_core.IssueType.ChangeAmplification,
134
136
  severity: hotspot.amplificationFactor > 40 ? import_core.Severity.Critical : import_core.Severity.Major,
@@ -137,7 +139,7 @@ async function analyzeChangeAmplification(options) {
137
139
  suggestion: `Reduce coupling. Fan-out is ${hotspot.fanOut}, Fan-in is ${hotspot.fanIn}.`
138
140
  });
139
141
  }
140
- if (hotspot.amplificationFactor > 5) {
142
+ if (hotspot.amplificationFactor > 5 && !isSharedUtility) {
141
143
  results.push({
142
144
  fileName: hotspot.file,
143
145
  issues,
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  __require,
4
4
  analyzeChangeAmplification
5
- } from "./chunk-SPXGOPNW.mjs";
5
+ } from "./chunk-KUIEB4UN.mjs";
6
6
 
7
7
  // src/cli.ts
8
8
  import { Command } from "commander";
package/dist/index.js CHANGED
@@ -128,7 +128,9 @@ async function analyzeChangeAmplification(options) {
128
128
  };
129
129
  for (const hotspot of riskResult.hotspots) {
130
130
  const issues = [];
131
- if (hotspot.amplificationFactor > 20) {
131
+ const fileName = path.basename(hotspot.file).toLowerCase();
132
+ const isSharedUtility = fileName === "index.ts" || fileName === "index.js" || fileName === "index.tsx" || fileName === "index.jsx" || fileName.includes("logger") || fileName.includes("log.ts") || fileName.includes("constants") || fileName.includes("types.ts") || fileName.includes("enums.ts") || fileName.startsWith("all-") || fileName.endsWith(".meta.ts") || fileName.endsWith(".meta.js");
133
+ if (hotspot.amplificationFactor > 20 && !isSharedUtility) {
132
134
  issues.push({
133
135
  type: import_core.IssueType.ChangeAmplification,
134
136
  severity: hotspot.amplificationFactor > 40 ? import_core.Severity.Critical : import_core.Severity.Major,
@@ -137,7 +139,7 @@ async function analyzeChangeAmplification(options) {
137
139
  suggestion: `Reduce coupling. Fan-out is ${hotspot.fanOut}, Fan-in is ${hotspot.fanIn}.`
138
140
  });
139
141
  }
140
- if (hotspot.amplificationFactor > 5) {
142
+ if (hotspot.amplificationFactor > 5 && !isSharedUtility) {
141
143
  results.push({
142
144
  fileName: hotspot.file,
143
145
  issues,
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  analyzeChangeAmplification
3
- } from "./chunk-SPXGOPNW.mjs";
3
+ } from "./chunk-KUIEB4UN.mjs";
4
4
 
5
5
  // src/index.ts
6
6
  import { ToolRegistry } from "@aiready/core";
package/package.json CHANGED
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "name": "@aiready/change-amplification",
3
- "version": "0.14.15",
3
+ "version": "0.14.17",
4
4
  "description": "AI-Readiness: Change Amplification Detection",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",
8
8
  "dependencies": {
9
- "@typescript-eslint/typescript-estree": "^8.0.0",
10
- "commander": "^14.0.0",
11
- "glob": "^13.0.0",
12
- "chalk": "^5.3.0",
13
- "@aiready/core": "0.24.16"
9
+ "@typescript-eslint/typescript-estree": "^8.58.0",
10
+ "chalk": "^5.6.2",
11
+ "commander": "^14.0.3",
12
+ "glob": "^13.0.6",
13
+ "@aiready/core": "0.24.20"
14
14
  },
15
15
  "devDependencies": {
16
- "@types/node": "^24.0.0",
17
- "@typescript-eslint/types": "^8.0.0",
18
- "tsup": "^8.0.2",
19
- "typescript": "^5.4.5",
20
- "vitest": "^4.0.0"
16
+ "@types/node": "^24.12.2",
17
+ "@typescript-eslint/types": "^8.58.0",
18
+ "tsup": "^8.5.1",
19
+ "typescript": "^6.0.2",
20
+ "vitest": "^4.1.2"
21
21
  },
22
22
  "exports": {
23
23
  ".": {
@@ -42,6 +42,9 @@
42
42
  "test:watch": "vitest",
43
43
  "lint": "eslint src --ext .ts",
44
44
  "type-check": "tsc --noEmit",
45
- "format-check": "prettier --check . --ignore-path ../../.prettierignore"
45
+ "format-check": "prettier --check . --ignore-path ../../.prettierignore",
46
+ "format": "prettier --write . --ignore-path ../../.prettierignore",
47
+ "lint:fix": "eslint . --fix",
48
+ "test:coverage": "vitest run --coverage"
46
49
  }
47
50
  }
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { CHANGE_AMPLIFICATION_PROVIDER } from '../provider';
3
+ import { validateSpokeOutput } from '@aiready/core';
4
+
5
+ // Mock core functions to avoid actual FS access
6
+ vi.mock('@aiready/core', async (importOriginal) => {
7
+ const actual = await importOriginal<typeof import('@aiready/core')>();
8
+ return {
9
+ ...actual,
10
+ scanFiles: vi.fn().mockResolvedValue(['file1.ts']),
11
+ readFileContent: vi.fn().mockImplementation((file) => {
12
+ if (file === 'file1.ts')
13
+ return 'import { x } from "./module"; export const y = x + 1;';
14
+ return '';
15
+ }),
16
+ };
17
+ });
18
+
19
+ describe('Change Amplification Contract Validation', () => {
20
+ it('should produce output matching the SpokeOutput contract', async () => {
21
+ const options = {
22
+ rootDir: './test',
23
+ };
24
+
25
+ const output = await CHANGE_AMPLIFICATION_PROVIDER.analyze(options);
26
+
27
+ // 1. Structural Validation
28
+ const validation = validateSpokeOutput('change-amplification', output);
29
+ if (!validation.valid) {
30
+ console.error('Contract Validation Errors:', validation.errors);
31
+ }
32
+ expect(validation.valid).toBe(true);
33
+
34
+ // 2. Scoring Validation
35
+ const score = CHANGE_AMPLIFICATION_PROVIDER.score(output, options);
36
+ expect(score).toBeDefined();
37
+ expect(typeof score.score).toBe('number');
38
+ expect(score.score).toBeGreaterThanOrEqual(0);
39
+ expect(score.score).toBeLessThanOrEqual(100);
40
+ });
41
+ });
package/src/analyzer.ts CHANGED
@@ -135,7 +135,25 @@ export async function analyzeChangeAmplification(
135
135
 
136
136
  for (const hotspot of riskResult.hotspots) {
137
137
  const issues: ChangeAmplificationIssue[] = [];
138
- if (hotspot.amplificationFactor > 20) {
138
+
139
+ // Check if this is a barrel/index file or a common utility pattern (intentional re-export or shared use)
140
+ const fileName = path.basename(hotspot.file).toLowerCase();
141
+ const isSharedUtility =
142
+ fileName === 'index.ts' ||
143
+ fileName === 'index.js' ||
144
+ fileName === 'index.tsx' ||
145
+ fileName === 'index.jsx' ||
146
+ fileName.includes('logger') ||
147
+ fileName.includes('log.ts') ||
148
+ fileName.includes('constants') ||
149
+ fileName.includes('types.ts') ||
150
+ fileName.includes('enums.ts') ||
151
+ fileName.startsWith('all-') ||
152
+ fileName.endsWith('.meta.ts') ||
153
+ fileName.endsWith('.meta.js');
154
+
155
+ // Only flag high amplification if it's NOT a barrel file or shared utility
156
+ if (hotspot.amplificationFactor > 20 && !isSharedUtility) {
139
157
  issues.push({
140
158
  type: IssueType.ChangeAmplification,
141
159
  severity:
@@ -147,7 +165,8 @@ export async function analyzeChangeAmplification(
147
165
  }
148
166
 
149
167
  // We only push results for files that have either high fan-in or fan-out
150
- if (hotspot.amplificationFactor > 5) {
168
+ // Also exclude barrel files and shared utilities from the results
169
+ if (hotspot.amplificationFactor > 5 && !isSharedUtility) {
151
170
  results.push({
152
171
  fileName: hotspot.file,
153
172
  issues,
package/tsconfig.json CHANGED
@@ -2,7 +2,8 @@
2
2
  "extends": "../../tsconfig.json",
3
3
  "compilerOptions": {
4
4
  "outDir": "dist",
5
- "rootDir": "src"
5
+ "rootDir": "src",
6
+ "ignoreDeprecations": "6.0"
6
7
  },
7
8
  "include": ["src/**/*"]
8
9
  }