@aiready/context-analyzer 0.9.34 → 0.9.36
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/.github/FUNDING.yml +2 -2
- package/.turbo/turbo-build.log +9 -9
- package/.turbo/turbo-test.log +19 -19
- package/CONTRIBUTING.md +10 -2
- package/dist/chunk-7LUSCLGR.mjs +2058 -0
- package/dist/cli.js +346 -83
- package/dist/cli.mjs +137 -33
- package/dist/index.js +231 -56
- package/dist/index.mjs +1 -1
- package/dist/python-context-GOH747QU.mjs +202 -0
- package/package.json +2 -2
- package/src/__tests__/analyzer.test.ts +69 -17
- package/src/__tests__/auto-detection.test.ts +1 -1
- package/src/__tests__/enhanced-cohesion.test.ts +19 -7
- package/src/__tests__/file-classification.test.ts +188 -53
- package/src/__tests__/fragmentation-advanced.test.ts +2 -11
- package/src/__tests__/fragmentation-coupling.test.ts +8 -2
- package/src/__tests__/fragmentation-log.test.ts +9 -9
- package/src/__tests__/scoring.test.ts +19 -7
- package/src/__tests__/structural-cohesion.test.ts +33 -21
- package/src/analyzer.ts +724 -376
- package/src/analyzers/python-context.ts +33 -10
- package/src/cli.ts +223 -59
- package/src/index.ts +112 -55
- package/src/scoring.ts +53 -43
- package/src/semantic-analysis.ts +73 -55
- package/src/types.ts +12 -13
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// src/analyzers/python-context.ts
|
|
2
|
+
import { getParser, estimateTokens } from "@aiready/core";
|
|
3
|
+
import { resolve, relative, dirname, join } from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
async function analyzePythonContext(files, rootDir) {
|
|
6
|
+
const results = [];
|
|
7
|
+
const parser = getParser("dummy.py");
|
|
8
|
+
if (!parser) {
|
|
9
|
+
console.warn("Python parser not available");
|
|
10
|
+
return results;
|
|
11
|
+
}
|
|
12
|
+
const pythonFiles = files.filter((f) => f.toLowerCase().endsWith(".py"));
|
|
13
|
+
void relative;
|
|
14
|
+
void join;
|
|
15
|
+
const dependencyGraph = await buildPythonDependencyGraph(
|
|
16
|
+
pythonFiles,
|
|
17
|
+
rootDir
|
|
18
|
+
);
|
|
19
|
+
for (const file of pythonFiles) {
|
|
20
|
+
try {
|
|
21
|
+
const code = await fs.promises.readFile(file, "utf-8");
|
|
22
|
+
const result = parser.parse(code, file);
|
|
23
|
+
const imports = result.imports.map((imp) => ({
|
|
24
|
+
source: imp.source,
|
|
25
|
+
specifiers: imp.specifiers,
|
|
26
|
+
isRelative: imp.source.startsWith("."),
|
|
27
|
+
resolvedPath: resolvePythonImport(file, imp.source, rootDir)
|
|
28
|
+
}));
|
|
29
|
+
const exports = result.exports.map((exp) => ({
|
|
30
|
+
name: exp.name,
|
|
31
|
+
type: exp.type
|
|
32
|
+
}));
|
|
33
|
+
const linesOfCode = code.split("\n").length;
|
|
34
|
+
const importDepth = await calculatePythonImportDepth(
|
|
35
|
+
file,
|
|
36
|
+
dependencyGraph,
|
|
37
|
+
/* @__PURE__ */ new Set()
|
|
38
|
+
);
|
|
39
|
+
const contextBudget = estimateContextBudget(
|
|
40
|
+
code,
|
|
41
|
+
imports,
|
|
42
|
+
dependencyGraph
|
|
43
|
+
);
|
|
44
|
+
const cohesion = calculatePythonCohesion(exports, imports);
|
|
45
|
+
const circularDependencies = detectCircularDependencies(
|
|
46
|
+
file,
|
|
47
|
+
dependencyGraph
|
|
48
|
+
);
|
|
49
|
+
results.push({
|
|
50
|
+
file,
|
|
51
|
+
importDepth,
|
|
52
|
+
contextBudget,
|
|
53
|
+
cohesion,
|
|
54
|
+
imports,
|
|
55
|
+
exports,
|
|
56
|
+
metrics: {
|
|
57
|
+
linesOfCode,
|
|
58
|
+
importCount: imports.length,
|
|
59
|
+
exportCount: exports.length,
|
|
60
|
+
circularDependencies
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.warn(`Failed to analyze ${file}:`, error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
async function buildPythonDependencyGraph(files, rootDir) {
|
|
70
|
+
const graph = /* @__PURE__ */ new Map();
|
|
71
|
+
const parser = getParser("dummy.py");
|
|
72
|
+
if (!parser) return graph;
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
try {
|
|
75
|
+
const code = await fs.promises.readFile(file, "utf-8");
|
|
76
|
+
const result = parser.parse(code, file);
|
|
77
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
78
|
+
for (const imp of result.imports) {
|
|
79
|
+
const resolved = resolvePythonImport(file, imp.source, rootDir);
|
|
80
|
+
if (resolved && files.includes(resolved)) {
|
|
81
|
+
dependencies.add(resolved);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
graph.set(file, dependencies);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
void error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return graph;
|
|
90
|
+
}
|
|
91
|
+
function resolvePythonImport(fromFile, importPath, rootDir) {
|
|
92
|
+
const dir = dirname(fromFile);
|
|
93
|
+
if (importPath.startsWith(".")) {
|
|
94
|
+
const parts = importPath.split(".");
|
|
95
|
+
let upCount = 0;
|
|
96
|
+
while (parts[0] === "") {
|
|
97
|
+
upCount++;
|
|
98
|
+
parts.shift();
|
|
99
|
+
}
|
|
100
|
+
let targetDir = dir;
|
|
101
|
+
for (let i = 0; i < upCount - 1; i++) {
|
|
102
|
+
targetDir = dirname(targetDir);
|
|
103
|
+
}
|
|
104
|
+
const modulePath = parts.join("/");
|
|
105
|
+
const possiblePaths = [
|
|
106
|
+
resolve(targetDir, `${modulePath}.py`),
|
|
107
|
+
resolve(targetDir, modulePath, "__init__.py")
|
|
108
|
+
];
|
|
109
|
+
for (const path of possiblePaths) {
|
|
110
|
+
if (fs.existsSync(path)) {
|
|
111
|
+
return path;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
const modulePath = importPath.replace(/\./g, "/");
|
|
116
|
+
const possiblePaths = [
|
|
117
|
+
resolve(rootDir, `${modulePath}.py`),
|
|
118
|
+
resolve(rootDir, modulePath, "__init__.py")
|
|
119
|
+
];
|
|
120
|
+
for (const path of possiblePaths) {
|
|
121
|
+
if (fs.existsSync(path)) {
|
|
122
|
+
return path;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
async function calculatePythonImportDepth(file, dependencyGraph, visited, depth = 0) {
|
|
129
|
+
if (visited.has(file)) {
|
|
130
|
+
return depth;
|
|
131
|
+
}
|
|
132
|
+
visited.add(file);
|
|
133
|
+
const dependencies = dependencyGraph.get(file) || /* @__PURE__ */ new Set();
|
|
134
|
+
if (dependencies.size === 0) {
|
|
135
|
+
return depth;
|
|
136
|
+
}
|
|
137
|
+
let maxDepth = depth;
|
|
138
|
+
for (const dep of dependencies) {
|
|
139
|
+
const depDepth = await calculatePythonImportDepth(
|
|
140
|
+
dep,
|
|
141
|
+
dependencyGraph,
|
|
142
|
+
new Set(visited),
|
|
143
|
+
depth + 1
|
|
144
|
+
);
|
|
145
|
+
maxDepth = Math.max(maxDepth, depDepth);
|
|
146
|
+
}
|
|
147
|
+
return maxDepth;
|
|
148
|
+
}
|
|
149
|
+
function estimateContextBudget(code, imports, dependencyGraph) {
|
|
150
|
+
let budget = estimateTokens(code);
|
|
151
|
+
const avgTokensPerDep = 500;
|
|
152
|
+
budget += imports.length * avgTokensPerDep;
|
|
153
|
+
return budget;
|
|
154
|
+
}
|
|
155
|
+
function calculatePythonCohesion(exports, imports) {
|
|
156
|
+
if (exports.length === 0) return 1;
|
|
157
|
+
const exportCount = exports.length;
|
|
158
|
+
const importCount = imports.length;
|
|
159
|
+
let cohesion = 1;
|
|
160
|
+
if (exportCount > 10) {
|
|
161
|
+
cohesion *= 0.6;
|
|
162
|
+
} else if (exportCount > 5) {
|
|
163
|
+
cohesion *= 0.8;
|
|
164
|
+
}
|
|
165
|
+
if (exportCount > 0) {
|
|
166
|
+
const ratio = importCount / exportCount;
|
|
167
|
+
if (ratio > 2) {
|
|
168
|
+
cohesion *= 1.1;
|
|
169
|
+
} else if (ratio < 0.5) {
|
|
170
|
+
cohesion *= 0.9;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return Math.min(1, Math.max(0, cohesion));
|
|
174
|
+
}
|
|
175
|
+
function detectCircularDependencies(file, dependencyGraph) {
|
|
176
|
+
const circular = [];
|
|
177
|
+
const visited = /* @__PURE__ */ new Set();
|
|
178
|
+
const recursionStack = /* @__PURE__ */ new Set();
|
|
179
|
+
function dfs(current, path) {
|
|
180
|
+
if (recursionStack.has(current)) {
|
|
181
|
+
const cycleStart = path.indexOf(current);
|
|
182
|
+
const cycle = path.slice(cycleStart).concat([current]);
|
|
183
|
+
circular.push(cycle.join(" \u2192 "));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (visited.has(current)) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
visited.add(current);
|
|
190
|
+
recursionStack.add(current);
|
|
191
|
+
const dependencies = dependencyGraph.get(current) || /* @__PURE__ */ new Set();
|
|
192
|
+
for (const dep of dependencies) {
|
|
193
|
+
dfs(dep, [...path, current]);
|
|
194
|
+
}
|
|
195
|
+
recursionStack.delete(current);
|
|
196
|
+
}
|
|
197
|
+
dfs(file, []);
|
|
198
|
+
return [...new Set(circular)];
|
|
199
|
+
}
|
|
200
|
+
export {
|
|
201
|
+
analyzePythonContext
|
|
202
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/context-analyzer",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.36",
|
|
4
4
|
"description": "AI context window cost analysis - detect fragmented code, deep import chains, and expensive context budgets",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"commander": "^14.0.0",
|
|
50
50
|
"chalk": "^5.3.0",
|
|
51
51
|
"prompts": "^2.4.2",
|
|
52
|
-
"@aiready/core": "0.9.
|
|
52
|
+
"@aiready/core": "0.9.33"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^24.0.0",
|
|
@@ -38,8 +38,14 @@ describe('calculateImportDepth', () => {
|
|
|
38
38
|
it('should calculate import depth correctly', () => {
|
|
39
39
|
const files = [
|
|
40
40
|
{ file: 'a.ts', content: 'export const a = 1;' },
|
|
41
|
-
{
|
|
42
|
-
|
|
41
|
+
{
|
|
42
|
+
file: 'b.ts',
|
|
43
|
+
content: 'import { a } from "a.ts";\nexport const b = a;',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
file: 'c.ts',
|
|
47
|
+
content: 'import { b } from "b.ts";\nexport const c = b;',
|
|
48
|
+
},
|
|
43
49
|
];
|
|
44
50
|
|
|
45
51
|
const graph = buildDependencyGraph(files);
|
|
@@ -51,8 +57,14 @@ describe('calculateImportDepth', () => {
|
|
|
51
57
|
|
|
52
58
|
it('should handle circular dependencies gracefully', () => {
|
|
53
59
|
const files = [
|
|
54
|
-
{
|
|
55
|
-
|
|
60
|
+
{
|
|
61
|
+
file: 'a.ts',
|
|
62
|
+
content: 'import { b } from "./b";\nexport const a = 1;',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
file: 'b.ts',
|
|
66
|
+
content: 'import { a } from "./a";\nexport const b = 2;',
|
|
67
|
+
},
|
|
56
68
|
];
|
|
57
69
|
|
|
58
70
|
const graph = buildDependencyGraph(files);
|
|
@@ -67,8 +79,14 @@ describe('getTransitiveDependencies', () => {
|
|
|
67
79
|
it('should get all transitive dependencies', () => {
|
|
68
80
|
const files = [
|
|
69
81
|
{ file: 'a.ts', content: 'export const a = 1;' },
|
|
70
|
-
{
|
|
71
|
-
|
|
82
|
+
{
|
|
83
|
+
file: 'b.ts',
|
|
84
|
+
content: 'import { a } from "a.ts";\nexport const b = a;',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
file: 'c.ts',
|
|
88
|
+
content: 'import { b } from "b.ts";\nexport const c = b;',
|
|
89
|
+
},
|
|
72
90
|
];
|
|
73
91
|
|
|
74
92
|
const graph = buildDependencyGraph(files);
|
|
@@ -84,7 +102,10 @@ describe('calculateContextBudget', () => {
|
|
|
84
102
|
it('should calculate total token cost including dependencies', () => {
|
|
85
103
|
const files = [
|
|
86
104
|
{ file: 'a.ts', content: 'export const a = 1;'.repeat(10) }, // ~40 tokens
|
|
87
|
-
{
|
|
105
|
+
{
|
|
106
|
+
file: 'b.ts',
|
|
107
|
+
content: 'import { a } from "./a";\nexport const b = a;'.repeat(10),
|
|
108
|
+
}, // ~60 tokens
|
|
88
109
|
];
|
|
89
110
|
|
|
90
111
|
const graph = buildDependencyGraph(files);
|
|
@@ -98,8 +119,14 @@ describe('calculateContextBudget', () => {
|
|
|
98
119
|
describe('detectCircularDependencies', () => {
|
|
99
120
|
it('should detect circular dependencies', () => {
|
|
100
121
|
const files = [
|
|
101
|
-
{
|
|
102
|
-
|
|
122
|
+
{
|
|
123
|
+
file: 'a.ts',
|
|
124
|
+
content: 'import { b } from "b.ts";\nexport const a = 1;',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
file: 'b.ts',
|
|
128
|
+
content: 'import { a } from "a.ts";\nexport const b = 2;',
|
|
129
|
+
},
|
|
103
130
|
];
|
|
104
131
|
|
|
105
132
|
const graph = buildDependencyGraph(files);
|
|
@@ -111,7 +138,10 @@ describe('detectCircularDependencies', () => {
|
|
|
111
138
|
it('should return empty for no circular dependencies', () => {
|
|
112
139
|
const files = [
|
|
113
140
|
{ file: 'a.ts', content: 'export const a = 1;' },
|
|
114
|
-
{
|
|
141
|
+
{
|
|
142
|
+
file: 'b.ts',
|
|
143
|
+
content: 'import { a } from "a.ts";\nexport const b = a;',
|
|
144
|
+
},
|
|
115
145
|
];
|
|
116
146
|
|
|
117
147
|
const graph = buildDependencyGraph(files);
|
|
@@ -123,7 +153,9 @@ describe('detectCircularDependencies', () => {
|
|
|
123
153
|
|
|
124
154
|
describe('calculateCohesion', () => {
|
|
125
155
|
it('should return 1 for single export', () => {
|
|
126
|
-
const exports = [
|
|
156
|
+
const exports = [
|
|
157
|
+
{ name: 'foo', type: 'function' as const, inferredDomain: 'user' },
|
|
158
|
+
];
|
|
127
159
|
expect(calculateCohesion(exports)).toBe(1);
|
|
128
160
|
});
|
|
129
161
|
|
|
@@ -142,7 +174,11 @@ describe('calculateCohesion', () => {
|
|
|
142
174
|
const exports = [
|
|
143
175
|
{ name: 'getUser', type: 'function' as const, inferredDomain: 'user' },
|
|
144
176
|
{ name: 'getOrder', type: 'function' as const, inferredDomain: 'order' },
|
|
145
|
-
{
|
|
177
|
+
{
|
|
178
|
+
name: 'parseConfig',
|
|
179
|
+
type: 'function' as const,
|
|
180
|
+
inferredDomain: 'config',
|
|
181
|
+
},
|
|
146
182
|
];
|
|
147
183
|
|
|
148
184
|
const cohesion = calculateCohesion(exports);
|
|
@@ -153,23 +189,39 @@ describe('calculateCohesion', () => {
|
|
|
153
189
|
const exports = [
|
|
154
190
|
{ name: 'mockUser', type: 'function' as const, inferredDomain: 'user' },
|
|
155
191
|
{ name: 'mockOrder', type: 'function' as const, inferredDomain: 'order' },
|
|
156
|
-
{
|
|
192
|
+
{
|
|
193
|
+
name: 'setupTestDb',
|
|
194
|
+
type: 'function' as const,
|
|
195
|
+
inferredDomain: 'helper',
|
|
196
|
+
},
|
|
157
197
|
];
|
|
158
198
|
|
|
159
199
|
// Test file - should return 1 despite mixed domains
|
|
160
|
-
const cohesionTestFile = calculateCohesion(
|
|
200
|
+
const cohesionTestFile = calculateCohesion(
|
|
201
|
+
exports,
|
|
202
|
+
'src/__tests__/helpers.test.ts'
|
|
203
|
+
);
|
|
161
204
|
expect(cohesionTestFile).toBe(1);
|
|
162
205
|
|
|
163
206
|
// Mock file - should return 1 despite mixed domains
|
|
164
|
-
const cohesionMockFile = calculateCohesion(
|
|
207
|
+
const cohesionMockFile = calculateCohesion(
|
|
208
|
+
exports,
|
|
209
|
+
'src/test-utils/mocks.ts'
|
|
210
|
+
);
|
|
165
211
|
expect(cohesionMockFile).toBe(1);
|
|
166
212
|
|
|
167
213
|
// Fixture file - should return 1 despite mixed domains
|
|
168
|
-
const cohesionFixtureFile = calculateCohesion(
|
|
214
|
+
const cohesionFixtureFile = calculateCohesion(
|
|
215
|
+
exports,
|
|
216
|
+
'src/fixtures/data.ts'
|
|
217
|
+
);
|
|
169
218
|
expect(cohesionFixtureFile).toBe(1);
|
|
170
219
|
|
|
171
220
|
// Regular file - should have low cohesion
|
|
172
|
-
const cohesionRegularFile = calculateCohesion(
|
|
221
|
+
const cohesionRegularFile = calculateCohesion(
|
|
222
|
+
exports,
|
|
223
|
+
'src/utils/helpers.ts'
|
|
224
|
+
);
|
|
173
225
|
expect(cohesionRegularFile).toBeLessThan(0.5);
|
|
174
226
|
});
|
|
175
227
|
});
|
|
@@ -20,7 +20,7 @@ describe('Auto-detection from folder structure', () => {
|
|
|
20
20
|
|
|
21
21
|
// Should detect 'payment' from processPayment (now part of auto-detected keywords)
|
|
22
22
|
expect(paymentsNode?.exports[0].inferredDomain).toBe('payment');
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
// Should detect 'order' from createOrder
|
|
25
25
|
expect(ordersNode?.exports[0].inferredDomain).toBe('order');
|
|
26
26
|
});
|
|
@@ -10,7 +10,7 @@ describe('Enhanced Cohesion Calculation', () => {
|
|
|
10
10
|
];
|
|
11
11
|
|
|
12
12
|
const cohesion = calculateCohesion(exports);
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
// With mixed domains (user, product) and no import data, should use domain-based calculation
|
|
15
15
|
// Domain entropy for 2 different domains = low cohesion
|
|
16
16
|
expect(cohesion).toBeLessThan(0.5);
|
|
@@ -33,7 +33,7 @@ describe('Enhanced Cohesion Calculation', () => {
|
|
|
33
33
|
];
|
|
34
34
|
|
|
35
35
|
const cohesion = calculateCohesion(exports);
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
// Even though domains differ, imports are identical (Jaccard = 1.0)
|
|
38
38
|
// Enhanced cohesion = 0.6 * 1.0 + 0.4 * 0.0 (different domains) = 0.6
|
|
39
39
|
// Should be >= 0.6 (import-based weight)
|
|
@@ -72,8 +72,10 @@ describe('Enhanced Cohesion Calculation', () => {
|
|
|
72
72
|
];
|
|
73
73
|
|
|
74
74
|
const cohesionWithShared = calculateCohesion(exportsWithSharedImports);
|
|
75
|
-
const cohesionWithoutShared = calculateCohesion(
|
|
76
|
-
|
|
75
|
+
const cohesionWithoutShared = calculateCohesion(
|
|
76
|
+
exportsWithoutSharedImports
|
|
77
|
+
);
|
|
78
|
+
|
|
77
79
|
// Shared imports should result in higher cohesion
|
|
78
80
|
expect(cohesionWithShared).toBeGreaterThan(cohesionWithoutShared);
|
|
79
81
|
});
|
|
@@ -95,7 +97,7 @@ describe('Enhanced Cohesion Calculation', () => {
|
|
|
95
97
|
];
|
|
96
98
|
|
|
97
99
|
const cohesion = calculateCohesion(exports);
|
|
98
|
-
|
|
100
|
+
|
|
99
101
|
// Should fall back to domain-based when not all exports have import data
|
|
100
102
|
expect(cohesion).toBeGreaterThan(0);
|
|
101
103
|
expect(cohesion).toBeLessThan(1);
|
|
@@ -116,8 +118,18 @@ describe('Enhanced Cohesion Calculation', () => {
|
|
|
116
118
|
|
|
117
119
|
it('should return 1 for test files regardless of domains or imports', () => {
|
|
118
120
|
const exports: ExportInfo[] = [
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
+
{
|
|
122
|
+
name: 'testUserLogin',
|
|
123
|
+
type: 'function',
|
|
124
|
+
inferredDomain: 'user',
|
|
125
|
+
imports: ['react'],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'testProductView',
|
|
129
|
+
type: 'function',
|
|
130
|
+
inferredDomain: 'product',
|
|
131
|
+
imports: [],
|
|
132
|
+
},
|
|
121
133
|
];
|
|
122
134
|
|
|
123
135
|
const cohesion = calculateCohesion(exports, 'src/utils/test-helpers.ts');
|