@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.
@@ -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.34",
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.31"
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
- { file: 'b.ts', content: 'import { a } from "a.ts";\nexport const b = a;' },
42
- { file: 'c.ts', content: 'import { b } from "b.ts";\nexport const c = b;' },
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
- { file: 'a.ts', content: 'import { b } from "./b";\nexport const a = 1;' },
55
- { file: 'b.ts', content: 'import { a } from "./a";\nexport const b = 2;' },
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
- { file: 'b.ts', content: 'import { a } from "a.ts";\nexport const b = a;' },
71
- { file: 'c.ts', content: 'import { b } from "b.ts";\nexport const c = b;' },
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
- { file: 'b.ts', content: 'import { a } from "./a";\nexport const b = a;'.repeat(10) }, // ~60 tokens
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
- { file: 'a.ts', content: 'import { b } from "b.ts";\nexport const a = 1;' },
102
- { file: 'b.ts', content: 'import { a } from "a.ts";\nexport const b = 2;' },
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
- { file: 'b.ts', content: 'import { a } from "a.ts";\nexport const b = a;' },
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 = [{ name: 'foo', type: 'function' as const, inferredDomain: 'user' }];
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
- { name: 'parseConfig', type: 'function' as const, inferredDomain: 'config' },
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
- { name: 'setupTestDb', type: 'function' as const, inferredDomain: 'helper' },
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(exports, 'src/__tests__/helpers.test.ts');
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(exports, 'src/test-utils/mocks.ts');
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(exports, 'src/fixtures/data.ts');
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(exports, 'src/utils/helpers.ts');
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(exportsWithoutSharedImports);
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
- { name: 'testUserLogin', type: 'function', inferredDomain: 'user', imports: ['react'] },
120
- { name: 'testProductView', type: 'function', inferredDomain: 'product', imports: [] },
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');