@aiready/context-analyzer 0.22.14 → 0.22.16
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 +19 -19
- package/.turbo/turbo-format-check.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +347 -25
- package/.turbo/turbo-type-check.log +1 -1
- package/dist/chunk-2LEHY2GV.mjs +1287 -0
- package/dist/chunk-P2ZQGQAO.mjs +1282 -0
- package/dist/chunk-QGI23DBA.mjs +1282 -0
- package/dist/chunk-QTB4KYCX.mjs +1260 -0
- package/dist/chunk-RQ5BQLT6.mjs +102 -0
- package/dist/chunk-VYFHSGV6.mjs +1283 -0
- package/dist/chunk-WLXLBWDU.mjs +96 -0
- package/dist/chunk-XDYPMFCH.mjs +1250 -0
- package/dist/cli-action-332WE54N.mjs +95 -0
- package/dist/cli-action-7QXG7LHS.mjs +95 -0
- package/dist/cli-action-BIX6TYXF.mjs +95 -0
- package/dist/cli-action-BUGVCH44.mjs +95 -0
- package/dist/cli-action-JKG3R6RV.mjs +93 -0
- package/dist/cli-action-RO24U52W.mjs +95 -0
- package/dist/cli-action-WAZ5KM6X.mjs +95 -0
- package/dist/cli-action-XDKINE2R.mjs +95 -0
- package/dist/cli-action-Y6VATXMV.mjs +95 -0
- package/dist/cli.js +79 -31
- package/dist/cli.mjs +1 -1
- package/dist/console-report-CVGRMWEU.mjs +74 -0
- package/dist/html-report-BYGKWC3K.mjs +73 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +71 -25
- package/dist/index.mjs +3 -3
- package/dist/orchestrator-2KQNMO2L.mjs +10 -0
- package/dist/orchestrator-66ZVNOLR.mjs +10 -0
- package/dist/orchestrator-KM2OJPZD.mjs +10 -0
- package/dist/orchestrator-MKDZPRBA.mjs +10 -0
- package/dist/orchestrator-QSHWWBWS.mjs +10 -0
- package/dist/orchestrator-WFQPMNSD.mjs +10 -0
- package/dist/python-context-H2OLC5JN.mjs +162 -0
- package/dist/python-context-OBP7JD5P.mjs +162 -0
- package/package.json +8 -6
- package/src/__tests__/analyzer.test.ts +4 -3
- package/src/__tests__/issue-analyzer.test.ts +4 -2
- package/src/classify/file-classifiers.ts +14 -13
- package/src/cli-action.ts +6 -3
- package/src/graph-builder.ts +43 -8
- package/src/issue-analyzer.ts +19 -7
- package/src/orchestrator.ts +6 -4
- package/src/report/console-report.ts +2 -2
- package/src/report/html-report.ts +2 -2
- package/src/semantic/domain-inference.ts +1 -1
- package/src/types.ts +2 -0
- package/src/utils/dependency-graph-utils.ts +22 -13
package/dist/index.js
CHANGED
|
@@ -177,6 +177,7 @@ function analyzeIssues(params) {
|
|
|
177
177
|
const {
|
|
178
178
|
file,
|
|
179
179
|
importDepth,
|
|
180
|
+
tokenCost,
|
|
180
181
|
contextBudget,
|
|
181
182
|
cohesionScore,
|
|
182
183
|
fragmentationScore,
|
|
@@ -211,21 +212,29 @@ function analyzeIssues(params) {
|
|
|
211
212
|
recommendations.push("Consider reducing dependency depth");
|
|
212
213
|
potentialSavings += contextBudget * 0.15;
|
|
213
214
|
}
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
const MAX_FILE_TOKENS = 1e4;
|
|
216
|
+
if (tokenCost > MAX_FILE_TOKENS) {
|
|
217
|
+
if (severity !== import_core3.Severity.Critical) severity = import_core3.Severity.Major;
|
|
216
218
|
issues.push(
|
|
217
|
-
`
|
|
219
|
+
`File is excessively large (${tokenCost.toLocaleString()} tokens)`
|
|
218
220
|
);
|
|
219
221
|
recommendations.push(
|
|
220
|
-
"Split into smaller modules
|
|
222
|
+
"Split file into smaller, single-responsibility modules"
|
|
221
223
|
);
|
|
224
|
+
}
|
|
225
|
+
if (contextBudget > maxContextBudget * 1.5) {
|
|
226
|
+
severity = import_core3.Severity.Critical;
|
|
227
|
+
issues.push(
|
|
228
|
+
`Total context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
|
|
229
|
+
);
|
|
230
|
+
recommendations.push("Reduce dependency tree width or reduce deep imports");
|
|
222
231
|
potentialSavings += contextBudget * 0.4;
|
|
223
232
|
} else if (contextBudget > maxContextBudget) {
|
|
224
233
|
if (severity !== import_core3.Severity.Critical) severity = import_core3.Severity.Major;
|
|
225
234
|
issues.push(
|
|
226
|
-
`
|
|
235
|
+
`Total context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
|
|
227
236
|
);
|
|
228
|
-
recommendations.push("
|
|
237
|
+
recommendations.push("Optimize dependency graph and reduce deep imports");
|
|
229
238
|
potentialSavings += contextBudget * 0.2;
|
|
230
239
|
}
|
|
231
240
|
if (cohesionScore < minCohesion * 0.5) {
|
|
@@ -310,19 +319,27 @@ function calculateImportDepthFromEdges(file, edges, visited = /* @__PURE__ */ ne
|
|
|
310
319
|
return maxDepth;
|
|
311
320
|
}
|
|
312
321
|
function getTransitiveDependenciesFromEdges(file, edges, visited = /* @__PURE__ */ new Set()) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
)
|
|
322
|
+
const result = /* @__PURE__ */ new Map();
|
|
323
|
+
function dfs(current, depth) {
|
|
324
|
+
if (visited.has(current)) {
|
|
325
|
+
const existingDepth = result.get(current);
|
|
326
|
+
if (existingDepth !== void 0 && depth < existingDepth) {
|
|
327
|
+
result.set(current, depth);
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
visited.add(current);
|
|
332
|
+
if (current !== file) {
|
|
333
|
+
result.set(current, depth);
|
|
334
|
+
}
|
|
335
|
+
const dependencies = edges.get(current);
|
|
336
|
+
if (!dependencies) return;
|
|
337
|
+
for (const dep of dependencies) {
|
|
338
|
+
dfs(dep, depth + 1);
|
|
339
|
+
}
|
|
324
340
|
}
|
|
325
|
-
|
|
341
|
+
dfs(file, 0);
|
|
342
|
+
return result;
|
|
326
343
|
}
|
|
327
344
|
function detectGraphCycles(edges) {
|
|
328
345
|
const cycles = [];
|
|
@@ -647,6 +664,30 @@ var init_ast_utils = __esm({
|
|
|
647
664
|
// src/graph-builder.ts
|
|
648
665
|
function resolveImport(source, importingFile, allFiles) {
|
|
649
666
|
if (!source.startsWith(".") && !source.startsWith("/")) {
|
|
667
|
+
const externalIgnores = [
|
|
668
|
+
"react",
|
|
669
|
+
"next",
|
|
670
|
+
"lucide-react",
|
|
671
|
+
"framer-motion",
|
|
672
|
+
"@aws-sdk",
|
|
673
|
+
"stripe",
|
|
674
|
+
"clsx",
|
|
675
|
+
"tailwind-merge",
|
|
676
|
+
"zod",
|
|
677
|
+
"commander",
|
|
678
|
+
"chalk",
|
|
679
|
+
"fs",
|
|
680
|
+
"path",
|
|
681
|
+
"util",
|
|
682
|
+
"child_process",
|
|
683
|
+
"os",
|
|
684
|
+
"crypto"
|
|
685
|
+
];
|
|
686
|
+
if (externalIgnores.some(
|
|
687
|
+
(pkg) => source === pkg || source.startsWith(`${pkg}/`)
|
|
688
|
+
)) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
650
691
|
if (source.startsWith("@aiready/")) {
|
|
651
692
|
const pkgName = source.split("/")[1];
|
|
652
693
|
const possiblePaths = [
|
|
@@ -725,8 +766,9 @@ async function buildDependencyGraph(files, options) {
|
|
|
725
766
|
const allFilePaths = new Set(files.map((f) => f.file));
|
|
726
767
|
for (const { file, content } of files) {
|
|
727
768
|
const { imports: astImports } = await (0, import_core5.parseFileExports)(content, file);
|
|
728
|
-
const
|
|
729
|
-
const
|
|
769
|
+
const runtimeImports = astImports.filter((i) => !i.isTypeOnly);
|
|
770
|
+
const resolvedImports = runtimeImports.map((i) => resolveImport(i.source, file, allFilePaths)).filter((path) => path !== null);
|
|
771
|
+
const importSources = runtimeImports.map((i) => i.source);
|
|
730
772
|
const exports2 = await extractExportsWithAST(
|
|
731
773
|
content,
|
|
732
774
|
file,
|
|
@@ -779,13 +821,14 @@ function calculateContextBudget(file, graph) {
|
|
|
779
821
|
if (!node) return 0;
|
|
780
822
|
let totalTokens = node.tokenCost;
|
|
781
823
|
const deps = getTransitiveDependencies(file, graph);
|
|
782
|
-
for (const dep of deps) {
|
|
824
|
+
for (const [dep, depth] of deps.entries()) {
|
|
783
825
|
const depNode = graph.nodes.get(dep);
|
|
784
826
|
if (depNode) {
|
|
785
|
-
|
|
827
|
+
const discountFactor = Math.pow(0.8, depth - 1);
|
|
828
|
+
totalTokens += depNode.tokenCost * discountFactor;
|
|
786
829
|
}
|
|
787
830
|
}
|
|
788
|
-
return totalTokens;
|
|
831
|
+
return Math.round(totalTokens);
|
|
789
832
|
}
|
|
790
833
|
function detectCircularDependencies(graph) {
|
|
791
834
|
return detectGraphCycles(graph.edges);
|
|
@@ -1604,6 +1647,7 @@ function mapNodeToResult(node, graph, clusters, allCircularDeps, options) {
|
|
|
1604
1647
|
{
|
|
1605
1648
|
file,
|
|
1606
1649
|
importDepth,
|
|
1650
|
+
tokenCost,
|
|
1607
1651
|
contextBudget,
|
|
1608
1652
|
cohesionScore,
|
|
1609
1653
|
fragmentationScore,
|
|
@@ -1627,8 +1671,8 @@ function mapNodeToResult(node, graph, clusters, allCircularDeps, options) {
|
|
|
1627
1671
|
tokenCost,
|
|
1628
1672
|
linesOfCode: node.linesOfCode,
|
|
1629
1673
|
importDepth,
|
|
1630
|
-
dependencyCount: transitiveDeps.
|
|
1631
|
-
dependencyList: transitiveDeps,
|
|
1674
|
+
dependencyCount: transitiveDeps.size,
|
|
1675
|
+
dependencyList: Array.from(transitiveDeps.keys()),
|
|
1632
1676
|
circularDeps,
|
|
1633
1677
|
cohesionScore,
|
|
1634
1678
|
domains: Array.from(
|
|
@@ -1688,6 +1732,8 @@ async function analyzeContext(options) {
|
|
|
1688
1732
|
const { severity, issues, recommendations, potentialSavings } = analyzeIssues({
|
|
1689
1733
|
file: metric.file,
|
|
1690
1734
|
importDepth: metric.importDepth,
|
|
1735
|
+
tokenCost: metric.contextBudget,
|
|
1736
|
+
// For Python, we use the reported budget as self-cost for now
|
|
1691
1737
|
contextBudget: metric.contextBudget,
|
|
1692
1738
|
cohesionScore: metric.cohesion,
|
|
1693
1739
|
fragmentationScore: 0,
|
package/dist/index.mjs
CHANGED
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
getClassificationRecommendations,
|
|
13
13
|
getGeneralRecommendations,
|
|
14
14
|
getTransitiveDependencies
|
|
15
|
-
} from "./chunk-
|
|
16
|
-
import "./chunk-
|
|
15
|
+
} from "./chunk-2LEHY2GV.mjs";
|
|
16
|
+
import "./chunk-RQ5BQLT6.mjs";
|
|
17
17
|
import {
|
|
18
18
|
generateSummary
|
|
19
19
|
} from "./chunk-HD4Y3GYL.mjs";
|
|
@@ -230,7 +230,7 @@ var CONTEXT_ANALYZER_PROVIDER = {
|
|
|
230
230
|
id: ToolName2.ContextAnalyzer,
|
|
231
231
|
alias: ["context", "fragmentation", "budget"],
|
|
232
232
|
async analyze(options) {
|
|
233
|
-
const { analyzeContext: analyzeContext2 } = await import("./orchestrator-
|
|
233
|
+
const { analyzeContext: analyzeContext2 } = await import("./orchestrator-66ZVNOLR.mjs");
|
|
234
234
|
const { generateSummary: generateSummary2 } = await import("./summary-TZFB6ZFM.mjs");
|
|
235
235
|
const results = await analyzeContext2(options);
|
|
236
236
|
const summary = generateSummary2(results, options);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
calculateImportDepthFromEdges,
|
|
3
|
+
detectGraphCyclesFromFile
|
|
4
|
+
} from "./chunk-WLXLBWDU.mjs";
|
|
5
|
+
|
|
6
|
+
// src/analyzers/python-context.ts
|
|
7
|
+
import { getParser, estimateTokens } from "@aiready/core";
|
|
8
|
+
import { resolve, relative, dirname, join } from "path";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
async function analyzePythonContext(files, rootDir) {
|
|
11
|
+
const results = [];
|
|
12
|
+
const parser = await getParser("dummy.py");
|
|
13
|
+
if (!parser) {
|
|
14
|
+
console.warn("Python parser not available");
|
|
15
|
+
return results;
|
|
16
|
+
}
|
|
17
|
+
const pythonFiles = files.filter((f) => f.toLowerCase().endsWith(".py"));
|
|
18
|
+
void relative;
|
|
19
|
+
void join;
|
|
20
|
+
const dependencyGraph = await buildPythonDependencyGraph(
|
|
21
|
+
pythonFiles,
|
|
22
|
+
rootDir
|
|
23
|
+
);
|
|
24
|
+
for (const file of pythonFiles) {
|
|
25
|
+
try {
|
|
26
|
+
const code = await fs.promises.readFile(file, "utf-8");
|
|
27
|
+
const result = parser.parse(code, file);
|
|
28
|
+
const imports = result.imports.map((imp) => ({
|
|
29
|
+
source: imp.source,
|
|
30
|
+
specifiers: imp.specifiers,
|
|
31
|
+
isRelative: imp.source.startsWith("."),
|
|
32
|
+
resolvedPath: resolvePythonImport(file, imp.source, rootDir)
|
|
33
|
+
}));
|
|
34
|
+
const exports = result.exports.map((exp) => ({
|
|
35
|
+
name: exp.name,
|
|
36
|
+
type: exp.type
|
|
37
|
+
}));
|
|
38
|
+
const linesOfCode = code.split("\n").length;
|
|
39
|
+
const importDepth = calculateImportDepthFromEdges(
|
|
40
|
+
file,
|
|
41
|
+
dependencyGraph,
|
|
42
|
+
/* @__PURE__ */ new Set()
|
|
43
|
+
);
|
|
44
|
+
const contextBudget = estimateContextBudget(
|
|
45
|
+
code,
|
|
46
|
+
imports,
|
|
47
|
+
dependencyGraph
|
|
48
|
+
);
|
|
49
|
+
const cohesion = calculatePythonCohesion(exports, imports);
|
|
50
|
+
const circularDependencies = detectGraphCyclesFromFile(
|
|
51
|
+
file,
|
|
52
|
+
dependencyGraph
|
|
53
|
+
).map((cycle) => cycle.join(" -> "));
|
|
54
|
+
results.push({
|
|
55
|
+
file,
|
|
56
|
+
importDepth,
|
|
57
|
+
contextBudget,
|
|
58
|
+
cohesion,
|
|
59
|
+
imports,
|
|
60
|
+
exports,
|
|
61
|
+
metrics: {
|
|
62
|
+
linesOfCode,
|
|
63
|
+
importCount: imports.length,
|
|
64
|
+
exportCount: exports.length,
|
|
65
|
+
circularDependencies
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn(`Failed to analyze ${file}:`, error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
async function buildPythonDependencyGraph(files, rootDir) {
|
|
75
|
+
const graph = /* @__PURE__ */ new Map();
|
|
76
|
+
const parser = await getParser("dummy.py");
|
|
77
|
+
if (!parser) return graph;
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
try {
|
|
80
|
+
const code = await fs.promises.readFile(file, "utf-8");
|
|
81
|
+
const result = parser.parse(code, file);
|
|
82
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
83
|
+
for (const imp of result.imports) {
|
|
84
|
+
const resolved = resolvePythonImport(file, imp.source, rootDir);
|
|
85
|
+
if (resolved && files.includes(resolved)) {
|
|
86
|
+
dependencies.add(resolved);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
graph.set(file, dependencies);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
void error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return graph;
|
|
95
|
+
}
|
|
96
|
+
function resolvePythonImport(fromFile, importPath, rootDir) {
|
|
97
|
+
const dir = dirname(fromFile);
|
|
98
|
+
if (importPath.startsWith(".")) {
|
|
99
|
+
const parts = importPath.split(".");
|
|
100
|
+
let upCount = 0;
|
|
101
|
+
while (parts[0] === "") {
|
|
102
|
+
upCount++;
|
|
103
|
+
parts.shift();
|
|
104
|
+
}
|
|
105
|
+
let targetDir = dir;
|
|
106
|
+
for (let i = 0; i < upCount - 1; i++) {
|
|
107
|
+
targetDir = dirname(targetDir);
|
|
108
|
+
}
|
|
109
|
+
const modulePath = parts.join("/");
|
|
110
|
+
const possiblePaths = [
|
|
111
|
+
resolve(targetDir, `${modulePath}.py`),
|
|
112
|
+
resolve(targetDir, modulePath, "__init__.py")
|
|
113
|
+
];
|
|
114
|
+
for (const path of possiblePaths) {
|
|
115
|
+
if (fs.existsSync(path)) {
|
|
116
|
+
return path;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
const modulePath = importPath.replace(/\./g, "/");
|
|
121
|
+
const possiblePaths = [
|
|
122
|
+
resolve(rootDir, `${modulePath}.py`),
|
|
123
|
+
resolve(rootDir, modulePath, "__init__.py")
|
|
124
|
+
];
|
|
125
|
+
for (const path of possiblePaths) {
|
|
126
|
+
if (fs.existsSync(path)) {
|
|
127
|
+
return path;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
function estimateContextBudget(code, imports, dependencyGraph) {
|
|
134
|
+
void dependencyGraph;
|
|
135
|
+
let budget = estimateTokens(code);
|
|
136
|
+
const avgTokensPerDep = 500;
|
|
137
|
+
budget += imports.length * avgTokensPerDep;
|
|
138
|
+
return budget;
|
|
139
|
+
}
|
|
140
|
+
function calculatePythonCohesion(exports, imports) {
|
|
141
|
+
if (exports.length === 0) return 1;
|
|
142
|
+
const exportCount = exports.length;
|
|
143
|
+
const importCount = imports.length;
|
|
144
|
+
let cohesion = 1;
|
|
145
|
+
if (exportCount > 10) {
|
|
146
|
+
cohesion *= 0.6;
|
|
147
|
+
} else if (exportCount > 5) {
|
|
148
|
+
cohesion *= 0.8;
|
|
149
|
+
}
|
|
150
|
+
if (exportCount > 0) {
|
|
151
|
+
const ratio = importCount / exportCount;
|
|
152
|
+
if (ratio > 2) {
|
|
153
|
+
cohesion *= 1.1;
|
|
154
|
+
} else if (ratio < 0.5) {
|
|
155
|
+
cohesion *= 0.9;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return Math.min(1, Math.max(0, cohesion));
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
analyzePythonContext
|
|
162
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
calculateImportDepthFromEdges,
|
|
3
|
+
detectGraphCyclesFromFile
|
|
4
|
+
} from "./chunk-RQ5BQLT6.mjs";
|
|
5
|
+
|
|
6
|
+
// src/analyzers/python-context.ts
|
|
7
|
+
import { getParser, estimateTokens } from "@aiready/core";
|
|
8
|
+
import { resolve, relative, dirname, join } from "path";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
async function analyzePythonContext(files, rootDir) {
|
|
11
|
+
const results = [];
|
|
12
|
+
const parser = await getParser("dummy.py");
|
|
13
|
+
if (!parser) {
|
|
14
|
+
console.warn("Python parser not available");
|
|
15
|
+
return results;
|
|
16
|
+
}
|
|
17
|
+
const pythonFiles = files.filter((f) => f.toLowerCase().endsWith(".py"));
|
|
18
|
+
void relative;
|
|
19
|
+
void join;
|
|
20
|
+
const dependencyGraph = await buildPythonDependencyGraph(
|
|
21
|
+
pythonFiles,
|
|
22
|
+
rootDir
|
|
23
|
+
);
|
|
24
|
+
for (const file of pythonFiles) {
|
|
25
|
+
try {
|
|
26
|
+
const code = await fs.promises.readFile(file, "utf-8");
|
|
27
|
+
const result = parser.parse(code, file);
|
|
28
|
+
const imports = result.imports.map((imp) => ({
|
|
29
|
+
source: imp.source,
|
|
30
|
+
specifiers: imp.specifiers,
|
|
31
|
+
isRelative: imp.source.startsWith("."),
|
|
32
|
+
resolvedPath: resolvePythonImport(file, imp.source, rootDir)
|
|
33
|
+
}));
|
|
34
|
+
const exports = result.exports.map((exp) => ({
|
|
35
|
+
name: exp.name,
|
|
36
|
+
type: exp.type
|
|
37
|
+
}));
|
|
38
|
+
const linesOfCode = code.split("\n").length;
|
|
39
|
+
const importDepth = calculateImportDepthFromEdges(
|
|
40
|
+
file,
|
|
41
|
+
dependencyGraph,
|
|
42
|
+
/* @__PURE__ */ new Set()
|
|
43
|
+
);
|
|
44
|
+
const contextBudget = estimateContextBudget(
|
|
45
|
+
code,
|
|
46
|
+
imports,
|
|
47
|
+
dependencyGraph
|
|
48
|
+
);
|
|
49
|
+
const cohesion = calculatePythonCohesion(exports, imports);
|
|
50
|
+
const circularDependencies = detectGraphCyclesFromFile(
|
|
51
|
+
file,
|
|
52
|
+
dependencyGraph
|
|
53
|
+
).map((cycle) => cycle.join(" -> "));
|
|
54
|
+
results.push({
|
|
55
|
+
file,
|
|
56
|
+
importDepth,
|
|
57
|
+
contextBudget,
|
|
58
|
+
cohesion,
|
|
59
|
+
imports,
|
|
60
|
+
exports,
|
|
61
|
+
metrics: {
|
|
62
|
+
linesOfCode,
|
|
63
|
+
importCount: imports.length,
|
|
64
|
+
exportCount: exports.length,
|
|
65
|
+
circularDependencies
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn(`Failed to analyze ${file}:`, error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
async function buildPythonDependencyGraph(files, rootDir) {
|
|
75
|
+
const graph = /* @__PURE__ */ new Map();
|
|
76
|
+
const parser = await getParser("dummy.py");
|
|
77
|
+
if (!parser) return graph;
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
try {
|
|
80
|
+
const code = await fs.promises.readFile(file, "utf-8");
|
|
81
|
+
const result = parser.parse(code, file);
|
|
82
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
83
|
+
for (const imp of result.imports) {
|
|
84
|
+
const resolved = resolvePythonImport(file, imp.source, rootDir);
|
|
85
|
+
if (resolved && files.includes(resolved)) {
|
|
86
|
+
dependencies.add(resolved);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
graph.set(file, dependencies);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
void error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return graph;
|
|
95
|
+
}
|
|
96
|
+
function resolvePythonImport(fromFile, importPath, rootDir) {
|
|
97
|
+
const dir = dirname(fromFile);
|
|
98
|
+
if (importPath.startsWith(".")) {
|
|
99
|
+
const parts = importPath.split(".");
|
|
100
|
+
let upCount = 0;
|
|
101
|
+
while (parts[0] === "") {
|
|
102
|
+
upCount++;
|
|
103
|
+
parts.shift();
|
|
104
|
+
}
|
|
105
|
+
let targetDir = dir;
|
|
106
|
+
for (let i = 0; i < upCount - 1; i++) {
|
|
107
|
+
targetDir = dirname(targetDir);
|
|
108
|
+
}
|
|
109
|
+
const modulePath = parts.join("/");
|
|
110
|
+
const possiblePaths = [
|
|
111
|
+
resolve(targetDir, `${modulePath}.py`),
|
|
112
|
+
resolve(targetDir, modulePath, "__init__.py")
|
|
113
|
+
];
|
|
114
|
+
for (const path of possiblePaths) {
|
|
115
|
+
if (fs.existsSync(path)) {
|
|
116
|
+
return path;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
const modulePath = importPath.replace(/\./g, "/");
|
|
121
|
+
const possiblePaths = [
|
|
122
|
+
resolve(rootDir, `${modulePath}.py`),
|
|
123
|
+
resolve(rootDir, modulePath, "__init__.py")
|
|
124
|
+
];
|
|
125
|
+
for (const path of possiblePaths) {
|
|
126
|
+
if (fs.existsSync(path)) {
|
|
127
|
+
return path;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
function estimateContextBudget(code, imports, dependencyGraph) {
|
|
134
|
+
void dependencyGraph;
|
|
135
|
+
let budget = estimateTokens(code);
|
|
136
|
+
const avgTokensPerDep = 500;
|
|
137
|
+
budget += imports.length * avgTokensPerDep;
|
|
138
|
+
return budget;
|
|
139
|
+
}
|
|
140
|
+
function calculatePythonCohesion(exports, imports) {
|
|
141
|
+
if (exports.length === 0) return 1;
|
|
142
|
+
const exportCount = exports.length;
|
|
143
|
+
const importCount = imports.length;
|
|
144
|
+
let cohesion = 1;
|
|
145
|
+
if (exportCount > 10) {
|
|
146
|
+
cohesion *= 0.6;
|
|
147
|
+
} else if (exportCount > 5) {
|
|
148
|
+
cohesion *= 0.8;
|
|
149
|
+
}
|
|
150
|
+
if (exportCount > 0) {
|
|
151
|
+
const ratio = importCount / exportCount;
|
|
152
|
+
if (ratio > 2) {
|
|
153
|
+
cohesion *= 1.1;
|
|
154
|
+
} else if (ratio < 0.5) {
|
|
155
|
+
cohesion *= 0.9;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return Math.min(1, Math.max(0, cohesion));
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
analyzePythonContext
|
|
162
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/context-analyzer",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.16",
|
|
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",
|
|
@@ -39,17 +39,17 @@
|
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
|
42
|
-
"url": "https://github.com/
|
|
42
|
+
"url": "https://github.com/getaiready/aiready-context-analyzer.git"
|
|
43
43
|
},
|
|
44
|
-
"homepage": "https://github.com/
|
|
44
|
+
"homepage": "https://github.com/getaiready/aiready-context-analyzer",
|
|
45
45
|
"bugs": {
|
|
46
|
-
"url": "https://github.com/
|
|
46
|
+
"url": "https://github.com/getaiready/aiready-context-analyzer/issues"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"commander": "^14.0.0",
|
|
50
50
|
"chalk": "^5.3.0",
|
|
51
51
|
"prompts": "^2.4.2",
|
|
52
|
-
"@aiready/core": "0.24.
|
|
52
|
+
"@aiready/core": "0.24.19"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^24.0.0",
|
|
@@ -70,6 +70,8 @@
|
|
|
70
70
|
"clean": "rm -rf dist",
|
|
71
71
|
"release": "pnpm build && pnpm publish --no-git-checks",
|
|
72
72
|
"type-check": "tsc --noEmit",
|
|
73
|
-
"format-check": "prettier --check . --ignore-path ../../.prettierignore"
|
|
73
|
+
"format-check": "prettier --check . --ignore-path ../../.prettierignore",
|
|
74
|
+
"format": "prettier --write . --ignore-path ../../.prettierignore",
|
|
75
|
+
"lint:fix": "eslint . --fix"
|
|
74
76
|
}
|
|
75
77
|
}
|
|
@@ -93,9 +93,10 @@ describe('getTransitiveDependencies', () => {
|
|
|
93
93
|
const graph = await buildDependencyGraph(files);
|
|
94
94
|
const deps = getTransitiveDependencies('c.ts', graph);
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
expect(
|
|
98
|
-
expect(
|
|
96
|
+
const depKeys = Array.from(deps.keys());
|
|
97
|
+
expect(depKeys).toContain('b.ts');
|
|
98
|
+
expect(depKeys).toContain('a.ts');
|
|
99
|
+
expect(deps.size).toBe(2);
|
|
99
100
|
});
|
|
100
101
|
});
|
|
101
102
|
|
|
@@ -4,9 +4,11 @@ import { Severity } from '@aiready/core';
|
|
|
4
4
|
|
|
5
5
|
describe('analyzeIssues', () => {
|
|
6
6
|
const baseParams = {
|
|
7
|
-
file: '
|
|
7
|
+
file: 'test.ts',
|
|
8
8
|
importDepth: 2,
|
|
9
|
-
|
|
9
|
+
tokenCost: 1000,
|
|
10
|
+
contextBudget: 5000,
|
|
11
|
+
|
|
10
12
|
cohesionScore: 0.8,
|
|
11
13
|
fragmentationScore: 0.3,
|
|
12
14
|
maxDepth: 5,
|