@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.
- package/.turbo/turbo-build.log +11 -9
- package/.turbo/turbo-test.log +22 -12
- package/README.md +37 -0
- package/dist/chunk-JZ2SE4DB.mjs +1116 -0
- package/dist/chunk-RQCIJO5Z.mjs +1116 -0
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/cli.js +264 -17
- package/dist/cli.mjs +2 -1
- package/dist/index.js +259 -6
- package/dist/index.mjs +2 -1
- package/dist/python-context-UOPTQH44.mjs +192 -0
- package/package.json +3 -3
- package/src/analyzers/python-context.ts +323 -0
- package/src/index.ts +55 -5
|
@@ -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 =
|
|
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) => {
|