@aiready/context-analyzer 0.5.1 → 0.5.3
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 +8 -8
- package/.turbo/turbo-test.log +7 -6
- package/dist/chunk-EX7HCWAO.mjs +625 -0
- package/dist/cli.js +29 -14
- package/dist/cli.mjs +1 -1
- package/dist/index.js +29 -14
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/src/__tests__/analyzer.test.ts +24 -0
- package/src/analyzer.ts +50 -14
- package/src/index.ts +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/context-analyzer@0.5.
|
|
3
|
+
> @aiready/context-analyzer@0.5.3 build /Users/pengcao/projects/aiready/packages/context-analyzer
|
|
4
4
|
> tsup src/index.ts src/cli.ts --format cjs,esm --dts
|
|
5
5
|
|
|
6
6
|
[34mCLI[39m Building entry: src/cli.ts, src/index.ts
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
[34mCLI[39m Target: es2020
|
|
10
10
|
[34mCJS[39m Build start
|
|
11
11
|
[34mESM[39m Build start
|
|
12
|
-
[32mCJS[39m [1mdist/
|
|
13
|
-
[32mCJS[39m [1mdist/
|
|
14
|
-
[32mCJS[39m ⚡️ Build success in
|
|
15
|
-
[32mESM[39m [1mdist/index.mjs [22m[32m164.00 B[39m
|
|
12
|
+
[32mCJS[39m [1mdist/cli.js [22m[32m39.84 KB[39m
|
|
13
|
+
[32mCJS[39m [1mdist/index.js [22m[32m21.19 KB[39m
|
|
14
|
+
[32mCJS[39m ⚡️ Build success in 57ms
|
|
16
15
|
[32mESM[39m [1mdist/cli.mjs [22m[32m18.45 KB[39m
|
|
17
|
-
[32mESM[39m [1mdist/chunk-
|
|
18
|
-
[32mESM[39m
|
|
16
|
+
[32mESM[39m [1mdist/chunk-EX7HCWAO.mjs [22m[32m20.05 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m164.00 B[39m
|
|
18
|
+
[32mESM[39m ⚡️ Build success in 57ms
|
|
19
19
|
DTS Build start
|
|
20
|
-
DTS ⚡️ Build success in
|
|
20
|
+
DTS ⚡️ Build success in 529ms
|
|
21
21
|
DTS dist/cli.d.ts 20.00 B
|
|
22
22
|
DTS dist/index.d.ts 2.44 KB
|
|
23
23
|
DTS dist/cli.d.mts 20.00 B
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/context-analyzer@0.5.
|
|
3
|
+
> @aiready/context-analyzer@0.5.3 test /Users/pengcao/projects/aiready/packages/context-analyzer
|
|
4
4
|
> vitest run
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
[1m[7m[36m RUN [39m[27m[22m [36mv2.1.9 [39m[90m/Users/pengcao/projects/aiready/packages/context-analyzer[39m
|
|
8
8
|
|
|
9
|
-
[32m✓[39m [2msrc/__tests__/[22manalyzer[2m.test.ts[22m[2m (
|
|
9
|
+
[32m✓[39m [2msrc/__tests__/[22manalyzer[2m.test.ts[22m[2m (14)[22m
|
|
10
10
|
[32m✓[39m buildDependencyGraph[2m (1)[22m
|
|
11
11
|
[32m✓[39m should build a basic dependency graph
|
|
12
12
|
[32m✓[39m calculateImportDepth[2m (2)[22m
|
|
@@ -19,18 +19,19 @@
|
|
|
19
19
|
[32m✓[39m detectCircularDependencies[2m (2)[22m
|
|
20
20
|
[32m✓[39m should detect circular dependencies
|
|
21
21
|
[32m✓[39m should return empty for no circular dependencies
|
|
22
|
-
[32m✓[39m calculateCohesion[2m (
|
|
22
|
+
[32m✓[39m calculateCohesion[2m (4)[22m
|
|
23
23
|
[32m✓[39m should return 1 for single export
|
|
24
24
|
[32m✓[39m should return high cohesion for related exports
|
|
25
25
|
[32m✓[39m should return low cohesion for mixed exports
|
|
26
|
+
[32m✓[39m should return 1 for test files even with mixed domains
|
|
26
27
|
[32m✓[39m calculateFragmentation[2m (3)[22m
|
|
27
28
|
[32m✓[39m should return 0 for single file
|
|
28
29
|
[32m✓[39m should return 0 for files in same directory
|
|
29
30
|
[32m✓[39m should return high fragmentation for scattered files
|
|
30
31
|
|
|
31
32
|
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
32
|
-
[2m Tests [22m [1m[
|
|
33
|
-
[2m Start at [22m 08:
|
|
34
|
-
[2m Duration [22m
|
|
33
|
+
[2m Tests [22m [1m[32m14 passed[39m[22m[90m (14)[39m
|
|
34
|
+
[2m Start at [22m 08:26:52
|
|
35
|
+
[2m Duration [22m 317ms[2m (transform 60ms, setup 0ms, collect 67ms, tests 4ms, environment 0ms, prepare 46ms)[22m
|
|
35
36
|
|
|
36
37
|
[?25h
|
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { scanFiles, readFileContent } from "@aiready/core";
|
|
3
|
+
|
|
4
|
+
// src/analyzer.ts
|
|
5
|
+
import { estimateTokens } from "@aiready/core";
|
|
6
|
+
function buildDependencyGraph(files) {
|
|
7
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
8
|
+
const edges = /* @__PURE__ */ new Map();
|
|
9
|
+
for (const { file, content } of files) {
|
|
10
|
+
const imports = extractImportsFromContent(content);
|
|
11
|
+
const exports = extractExports(content);
|
|
12
|
+
const tokenCost = estimateTokens(content);
|
|
13
|
+
const linesOfCode = content.split("\n").length;
|
|
14
|
+
nodes.set(file, {
|
|
15
|
+
file,
|
|
16
|
+
imports,
|
|
17
|
+
exports,
|
|
18
|
+
tokenCost,
|
|
19
|
+
linesOfCode
|
|
20
|
+
});
|
|
21
|
+
edges.set(file, new Set(imports));
|
|
22
|
+
}
|
|
23
|
+
return { nodes, edges };
|
|
24
|
+
}
|
|
25
|
+
function extractImportsFromContent(content) {
|
|
26
|
+
const imports = [];
|
|
27
|
+
const patterns = [
|
|
28
|
+
/import\s+.*?\s+from\s+['"](.+?)['"]/g,
|
|
29
|
+
// import ... from '...'
|
|
30
|
+
/import\s+['"](.+?)['"]/g,
|
|
31
|
+
// import '...'
|
|
32
|
+
/require\(['"](.+?)['"]\)/g
|
|
33
|
+
// require('...')
|
|
34
|
+
];
|
|
35
|
+
for (const pattern of patterns) {
|
|
36
|
+
let match;
|
|
37
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
38
|
+
const importPath = match[1];
|
|
39
|
+
if (importPath && !importPath.startsWith("@") && !importPath.startsWith("node:")) {
|
|
40
|
+
imports.push(importPath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return [...new Set(imports)];
|
|
45
|
+
}
|
|
46
|
+
function calculateImportDepth(file, graph, visited = /* @__PURE__ */ new Set(), depth = 0) {
|
|
47
|
+
if (visited.has(file)) {
|
|
48
|
+
return depth;
|
|
49
|
+
}
|
|
50
|
+
const dependencies = graph.edges.get(file);
|
|
51
|
+
if (!dependencies || dependencies.size === 0) {
|
|
52
|
+
return depth;
|
|
53
|
+
}
|
|
54
|
+
visited.add(file);
|
|
55
|
+
let maxDepth = depth;
|
|
56
|
+
for (const dep of dependencies) {
|
|
57
|
+
const depDepth = calculateImportDepth(dep, graph, visited, depth + 1);
|
|
58
|
+
maxDepth = Math.max(maxDepth, depDepth);
|
|
59
|
+
}
|
|
60
|
+
visited.delete(file);
|
|
61
|
+
return maxDepth;
|
|
62
|
+
}
|
|
63
|
+
function getTransitiveDependencies(file, graph, visited = /* @__PURE__ */ new Set()) {
|
|
64
|
+
if (visited.has(file)) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
visited.add(file);
|
|
68
|
+
const dependencies = graph.edges.get(file);
|
|
69
|
+
if (!dependencies || dependencies.size === 0) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const allDeps = [];
|
|
73
|
+
for (const dep of dependencies) {
|
|
74
|
+
allDeps.push(dep);
|
|
75
|
+
allDeps.push(...getTransitiveDependencies(dep, graph, visited));
|
|
76
|
+
}
|
|
77
|
+
return [...new Set(allDeps)];
|
|
78
|
+
}
|
|
79
|
+
function calculateContextBudget(file, graph) {
|
|
80
|
+
const node = graph.nodes.get(file);
|
|
81
|
+
if (!node) return 0;
|
|
82
|
+
let totalTokens = node.tokenCost;
|
|
83
|
+
const deps = getTransitiveDependencies(file, graph);
|
|
84
|
+
for (const dep of deps) {
|
|
85
|
+
const depNode = graph.nodes.get(dep);
|
|
86
|
+
if (depNode) {
|
|
87
|
+
totalTokens += depNode.tokenCost;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return totalTokens;
|
|
91
|
+
}
|
|
92
|
+
function detectCircularDependencies(graph) {
|
|
93
|
+
const cycles = [];
|
|
94
|
+
const visited = /* @__PURE__ */ new Set();
|
|
95
|
+
const recursionStack = /* @__PURE__ */ new Set();
|
|
96
|
+
function dfs(file, path) {
|
|
97
|
+
if (recursionStack.has(file)) {
|
|
98
|
+
const cycleStart = path.indexOf(file);
|
|
99
|
+
if (cycleStart !== -1) {
|
|
100
|
+
cycles.push([...path.slice(cycleStart), file]);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (visited.has(file)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
visited.add(file);
|
|
108
|
+
recursionStack.add(file);
|
|
109
|
+
path.push(file);
|
|
110
|
+
const dependencies = graph.edges.get(file);
|
|
111
|
+
if (dependencies) {
|
|
112
|
+
for (const dep of dependencies) {
|
|
113
|
+
dfs(dep, [...path]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
recursionStack.delete(file);
|
|
117
|
+
}
|
|
118
|
+
for (const file of graph.nodes.keys()) {
|
|
119
|
+
if (!visited.has(file)) {
|
|
120
|
+
dfs(file, []);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return cycles;
|
|
124
|
+
}
|
|
125
|
+
function calculateCohesion(exports, filePath) {
|
|
126
|
+
if (exports.length === 0) return 1;
|
|
127
|
+
if (exports.length === 1) return 1;
|
|
128
|
+
if (filePath && isTestFile(filePath)) {
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
const domains = exports.map((e) => e.inferredDomain || "unknown");
|
|
132
|
+
const domainCounts = /* @__PURE__ */ new Map();
|
|
133
|
+
for (const domain of domains) {
|
|
134
|
+
domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
|
|
135
|
+
}
|
|
136
|
+
const total = domains.length;
|
|
137
|
+
let entropy = 0;
|
|
138
|
+
for (const count of domainCounts.values()) {
|
|
139
|
+
const p = count / total;
|
|
140
|
+
if (p > 0) {
|
|
141
|
+
entropy -= p * Math.log2(p);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const maxEntropy = Math.log2(total);
|
|
145
|
+
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
146
|
+
}
|
|
147
|
+
function isTestFile(filePath) {
|
|
148
|
+
const lower = filePath.toLowerCase();
|
|
149
|
+
return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__") || lower.includes(".test.") || lower.includes(".spec.");
|
|
150
|
+
}
|
|
151
|
+
function calculateFragmentation(files, domain) {
|
|
152
|
+
if (files.length <= 1) return 0;
|
|
153
|
+
const directories = new Set(files.map((f) => f.split("/").slice(0, -1).join("/")));
|
|
154
|
+
return (directories.size - 1) / (files.length - 1);
|
|
155
|
+
}
|
|
156
|
+
function detectModuleClusters(graph) {
|
|
157
|
+
const domainMap = /* @__PURE__ */ new Map();
|
|
158
|
+
for (const [file, node] of graph.nodes.entries()) {
|
|
159
|
+
const domains = node.exports.map((e) => e.inferredDomain || "unknown");
|
|
160
|
+
const primaryDomain = domains[0] || "unknown";
|
|
161
|
+
if (!domainMap.has(primaryDomain)) {
|
|
162
|
+
domainMap.set(primaryDomain, []);
|
|
163
|
+
}
|
|
164
|
+
domainMap.get(primaryDomain).push(file);
|
|
165
|
+
}
|
|
166
|
+
const clusters = [];
|
|
167
|
+
for (const [domain, files] of domainMap.entries()) {
|
|
168
|
+
if (files.length < 2) continue;
|
|
169
|
+
const totalTokens = files.reduce((sum, file) => {
|
|
170
|
+
const node = graph.nodes.get(file);
|
|
171
|
+
return sum + (node?.tokenCost || 0);
|
|
172
|
+
}, 0);
|
|
173
|
+
const fragmentationScore = calculateFragmentation(files, domain);
|
|
174
|
+
const avgCohesion = files.reduce((sum, file) => {
|
|
175
|
+
const node = graph.nodes.get(file);
|
|
176
|
+
return sum + (node ? calculateCohesion(node.exports, file) : 0);
|
|
177
|
+
}, 0) / files.length;
|
|
178
|
+
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
179
|
+
const consolidationPlan = generateConsolidationPlan(
|
|
180
|
+
domain,
|
|
181
|
+
files,
|
|
182
|
+
targetFiles
|
|
183
|
+
);
|
|
184
|
+
clusters.push({
|
|
185
|
+
domain,
|
|
186
|
+
files,
|
|
187
|
+
totalTokens,
|
|
188
|
+
fragmentationScore,
|
|
189
|
+
avgCohesion,
|
|
190
|
+
suggestedStructure: {
|
|
191
|
+
targetFiles,
|
|
192
|
+
consolidationPlan
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return clusters.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
|
|
197
|
+
}
|
|
198
|
+
function extractExports(content) {
|
|
199
|
+
const exports = [];
|
|
200
|
+
const patterns = [
|
|
201
|
+
/export\s+function\s+(\w+)/g,
|
|
202
|
+
/export\s+class\s+(\w+)/g,
|
|
203
|
+
/export\s+const\s+(\w+)/g,
|
|
204
|
+
/export\s+type\s+(\w+)/g,
|
|
205
|
+
/export\s+interface\s+(\w+)/g,
|
|
206
|
+
/export\s+default/g
|
|
207
|
+
];
|
|
208
|
+
const types = [
|
|
209
|
+
"function",
|
|
210
|
+
"class",
|
|
211
|
+
"const",
|
|
212
|
+
"type",
|
|
213
|
+
"interface",
|
|
214
|
+
"default"
|
|
215
|
+
];
|
|
216
|
+
patterns.forEach((pattern, index) => {
|
|
217
|
+
let match;
|
|
218
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
219
|
+
const name = match[1] || "default";
|
|
220
|
+
const type = types[index];
|
|
221
|
+
const inferredDomain = inferDomain(name);
|
|
222
|
+
exports.push({ name, type, inferredDomain });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
return exports;
|
|
226
|
+
}
|
|
227
|
+
function inferDomain(name) {
|
|
228
|
+
const lower = name.toLowerCase();
|
|
229
|
+
const domainKeywords = [
|
|
230
|
+
"authentication",
|
|
231
|
+
"authorization",
|
|
232
|
+
"payment",
|
|
233
|
+
"invoice",
|
|
234
|
+
"customer",
|
|
235
|
+
"product",
|
|
236
|
+
"order",
|
|
237
|
+
"cart",
|
|
238
|
+
"user",
|
|
239
|
+
"admin",
|
|
240
|
+
"repository",
|
|
241
|
+
"controller",
|
|
242
|
+
"service",
|
|
243
|
+
"config",
|
|
244
|
+
"model",
|
|
245
|
+
"view",
|
|
246
|
+
"auth",
|
|
247
|
+
"api",
|
|
248
|
+
"helper",
|
|
249
|
+
"util"
|
|
250
|
+
];
|
|
251
|
+
for (const keyword of domainKeywords) {
|
|
252
|
+
const wordBoundaryPattern = new RegExp(`\\b${keyword}\\b`, "i");
|
|
253
|
+
if (wordBoundaryPattern.test(name)) {
|
|
254
|
+
return keyword;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const keyword of domainKeywords) {
|
|
258
|
+
if (lower.includes(keyword)) {
|
|
259
|
+
return keyword;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return "unknown";
|
|
263
|
+
}
|
|
264
|
+
function generateConsolidationPlan(domain, files, targetFiles) {
|
|
265
|
+
const plan = [];
|
|
266
|
+
if (files.length <= targetFiles) {
|
|
267
|
+
return [`No consolidation needed for ${domain}`];
|
|
268
|
+
}
|
|
269
|
+
plan.push(
|
|
270
|
+
`Consolidate ${files.length} ${domain} files into ${targetFiles} cohesive file(s):`
|
|
271
|
+
);
|
|
272
|
+
const dirGroups = /* @__PURE__ */ new Map();
|
|
273
|
+
for (const file of files) {
|
|
274
|
+
const dir = file.split("/").slice(0, -1).join("/");
|
|
275
|
+
if (!dirGroups.has(dir)) {
|
|
276
|
+
dirGroups.set(dir, []);
|
|
277
|
+
}
|
|
278
|
+
dirGroups.get(dir).push(file);
|
|
279
|
+
}
|
|
280
|
+
plan.push(`1. Create unified ${domain} module file`);
|
|
281
|
+
plan.push(
|
|
282
|
+
`2. Move related functionality from ${files.length} scattered files`
|
|
283
|
+
);
|
|
284
|
+
plan.push(`3. Update imports in dependent files`);
|
|
285
|
+
plan.push(
|
|
286
|
+
`4. Remove old files after consolidation (verify with tests first)`
|
|
287
|
+
);
|
|
288
|
+
return plan;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/index.ts
|
|
292
|
+
async function getSmartDefaults(directory, userOptions) {
|
|
293
|
+
const files = await scanFiles({
|
|
294
|
+
rootDir: directory,
|
|
295
|
+
include: userOptions.include,
|
|
296
|
+
exclude: userOptions.exclude
|
|
297
|
+
});
|
|
298
|
+
const estimatedBlocks = files.length;
|
|
299
|
+
let maxDepth;
|
|
300
|
+
let maxContextBudget;
|
|
301
|
+
let minCohesion;
|
|
302
|
+
let maxFragmentation;
|
|
303
|
+
if (estimatedBlocks < 100) {
|
|
304
|
+
maxDepth = 4;
|
|
305
|
+
maxContextBudget = 8e3;
|
|
306
|
+
minCohesion = 0.5;
|
|
307
|
+
maxFragmentation = 0.5;
|
|
308
|
+
} else if (estimatedBlocks < 500) {
|
|
309
|
+
maxDepth = 5;
|
|
310
|
+
maxContextBudget = 15e3;
|
|
311
|
+
minCohesion = 0.45;
|
|
312
|
+
maxFragmentation = 0.6;
|
|
313
|
+
} else if (estimatedBlocks < 2e3) {
|
|
314
|
+
maxDepth = 7;
|
|
315
|
+
maxContextBudget = 25e3;
|
|
316
|
+
minCohesion = 0.4;
|
|
317
|
+
maxFragmentation = 0.7;
|
|
318
|
+
} else {
|
|
319
|
+
maxDepth = 10;
|
|
320
|
+
maxContextBudget = 4e4;
|
|
321
|
+
minCohesion = 0.35;
|
|
322
|
+
maxFragmentation = 0.8;
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
maxDepth,
|
|
326
|
+
maxContextBudget,
|
|
327
|
+
minCohesion,
|
|
328
|
+
maxFragmentation,
|
|
329
|
+
focus: "all",
|
|
330
|
+
includeNodeModules: false,
|
|
331
|
+
rootDir: userOptions.rootDir || directory,
|
|
332
|
+
include: userOptions.include,
|
|
333
|
+
exclude: userOptions.exclude
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
async function analyzeContext(options) {
|
|
337
|
+
const {
|
|
338
|
+
maxDepth = 5,
|
|
339
|
+
maxContextBudget = 1e4,
|
|
340
|
+
minCohesion = 0.6,
|
|
341
|
+
maxFragmentation = 0.5,
|
|
342
|
+
focus = "all",
|
|
343
|
+
includeNodeModules = false,
|
|
344
|
+
...scanOptions
|
|
345
|
+
} = options;
|
|
346
|
+
const files = await scanFiles({
|
|
347
|
+
...scanOptions,
|
|
348
|
+
// Only add node_modules to exclude if includeNodeModules is false
|
|
349
|
+
// The DEFAULT_EXCLUDE already includes node_modules, so this is only needed
|
|
350
|
+
// if user overrides the default exclude list
|
|
351
|
+
exclude: includeNodeModules && scanOptions.exclude ? scanOptions.exclude.filter((pattern) => pattern !== "**/node_modules/**") : scanOptions.exclude
|
|
352
|
+
});
|
|
353
|
+
const fileContents = await Promise.all(
|
|
354
|
+
files.map(async (file) => ({
|
|
355
|
+
file,
|
|
356
|
+
content: await readFileContent(file)
|
|
357
|
+
}))
|
|
358
|
+
);
|
|
359
|
+
const graph = buildDependencyGraph(fileContents);
|
|
360
|
+
const circularDeps = detectCircularDependencies(graph);
|
|
361
|
+
const clusters = detectModuleClusters(graph);
|
|
362
|
+
const fragmentationMap = /* @__PURE__ */ new Map();
|
|
363
|
+
for (const cluster of clusters) {
|
|
364
|
+
for (const file of cluster.files) {
|
|
365
|
+
fragmentationMap.set(file, cluster.fragmentationScore);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const results = [];
|
|
369
|
+
for (const { file } of fileContents) {
|
|
370
|
+
const node = graph.nodes.get(file);
|
|
371
|
+
if (!node) continue;
|
|
372
|
+
const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
|
|
373
|
+
const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
|
|
374
|
+
const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
|
|
375
|
+
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file) : 1;
|
|
376
|
+
const fragmentationScore = fragmentationMap.get(file) || 0;
|
|
377
|
+
const relatedFiles = [];
|
|
378
|
+
for (const cluster of clusters) {
|
|
379
|
+
if (cluster.files.includes(file)) {
|
|
380
|
+
relatedFiles.push(...cluster.files.filter((f) => f !== file));
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const { severity, issues, recommendations, potentialSavings } = analyzeIssues({
|
|
385
|
+
file,
|
|
386
|
+
importDepth,
|
|
387
|
+
contextBudget,
|
|
388
|
+
cohesionScore,
|
|
389
|
+
fragmentationScore,
|
|
390
|
+
maxDepth,
|
|
391
|
+
maxContextBudget,
|
|
392
|
+
minCohesion,
|
|
393
|
+
maxFragmentation,
|
|
394
|
+
circularDeps
|
|
395
|
+
});
|
|
396
|
+
const domains = [
|
|
397
|
+
...new Set(node.exports.map((e) => e.inferredDomain || "unknown"))
|
|
398
|
+
];
|
|
399
|
+
results.push({
|
|
400
|
+
file,
|
|
401
|
+
tokenCost: node.tokenCost,
|
|
402
|
+
linesOfCode: node.linesOfCode,
|
|
403
|
+
importDepth,
|
|
404
|
+
dependencyCount: dependencyList.length,
|
|
405
|
+
dependencyList,
|
|
406
|
+
circularDeps: circularDeps.filter((cycle) => cycle.includes(file)),
|
|
407
|
+
cohesionScore,
|
|
408
|
+
domains,
|
|
409
|
+
exportCount: node.exports.length,
|
|
410
|
+
contextBudget,
|
|
411
|
+
fragmentationScore,
|
|
412
|
+
relatedFiles,
|
|
413
|
+
severity,
|
|
414
|
+
issues,
|
|
415
|
+
recommendations,
|
|
416
|
+
potentialSavings
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
const issuesOnly = results.filter((r) => r.severity !== "info");
|
|
420
|
+
const sorted = issuesOnly.sort((a, b) => {
|
|
421
|
+
const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
|
|
422
|
+
const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
|
|
423
|
+
if (severityDiff !== 0) return severityDiff;
|
|
424
|
+
return b.contextBudget - a.contextBudget;
|
|
425
|
+
});
|
|
426
|
+
return sorted.length > 0 ? sorted : results;
|
|
427
|
+
}
|
|
428
|
+
function generateSummary(results) {
|
|
429
|
+
if (results.length === 0) {
|
|
430
|
+
return {
|
|
431
|
+
totalFiles: 0,
|
|
432
|
+
totalTokens: 0,
|
|
433
|
+
avgContextBudget: 0,
|
|
434
|
+
maxContextBudget: 0,
|
|
435
|
+
avgImportDepth: 0,
|
|
436
|
+
maxImportDepth: 0,
|
|
437
|
+
deepFiles: [],
|
|
438
|
+
avgFragmentation: 0,
|
|
439
|
+
fragmentedModules: [],
|
|
440
|
+
avgCohesion: 0,
|
|
441
|
+
lowCohesionFiles: [],
|
|
442
|
+
criticalIssues: 0,
|
|
443
|
+
majorIssues: 0,
|
|
444
|
+
minorIssues: 0,
|
|
445
|
+
totalPotentialSavings: 0,
|
|
446
|
+
topExpensiveFiles: []
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const totalFiles = results.length;
|
|
450
|
+
const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
|
|
451
|
+
const totalContextBudget = results.reduce(
|
|
452
|
+
(sum, r) => sum + r.contextBudget,
|
|
453
|
+
0
|
|
454
|
+
);
|
|
455
|
+
const avgContextBudget = totalContextBudget / totalFiles;
|
|
456
|
+
const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
|
|
457
|
+
const avgImportDepth = results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
|
|
458
|
+
const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
|
|
459
|
+
const deepFiles = results.filter((r) => r.importDepth >= 5).map((r) => ({ file: r.file, depth: r.importDepth })).sort((a, b) => b.depth - a.depth).slice(0, 10);
|
|
460
|
+
const avgFragmentation = results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
|
|
461
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
462
|
+
for (const result of results) {
|
|
463
|
+
for (const domain of result.domains) {
|
|
464
|
+
if (!moduleMap.has(domain)) {
|
|
465
|
+
moduleMap.set(domain, []);
|
|
466
|
+
}
|
|
467
|
+
moduleMap.get(domain).push(result);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const fragmentedModules = [];
|
|
471
|
+
for (const [domain, files] of moduleMap.entries()) {
|
|
472
|
+
if (files.length < 2) continue;
|
|
473
|
+
const fragmentationScore = files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
|
|
474
|
+
if (fragmentationScore < 0.3) continue;
|
|
475
|
+
const totalTokens2 = files.reduce((sum, f) => sum + f.tokenCost, 0);
|
|
476
|
+
const avgCohesion2 = files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
|
|
477
|
+
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
478
|
+
fragmentedModules.push({
|
|
479
|
+
domain,
|
|
480
|
+
files: files.map((f) => f.file),
|
|
481
|
+
totalTokens: totalTokens2,
|
|
482
|
+
fragmentationScore,
|
|
483
|
+
avgCohesion: avgCohesion2,
|
|
484
|
+
suggestedStructure: {
|
|
485
|
+
targetFiles,
|
|
486
|
+
consolidationPlan: [
|
|
487
|
+
`Consolidate ${files.length} ${domain} files into ${targetFiles} cohesive file(s)`,
|
|
488
|
+
`Current token cost: ${totalTokens2.toLocaleString()}`,
|
|
489
|
+
`Estimated savings: ${Math.floor(totalTokens2 * 0.3).toLocaleString()} tokens (30%)`
|
|
490
|
+
]
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
fragmentedModules.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
|
|
495
|
+
const avgCohesion = results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
|
|
496
|
+
const lowCohesionFiles = results.filter((r) => r.cohesionScore < 0.6).map((r) => ({ file: r.file, score: r.cohesionScore })).sort((a, b) => a.score - b.score).slice(0, 10);
|
|
497
|
+
const criticalIssues = results.filter((r) => r.severity === "critical").length;
|
|
498
|
+
const majorIssues = results.filter((r) => r.severity === "major").length;
|
|
499
|
+
const minorIssues = results.filter((r) => r.severity === "minor").length;
|
|
500
|
+
const totalPotentialSavings = results.reduce(
|
|
501
|
+
(sum, r) => sum + r.potentialSavings,
|
|
502
|
+
0
|
|
503
|
+
);
|
|
504
|
+
const topExpensiveFiles = results.sort((a, b) => b.contextBudget - a.contextBudget).slice(0, 10).map((r) => ({
|
|
505
|
+
file: r.file,
|
|
506
|
+
contextBudget: r.contextBudget,
|
|
507
|
+
severity: r.severity
|
|
508
|
+
}));
|
|
509
|
+
return {
|
|
510
|
+
totalFiles,
|
|
511
|
+
totalTokens,
|
|
512
|
+
avgContextBudget,
|
|
513
|
+
maxContextBudget,
|
|
514
|
+
avgImportDepth,
|
|
515
|
+
maxImportDepth,
|
|
516
|
+
deepFiles,
|
|
517
|
+
avgFragmentation,
|
|
518
|
+
fragmentedModules: fragmentedModules.slice(0, 10),
|
|
519
|
+
avgCohesion,
|
|
520
|
+
lowCohesionFiles,
|
|
521
|
+
criticalIssues,
|
|
522
|
+
majorIssues,
|
|
523
|
+
minorIssues,
|
|
524
|
+
totalPotentialSavings,
|
|
525
|
+
topExpensiveFiles
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
function analyzeIssues(params) {
|
|
529
|
+
const {
|
|
530
|
+
file,
|
|
531
|
+
importDepth,
|
|
532
|
+
contextBudget,
|
|
533
|
+
cohesionScore,
|
|
534
|
+
fragmentationScore,
|
|
535
|
+
maxDepth,
|
|
536
|
+
maxContextBudget,
|
|
537
|
+
minCohesion,
|
|
538
|
+
maxFragmentation,
|
|
539
|
+
circularDeps
|
|
540
|
+
} = params;
|
|
541
|
+
const issues = [];
|
|
542
|
+
const recommendations = [];
|
|
543
|
+
let severity = "info";
|
|
544
|
+
let potentialSavings = 0;
|
|
545
|
+
if (circularDeps.length > 0) {
|
|
546
|
+
severity = "critical";
|
|
547
|
+
issues.push(
|
|
548
|
+
`Part of ${circularDeps.length} circular dependency chain(s)`
|
|
549
|
+
);
|
|
550
|
+
recommendations.push("Break circular dependencies by extracting interfaces or using dependency injection");
|
|
551
|
+
potentialSavings += contextBudget * 0.2;
|
|
552
|
+
}
|
|
553
|
+
if (importDepth > maxDepth * 1.5) {
|
|
554
|
+
severity = severity === "critical" ? "critical" : "critical";
|
|
555
|
+
issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
|
|
556
|
+
recommendations.push("Flatten dependency tree or use facade pattern");
|
|
557
|
+
potentialSavings += contextBudget * 0.3;
|
|
558
|
+
} else if (importDepth > maxDepth) {
|
|
559
|
+
severity = severity === "critical" ? "critical" : "major";
|
|
560
|
+
issues.push(`Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`);
|
|
561
|
+
recommendations.push("Consider reducing dependency depth");
|
|
562
|
+
potentialSavings += contextBudget * 0.15;
|
|
563
|
+
}
|
|
564
|
+
if (contextBudget > maxContextBudget * 1.5) {
|
|
565
|
+
severity = severity === "critical" ? "critical" : "critical";
|
|
566
|
+
issues.push(`Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`);
|
|
567
|
+
recommendations.push("Split into smaller modules or reduce dependency tree");
|
|
568
|
+
potentialSavings += contextBudget * 0.4;
|
|
569
|
+
} else if (contextBudget > maxContextBudget) {
|
|
570
|
+
severity = severity === "critical" || severity === "major" ? severity : "major";
|
|
571
|
+
issues.push(`Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`);
|
|
572
|
+
recommendations.push("Reduce file size or dependencies");
|
|
573
|
+
potentialSavings += contextBudget * 0.2;
|
|
574
|
+
}
|
|
575
|
+
if (cohesionScore < minCohesion * 0.5) {
|
|
576
|
+
severity = severity === "critical" ? "critical" : "major";
|
|
577
|
+
issues.push(`Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`);
|
|
578
|
+
recommendations.push("Split file by domain - separate unrelated functionality");
|
|
579
|
+
potentialSavings += contextBudget * 0.25;
|
|
580
|
+
} else if (cohesionScore < minCohesion) {
|
|
581
|
+
severity = severity === "critical" || severity === "major" ? severity : "minor";
|
|
582
|
+
issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
|
|
583
|
+
recommendations.push("Consider grouping related exports together");
|
|
584
|
+
potentialSavings += contextBudget * 0.1;
|
|
585
|
+
}
|
|
586
|
+
if (fragmentationScore > maxFragmentation) {
|
|
587
|
+
severity = severity === "critical" || severity === "major" ? severity : "minor";
|
|
588
|
+
issues.push(`High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`);
|
|
589
|
+
recommendations.push("Consolidate with related files in same domain");
|
|
590
|
+
potentialSavings += contextBudget * 0.3;
|
|
591
|
+
}
|
|
592
|
+
if (issues.length === 0) {
|
|
593
|
+
issues.push("No significant issues detected");
|
|
594
|
+
recommendations.push("File is well-structured for AI context usage");
|
|
595
|
+
}
|
|
596
|
+
if (isBuildArtifact(file)) {
|
|
597
|
+
issues.push("Detected build artifact (bundled/output file)");
|
|
598
|
+
recommendations.push("Exclude build outputs (e.g., cdk.out, dist, build, .next) from analysis");
|
|
599
|
+
severity = downgradeSeverity(severity);
|
|
600
|
+
potentialSavings = 0;
|
|
601
|
+
}
|
|
602
|
+
return { severity, issues, recommendations, potentialSavings: Math.floor(potentialSavings) };
|
|
603
|
+
}
|
|
604
|
+
function isBuildArtifact(filePath) {
|
|
605
|
+
const lower = filePath.toLowerCase();
|
|
606
|
+
return lower.includes("/node_modules/") || lower.includes("/dist/") || lower.includes("/build/") || lower.includes("/out/") || lower.includes("/output/") || lower.includes("/cdk.out/") || lower.includes("/.next/") || /\/asset\.[^/]+\//.test(lower);
|
|
607
|
+
}
|
|
608
|
+
function downgradeSeverity(s) {
|
|
609
|
+
switch (s) {
|
|
610
|
+
case "critical":
|
|
611
|
+
return "minor";
|
|
612
|
+
case "major":
|
|
613
|
+
return "minor";
|
|
614
|
+
case "minor":
|
|
615
|
+
return "info";
|
|
616
|
+
default:
|
|
617
|
+
return "info";
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export {
|
|
622
|
+
getSmartDefaults,
|
|
623
|
+
analyzeContext,
|
|
624
|
+
generateSummary
|
|
625
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -150,9 +150,12 @@ function detectCircularDependencies(graph) {
|
|
|
150
150
|
}
|
|
151
151
|
return cycles;
|
|
152
152
|
}
|
|
153
|
-
function calculateCohesion(exports2) {
|
|
153
|
+
function calculateCohesion(exports2, filePath) {
|
|
154
154
|
if (exports2.length === 0) return 1;
|
|
155
155
|
if (exports2.length === 1) return 1;
|
|
156
|
+
if (filePath && isTestFile(filePath)) {
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
156
159
|
const domains = exports2.map((e) => e.inferredDomain || "unknown");
|
|
157
160
|
const domainCounts = /* @__PURE__ */ new Map();
|
|
158
161
|
for (const domain of domains) {
|
|
@@ -169,6 +172,10 @@ function calculateCohesion(exports2) {
|
|
|
169
172
|
const maxEntropy = Math.log2(total);
|
|
170
173
|
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
171
174
|
}
|
|
175
|
+
function isTestFile(filePath) {
|
|
176
|
+
const lower = filePath.toLowerCase();
|
|
177
|
+
return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__") || lower.includes(".test.") || lower.includes(".spec.");
|
|
178
|
+
}
|
|
172
179
|
function calculateFragmentation(files, domain) {
|
|
173
180
|
if (files.length <= 1) return 0;
|
|
174
181
|
const directories = new Set(files.map((f) => f.split("/").slice(0, -1).join("/")));
|
|
@@ -194,7 +201,7 @@ function detectModuleClusters(graph) {
|
|
|
194
201
|
const fragmentationScore = calculateFragmentation(files, domain);
|
|
195
202
|
const avgCohesion = files.reduce((sum, file) => {
|
|
196
203
|
const node = graph.nodes.get(file);
|
|
197
|
-
return sum + (node ? calculateCohesion(node.exports) : 0);
|
|
204
|
+
return sum + (node ? calculateCohesion(node.exports, file) : 0);
|
|
198
205
|
}, 0) / files.length;
|
|
199
206
|
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
200
207
|
const consolidationPlan = generateConsolidationPlan(
|
|
@@ -248,25 +255,33 @@ function extractExports(content) {
|
|
|
248
255
|
function inferDomain(name) {
|
|
249
256
|
const lower = name.toLowerCase();
|
|
250
257
|
const domainKeywords = [
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
"order",
|
|
254
|
-
"product",
|
|
258
|
+
"authentication",
|
|
259
|
+
"authorization",
|
|
255
260
|
"payment",
|
|
256
|
-
"cart",
|
|
257
261
|
"invoice",
|
|
258
262
|
"customer",
|
|
263
|
+
"product",
|
|
264
|
+
"order",
|
|
265
|
+
"cart",
|
|
266
|
+
"user",
|
|
259
267
|
"admin",
|
|
260
|
-
"api",
|
|
261
|
-
"util",
|
|
262
|
-
"helper",
|
|
263
|
-
"config",
|
|
264
|
-
"service",
|
|
265
268
|
"repository",
|
|
266
269
|
"controller",
|
|
270
|
+
"service",
|
|
271
|
+
"config",
|
|
267
272
|
"model",
|
|
268
|
-
"view"
|
|
273
|
+
"view",
|
|
274
|
+
"auth",
|
|
275
|
+
"api",
|
|
276
|
+
"helper",
|
|
277
|
+
"util"
|
|
269
278
|
];
|
|
279
|
+
for (const keyword of domainKeywords) {
|
|
280
|
+
const wordBoundaryPattern = new RegExp(`\\b${keyword}\\b`, "i");
|
|
281
|
+
if (wordBoundaryPattern.test(name)) {
|
|
282
|
+
return keyword;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
270
285
|
for (const keyword of domainKeywords) {
|
|
271
286
|
if (lower.includes(keyword)) {
|
|
272
287
|
return keyword;
|
|
@@ -341,7 +356,7 @@ async function analyzeContext(options) {
|
|
|
341
356
|
const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
|
|
342
357
|
const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
|
|
343
358
|
const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
|
|
344
|
-
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports) : 1;
|
|
359
|
+
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file) : 1;
|
|
345
360
|
const fragmentationScore = fragmentationMap.get(file) || 0;
|
|
346
361
|
const relatedFiles = [];
|
|
347
362
|
for (const cluster of clusters) {
|
package/dist/cli.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -148,9 +148,12 @@ function detectCircularDependencies(graph) {
|
|
|
148
148
|
}
|
|
149
149
|
return cycles;
|
|
150
150
|
}
|
|
151
|
-
function calculateCohesion(exports2) {
|
|
151
|
+
function calculateCohesion(exports2, filePath) {
|
|
152
152
|
if (exports2.length === 0) return 1;
|
|
153
153
|
if (exports2.length === 1) return 1;
|
|
154
|
+
if (filePath && isTestFile(filePath)) {
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
154
157
|
const domains = exports2.map((e) => e.inferredDomain || "unknown");
|
|
155
158
|
const domainCounts = /* @__PURE__ */ new Map();
|
|
156
159
|
for (const domain of domains) {
|
|
@@ -167,6 +170,10 @@ function calculateCohesion(exports2) {
|
|
|
167
170
|
const maxEntropy = Math.log2(total);
|
|
168
171
|
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
169
172
|
}
|
|
173
|
+
function isTestFile(filePath) {
|
|
174
|
+
const lower = filePath.toLowerCase();
|
|
175
|
+
return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__") || lower.includes(".test.") || lower.includes(".spec.");
|
|
176
|
+
}
|
|
170
177
|
function calculateFragmentation(files, domain) {
|
|
171
178
|
if (files.length <= 1) return 0;
|
|
172
179
|
const directories = new Set(files.map((f) => f.split("/").slice(0, -1).join("/")));
|
|
@@ -192,7 +199,7 @@ function detectModuleClusters(graph) {
|
|
|
192
199
|
const fragmentationScore = calculateFragmentation(files, domain);
|
|
193
200
|
const avgCohesion = files.reduce((sum, file) => {
|
|
194
201
|
const node = graph.nodes.get(file);
|
|
195
|
-
return sum + (node ? calculateCohesion(node.exports) : 0);
|
|
202
|
+
return sum + (node ? calculateCohesion(node.exports, file) : 0);
|
|
196
203
|
}, 0) / files.length;
|
|
197
204
|
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
198
205
|
const consolidationPlan = generateConsolidationPlan(
|
|
@@ -246,25 +253,33 @@ function extractExports(content) {
|
|
|
246
253
|
function inferDomain(name) {
|
|
247
254
|
const lower = name.toLowerCase();
|
|
248
255
|
const domainKeywords = [
|
|
249
|
-
"
|
|
250
|
-
"
|
|
251
|
-
"order",
|
|
252
|
-
"product",
|
|
256
|
+
"authentication",
|
|
257
|
+
"authorization",
|
|
253
258
|
"payment",
|
|
254
|
-
"cart",
|
|
255
259
|
"invoice",
|
|
256
260
|
"customer",
|
|
261
|
+
"product",
|
|
262
|
+
"order",
|
|
263
|
+
"cart",
|
|
264
|
+
"user",
|
|
257
265
|
"admin",
|
|
258
|
-
"api",
|
|
259
|
-
"util",
|
|
260
|
-
"helper",
|
|
261
|
-
"config",
|
|
262
|
-
"service",
|
|
263
266
|
"repository",
|
|
264
267
|
"controller",
|
|
268
|
+
"service",
|
|
269
|
+
"config",
|
|
265
270
|
"model",
|
|
266
|
-
"view"
|
|
271
|
+
"view",
|
|
272
|
+
"auth",
|
|
273
|
+
"api",
|
|
274
|
+
"helper",
|
|
275
|
+
"util"
|
|
267
276
|
];
|
|
277
|
+
for (const keyword of domainKeywords) {
|
|
278
|
+
const wordBoundaryPattern = new RegExp(`\\b${keyword}\\b`, "i");
|
|
279
|
+
if (wordBoundaryPattern.test(name)) {
|
|
280
|
+
return keyword;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
268
283
|
for (const keyword of domainKeywords) {
|
|
269
284
|
if (lower.includes(keyword)) {
|
|
270
285
|
return keyword;
|
|
@@ -383,7 +398,7 @@ async function analyzeContext(options) {
|
|
|
383
398
|
const importDepth = focus === "depth" || focus === "all" ? calculateImportDepth(file, graph) : 0;
|
|
384
399
|
const dependencyList = focus === "depth" || focus === "all" ? getTransitiveDependencies(file, graph) : [];
|
|
385
400
|
const contextBudget = focus === "all" ? calculateContextBudget(file, graph) : node.tokenCost;
|
|
386
|
-
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports) : 1;
|
|
401
|
+
const cohesionScore = focus === "cohesion" || focus === "all" ? calculateCohesion(node.exports, file) : 1;
|
|
387
402
|
const fragmentationScore = fragmentationMap.get(file) || 0;
|
|
388
403
|
const relatedFiles = [];
|
|
389
404
|
for (const cluster of clusters) {
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/context-analyzer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
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",
|
|
@@ -148,6 +148,30 @@ describe('calculateCohesion', () => {
|
|
|
148
148
|
const cohesion = calculateCohesion(exports);
|
|
149
149
|
expect(cohesion).toBeLessThan(0.5);
|
|
150
150
|
});
|
|
151
|
+
|
|
152
|
+
it('should return 1 for test files even with mixed domains', () => {
|
|
153
|
+
const exports = [
|
|
154
|
+
{ name: 'mockUser', type: 'function' as const, inferredDomain: 'user' },
|
|
155
|
+
{ name: 'mockOrder', type: 'function' as const, inferredDomain: 'order' },
|
|
156
|
+
{ name: 'setupTestDb', type: 'function' as const, inferredDomain: 'helper' },
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
// Test file - should return 1 despite mixed domains
|
|
160
|
+
const cohesionTestFile = calculateCohesion(exports, 'src/__tests__/helpers.test.ts');
|
|
161
|
+
expect(cohesionTestFile).toBe(1);
|
|
162
|
+
|
|
163
|
+
// Mock file - should return 1 despite mixed domains
|
|
164
|
+
const cohesionMockFile = calculateCohesion(exports, 'src/test-utils/mocks.ts');
|
|
165
|
+
expect(cohesionMockFile).toBe(1);
|
|
166
|
+
|
|
167
|
+
// Fixture file - should return 1 despite mixed domains
|
|
168
|
+
const cohesionFixtureFile = calculateCohesion(exports, 'src/fixtures/data.ts');
|
|
169
|
+
expect(cohesionFixtureFile).toBe(1);
|
|
170
|
+
|
|
171
|
+
// Regular file - should have low cohesion
|
|
172
|
+
const cohesionRegularFile = calculateCohesion(exports, 'src/utils/helpers.ts');
|
|
173
|
+
expect(cohesionRegularFile).toBeLessThan(0.5);
|
|
174
|
+
});
|
|
151
175
|
});
|
|
152
176
|
|
|
153
177
|
describe('calculateFragmentation', () => {
|
package/src/analyzer.ts
CHANGED
|
@@ -200,11 +200,19 @@ export function detectCircularDependencies(
|
|
|
200
200
|
/**
|
|
201
201
|
* Calculate cohesion score (how related are exports in a file)
|
|
202
202
|
* Uses entropy: low entropy = high cohesion
|
|
203
|
+
* @param exports - Array of export information
|
|
204
|
+
* @param filePath - Optional file path for context-aware scoring
|
|
203
205
|
*/
|
|
204
|
-
export function calculateCohesion(exports: ExportInfo[]): number {
|
|
206
|
+
export function calculateCohesion(exports: ExportInfo[], filePath?: string): number {
|
|
205
207
|
if (exports.length === 0) return 1;
|
|
206
208
|
if (exports.length === 1) return 1; // Single export = perfect cohesion
|
|
207
209
|
|
|
210
|
+
// Special case: Test/mock/fixture files are expected to have multi-domain exports
|
|
211
|
+
// They serve a single purpose (testing) even if they mock different domains
|
|
212
|
+
if (filePath && isTestFile(filePath)) {
|
|
213
|
+
return 1; // Test utilities are inherently cohesive despite mixed domains
|
|
214
|
+
}
|
|
215
|
+
|
|
208
216
|
const domains = exports.map((e) => e.inferredDomain || 'unknown');
|
|
209
217
|
const domainCounts = new Map<string, number>();
|
|
210
218
|
|
|
@@ -228,6 +236,22 @@ export function calculateCohesion(exports: ExportInfo[]): number {
|
|
|
228
236
|
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
229
237
|
}
|
|
230
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Check if a file is a test/mock/fixture file
|
|
241
|
+
*/
|
|
242
|
+
function isTestFile(filePath: string): boolean {
|
|
243
|
+
const lower = filePath.toLowerCase();
|
|
244
|
+
return (
|
|
245
|
+
lower.includes('test') ||
|
|
246
|
+
lower.includes('spec') ||
|
|
247
|
+
lower.includes('mock') ||
|
|
248
|
+
lower.includes('fixture') ||
|
|
249
|
+
lower.includes('__tests__') ||
|
|
250
|
+
lower.includes('.test.') ||
|
|
251
|
+
lower.includes('.spec.')
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
231
255
|
/**
|
|
232
256
|
* Calculate fragmentation score (how scattered is a domain)
|
|
233
257
|
*/
|
|
@@ -279,7 +303,7 @@ export function detectModuleClusters(
|
|
|
279
303
|
const avgCohesion =
|
|
280
304
|
files.reduce((sum, file) => {
|
|
281
305
|
const node = graph.nodes.get(file);
|
|
282
|
-
return sum + (node ? calculateCohesion(node.exports) : 0);
|
|
306
|
+
return sum + (node ? calculateCohesion(node.exports, file) : 0);
|
|
283
307
|
}, 0) / files.length;
|
|
284
308
|
|
|
285
309
|
// Generate consolidation plan
|
|
@@ -349,33 +373,45 @@ function extractExports(content: string): ExportInfo[] {
|
|
|
349
373
|
|
|
350
374
|
/**
|
|
351
375
|
* Infer domain from export name
|
|
352
|
-
* Uses common naming patterns
|
|
376
|
+
* Uses common naming patterns with word boundary matching
|
|
353
377
|
*/
|
|
354
378
|
function inferDomain(name: string): string {
|
|
355
379
|
const lower = name.toLowerCase();
|
|
356
380
|
|
|
357
|
-
//
|
|
381
|
+
// Domain keywords ordered from most specific to most general
|
|
382
|
+
// This prevents generic terms like 'util' from matching before specific domains
|
|
358
383
|
const domainKeywords = [
|
|
359
|
-
'
|
|
360
|
-
'
|
|
361
|
-
'order',
|
|
362
|
-
'product',
|
|
384
|
+
'authentication',
|
|
385
|
+
'authorization',
|
|
363
386
|
'payment',
|
|
364
|
-
'cart',
|
|
365
387
|
'invoice',
|
|
366
388
|
'customer',
|
|
389
|
+
'product',
|
|
390
|
+
'order',
|
|
391
|
+
'cart',
|
|
392
|
+
'user',
|
|
367
393
|
'admin',
|
|
368
|
-
'api',
|
|
369
|
-
'util',
|
|
370
|
-
'helper',
|
|
371
|
-
'config',
|
|
372
|
-
'service',
|
|
373
394
|
'repository',
|
|
374
395
|
'controller',
|
|
396
|
+
'service',
|
|
397
|
+
'config',
|
|
375
398
|
'model',
|
|
376
399
|
'view',
|
|
400
|
+
'auth',
|
|
401
|
+
'api',
|
|
402
|
+
'helper',
|
|
403
|
+
'util',
|
|
377
404
|
];
|
|
378
405
|
|
|
406
|
+
// Try word boundary matching first for more accurate detection
|
|
407
|
+
for (const keyword of domainKeywords) {
|
|
408
|
+
const wordBoundaryPattern = new RegExp(`\\b${keyword}\\b`, 'i');
|
|
409
|
+
if (wordBoundaryPattern.test(name)) {
|
|
410
|
+
return keyword;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Fallback to substring matching for compound words
|
|
379
415
|
for (const keyword of domainKeywords) {
|
|
380
416
|
if (lower.includes(keyword)) {
|
|
381
417
|
return keyword;
|
package/src/index.ts
CHANGED
|
@@ -157,7 +157,7 @@ export async function analyzeContext(
|
|
|
157
157
|
|
|
158
158
|
const cohesionScore =
|
|
159
159
|
focus === 'cohesion' || focus === 'all'
|
|
160
|
-
? calculateCohesion(node.exports)
|
|
160
|
+
? calculateCohesion(node.exports, file)
|
|
161
161
|
: 1;
|
|
162
162
|
|
|
163
163
|
const fragmentationScore = fragmentationMap.get(file) || 0;
|