@aiready/context-analyzer 0.7.19 → 0.8.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.
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Python Context Analyzer
3
+ *
4
+ * Analyzes Python code for:
5
+ * - Import chain depth
6
+ * - Context budget (tokens needed)
7
+ * - Module cohesion
8
+ * - Import fragmentation
9
+ */
10
+
11
+ import { getParser, estimateTokens } from '@aiready/core';
12
+ import { resolve, relative, dirname, join } from 'path';
13
+
14
+ export interface PythonContextMetrics {
15
+ file: string;
16
+ importDepth: number;
17
+ contextBudget: number; // Total tokens needed (file + dependencies)
18
+ cohesion: number; // 0-1, higher is better
19
+ imports: PythonImportInfo[];
20
+ exports: PythonExportInfo[];
21
+ metrics: {
22
+ linesOfCode: number;
23
+ importCount: number;
24
+ exportCount: number;
25
+ circularDependencies: string[];
26
+ };
27
+ }
28
+
29
+ export interface PythonImportInfo {
30
+ source: string;
31
+ specifiers: string[];
32
+ isRelative: boolean;
33
+ resolvedPath?: string;
34
+ }
35
+
36
+ export interface PythonExportInfo {
37
+ name: string;
38
+ type: string;
39
+ }
40
+
41
+ /**
42
+ * Analyze Python files for context metrics
43
+ */
44
+ export async function analyzePythonContext(
45
+ files: string[],
46
+ rootDir: string
47
+ ): Promise<PythonContextMetrics[]> {
48
+ const results: PythonContextMetrics[] = [];
49
+ const parser = getParser('dummy.py');
50
+
51
+ if (!parser) {
52
+ console.warn('Python parser not available');
53
+ return results;
54
+ }
55
+
56
+ const pythonFiles = files.filter(f => f.toLowerCase().endsWith('.py'));
57
+
58
+ // Build dependency graph first
59
+ const dependencyGraph = await buildPythonDependencyGraph(pythonFiles, rootDir);
60
+
61
+ for (const file of pythonFiles) {
62
+ try {
63
+ const fs = await import('fs');
64
+ const code = await fs.promises.readFile(file, 'utf-8');
65
+ const result = parser.parse(code, file);
66
+
67
+ const imports: PythonImportInfo[] = result.imports.map(imp => ({
68
+ source: imp.source,
69
+ specifiers: imp.specifiers,
70
+ isRelative: imp.source.startsWith('.'),
71
+ resolvedPath: resolvePythonImport(file, imp.source, rootDir),
72
+ }));
73
+
74
+ const exports: PythonExportInfo[] = result.exports.map(exp => ({
75
+ name: exp.name,
76
+ type: exp.type,
77
+ }));
78
+
79
+ // Calculate metrics
80
+ const linesOfCode = code.split('\n').length;
81
+ const importDepth = await calculatePythonImportDepth(file, dependencyGraph, new Set());
82
+ const contextBudget = estimateContextBudget(code, imports, dependencyGraph);
83
+ const cohesion = calculatePythonCohesion(exports, imports);
84
+ const circularDependencies = detectCircularDependencies(file, dependencyGraph);
85
+
86
+ results.push({
87
+ file,
88
+ importDepth,
89
+ contextBudget,
90
+ cohesion,
91
+ imports,
92
+ exports,
93
+ metrics: {
94
+ linesOfCode,
95
+ importCount: imports.length,
96
+ exportCount: exports.length,
97
+ circularDependencies,
98
+ },
99
+ });
100
+ } catch (error) {
101
+ console.warn(`Failed to analyze ${file}:`, error);
102
+ }
103
+ }
104
+
105
+ return results;
106
+ }
107
+
108
+ /**
109
+ * Build dependency graph for Python files
110
+ */
111
+ async function buildPythonDependencyGraph(
112
+ files: string[],
113
+ rootDir: string
114
+ ): Promise<Map<string, Set<string>>> {
115
+ const graph = new Map<string, Set<string>>();
116
+ const parser = getParser('dummy.py');
117
+
118
+ if (!parser) return graph;
119
+
120
+ for (const file of files) {
121
+ try {
122
+ const fs = await import('fs');
123
+ const code = await fs.promises.readFile(file, 'utf-8');
124
+ const result = parser.parse(code, file);
125
+
126
+ const dependencies = new Set<string>();
127
+
128
+ for (const imp of result.imports) {
129
+ const resolved = resolvePythonImport(file, imp.source, rootDir);
130
+ if (resolved && files.includes(resolved)) {
131
+ dependencies.add(resolved);
132
+ }
133
+ }
134
+
135
+ graph.set(file, dependencies);
136
+ } catch (error) {
137
+ // Skip files with errors
138
+ }
139
+ }
140
+
141
+ return graph;
142
+ }
143
+
144
+ /**
145
+ * Resolve Python import to file path
146
+ */
147
+ function resolvePythonImport(fromFile: string, importPath: string, rootDir: string): string | undefined {
148
+ const dir = dirname(fromFile);
149
+
150
+ // Handle relative imports
151
+ if (importPath.startsWith('.')) {
152
+ const parts = importPath.split('.');
153
+ let upCount = 0;
154
+ while (parts[0] === '') {
155
+ upCount++;
156
+ parts.shift();
157
+ }
158
+
159
+ let targetDir = dir;
160
+ for (let i = 0; i < upCount - 1; i++) {
161
+ targetDir = dirname(targetDir);
162
+ }
163
+
164
+ const modulePath = parts.join('/');
165
+ const possiblePaths = [
166
+ resolve(targetDir, `${modulePath}.py`),
167
+ resolve(targetDir, modulePath, '__init__.py'),
168
+ ];
169
+
170
+ const fs = require('fs');
171
+ for (const path of possiblePaths) {
172
+ if (fs.existsSync(path)) {
173
+ return path;
174
+ }
175
+ }
176
+ } else {
177
+ // Handle absolute imports (from project root)
178
+ const modulePath = importPath.replace(/\./g, '/');
179
+ const possiblePaths = [
180
+ resolve(rootDir, `${modulePath}.py`),
181
+ resolve(rootDir, modulePath, '__init__.py'),
182
+ ];
183
+
184
+ const fs = require('fs');
185
+ for (const path of possiblePaths) {
186
+ if (fs.existsSync(path)) {
187
+ return path;
188
+ }
189
+ }
190
+ }
191
+
192
+ return undefined;
193
+ }
194
+
195
+ /**
196
+ * Calculate import depth for a Python file
197
+ */
198
+ async function calculatePythonImportDepth(
199
+ file: string,
200
+ dependencyGraph: Map<string, Set<string>>,
201
+ visited: Set<string>,
202
+ depth: number = 0
203
+ ): Promise<number> {
204
+ if (visited.has(file)) {
205
+ return depth; // Circular dependency, stop here
206
+ }
207
+
208
+ visited.add(file);
209
+ const dependencies = dependencyGraph.get(file) || new Set();
210
+
211
+ if (dependencies.size === 0) {
212
+ return depth;
213
+ }
214
+
215
+ let maxDepth = depth;
216
+ for (const dep of dependencies) {
217
+ const depDepth = await calculatePythonImportDepth(
218
+ dep,
219
+ dependencyGraph,
220
+ new Set(visited),
221
+ depth + 1
222
+ );
223
+ maxDepth = Math.max(maxDepth, depDepth);
224
+ }
225
+
226
+ return maxDepth;
227
+ }
228
+
229
+ /**
230
+ * Estimate context budget (tokens needed for file + direct deps)
231
+ */
232
+ function estimateContextBudget(
233
+ code: string,
234
+ imports: PythonImportInfo[],
235
+ dependencyGraph: Map<string, Set<string>>
236
+ ): number {
237
+ // File tokens
238
+ let budget = estimateTokens(code);
239
+
240
+ // Add tokens for direct dependencies (simplified)
241
+ // In a full implementation, we'd load each dependency file
242
+ const avgTokensPerDep = 500; // Conservative estimate
243
+ budget += imports.length * avgTokensPerDep;
244
+
245
+ return budget;
246
+ }
247
+
248
+ /**
249
+ * Calculate cohesion for a Python module
250
+ *
251
+ * Cohesion = How related are the exports to each other?
252
+ * Higher cohesion = better (single responsibility)
253
+ */
254
+ function calculatePythonCohesion(
255
+ exports: PythonExportInfo[],
256
+ imports: PythonImportInfo[]
257
+ ): number {
258
+ if (exports.length === 0) return 1;
259
+
260
+ // Simple heuristic: files with many exports but few imports are less cohesive
261
+ const exportCount = exports.length;
262
+ const importCount = imports.length;
263
+
264
+ // Ideal: 1-5 exports per module
265
+ let cohesion = 1;
266
+
267
+ if (exportCount > 10) {
268
+ cohesion *= 0.6; // Too many exports = God module
269
+ } else if (exportCount > 5) {
270
+ cohesion *= 0.8;
271
+ }
272
+
273
+ // High import-to-export ratio suggests focused module
274
+ if (exportCount > 0) {
275
+ const ratio = importCount / exportCount;
276
+ if (ratio > 2) {
277
+ cohesion *= 1.1; // Good: imports more than it exports
278
+ } else if (ratio < 0.5) {
279
+ cohesion *= 0.9; // Bad: exports more than it imports
280
+ }
281
+ }
282
+
283
+ return Math.min(1, Math.max(0, cohesion));
284
+ }
285
+
286
+ /**
287
+ * Detect circular dependencies
288
+ */
289
+ function detectCircularDependencies(
290
+ file: string,
291
+ dependencyGraph: Map<string, Set<string>>
292
+ ): string[] {
293
+ const circular: string[] = [];
294
+ const visited = new Set<string>();
295
+ const recursionStack = new Set<string>();
296
+
297
+ function dfs(current: string, path: string[]): void {
298
+ if (recursionStack.has(current)) {
299
+ // Found a cycle
300
+ const cycleStart = path.indexOf(current);
301
+ const cycle = path.slice(cycleStart).concat([current]);
302
+ circular.push(cycle.join(' → '));
303
+ return;
304
+ }
305
+
306
+ if (visited.has(current)) {
307
+ return;
308
+ }
309
+
310
+ visited.add(current);
311
+ recursionStack.add(current);
312
+
313
+ const dependencies = dependencyGraph.get(current) || new Set();
314
+ for (const dep of dependencies) {
315
+ dfs(dep, [...path, current]);
316
+ }
317
+
318
+ recursionStack.delete(current);
319
+ }
320
+
321
+ dfs(file, []);
322
+ return [...new Set(circular)]; // Deduplicate
323
+ }
package/src/index.ts CHANGED
@@ -132,7 +132,7 @@ export async function analyzeContext(
132
132
  ...scanOptions
133
133
  } = options;
134
134
 
135
- // Scan files
135
+ // Scan files (supports .ts, .js, .tsx, .jsx, .py)
136
136
  // Note: scanFiles now automatically merges user excludes with DEFAULT_EXCLUDE
137
137
  const files = await scanFiles({
138
138
  ...scanOptions,
@@ -144,6 +144,10 @@ export async function analyzeContext(
144
144
  : scanOptions.exclude,
145
145
  });
146
146
 
147
+ // Separate files by language
148
+ const pythonFiles = files.filter(f => f.toLowerCase().endsWith('.py'));
149
+ const tsJsFiles = files.filter(f => !f.toLowerCase().endsWith('.py'));
150
+
147
151
  // Read all file contents
148
152
  const fileContents = await Promise.all(
149
153
  files.map(async (file) => ({
@@ -152,10 +156,53 @@ export async function analyzeContext(
152
156
  }))
153
157
  );
154
158
 
155
- // Build dependency graph
156
- const graph = buildDependencyGraph(fileContents);
159
+ // Build dependency graph (TS/JS)
160
+ const graph = buildDependencyGraph(fileContents.filter(f => !f.file.toLowerCase().endsWith('.py')));
161
+
162
+ // Analyze Python files separately (if any)
163
+ let pythonResults: ContextAnalysisResult[] = [];
164
+ if (pythonFiles.length > 0) {
165
+ const { analyzePythonContext } = await import('./analyzers/python-context');
166
+ const pythonMetrics = await analyzePythonContext(pythonFiles, scanOptions.rootDir || options.rootDir || '.');
167
+
168
+ // Convert Python metrics to ContextAnalysisResult format
169
+ pythonResults = pythonMetrics.map(metric => {
170
+ const { severity, issues, recommendations, potentialSavings } = analyzeIssues({
171
+ file: metric.file,
172
+ importDepth: metric.importDepth,
173
+ contextBudget: metric.contextBudget,
174
+ cohesionScore: metric.cohesion,
175
+ fragmentationScore: 0, // Python analyzer doesn't calculate fragmentation yet
176
+ maxDepth,
177
+ maxContextBudget,
178
+ minCohesion,
179
+ maxFragmentation,
180
+ circularDeps: metric.metrics.circularDependencies.map(cycle => cycle.split(' → ')),
181
+ });
182
+
183
+ return {
184
+ file: metric.file,
185
+ tokenCost: Math.floor(metric.contextBudget / (1 + metric.imports.length || 1)), // Estimate
186
+ linesOfCode: metric.metrics.linesOfCode,
187
+ importDepth: metric.importDepth,
188
+ dependencyCount: metric.imports.length,
189
+ dependencyList: metric.imports.map(imp => imp.resolvedPath || imp.source),
190
+ circularDeps: metric.metrics.circularDependencies.map(cycle => cycle.split(' → ')),
191
+ cohesionScore: metric.cohesion,
192
+ domains: ['python'], // Generic for now
193
+ exportCount: metric.exports.length,
194
+ contextBudget: metric.contextBudget,
195
+ fragmentationScore: 0,
196
+ relatedFiles: [],
197
+ severity,
198
+ issues,
199
+ recommendations,
200
+ potentialSavings,
201
+ };
202
+ });
203
+ }
157
204
 
158
- // Detect circular dependencies
205
+ // Detect circular dependencies (TS/JS)
159
206
  const circularDeps = detectCircularDependencies(graph);
160
207
 
161
208
  // Detect module clusters for fragmentation analysis
@@ -245,9 +292,12 @@ export async function analyzeContext(
245
292
  });
246
293
  }
247
294
 
295
+ // Merge Python and TS/JS results
296
+ const allResults = [...results, ...pythonResults];
297
+
248
298
  // Filter to only files with actual issues (not just info)
249
299
  // This reduces output noise and focuses on actionable problems
250
- const issuesOnly = results.filter(r => r.severity !== 'info');
300
+ const issuesOnly = allResults.filter(r => r.severity !== 'info');
251
301
 
252
302
  // Sort by severity and context budget
253
303
  const sorted = issuesOnly.sort((a, b) => {