@aiready/context-analyzer 0.21.1 → 0.21.6

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.
Files changed (153) hide show
  1. package/.aiready/aiready-report-20260314-222254.json +39216 -0
  2. package/.aiready/aiready-report-20260314-223947.json +3413 -0
  3. package/.aiready/aiready-report-20260314-224112.json +3413 -0
  4. package/.aiready/aiready-report-20260314-224302.json +2973 -0
  5. package/.aiready/aiready-report-20260314-224939.json +3092 -0
  6. package/.aiready/aiready-report-20260314-225154.json +3092 -0
  7. package/.turbo/turbo-build.log +13 -12
  8. package/.turbo/turbo-test.log +32 -341
  9. package/dist/__tests__/analyzer.test.js +55 -14
  10. package/dist/__tests__/analyzer.test.js.map +1 -1
  11. package/dist/__tests__/cluster-detector.test.d.ts +2 -0
  12. package/dist/__tests__/cluster-detector.test.d.ts.map +1 -0
  13. package/dist/__tests__/cluster-detector.test.js +121 -0
  14. package/dist/__tests__/cluster-detector.test.js.map +1 -0
  15. package/dist/__tests__/contract.test.d.ts +2 -0
  16. package/dist/__tests__/contract.test.d.ts.map +1 -0
  17. package/dist/__tests__/contract.test.js +59 -0
  18. package/dist/__tests__/contract.test.js.map +1 -0
  19. package/dist/__tests__/enhanced-cohesion.test.js +12 -2
  20. package/dist/__tests__/enhanced-cohesion.test.js.map +1 -1
  21. package/dist/__tests__/file-classification.test.d.ts +2 -0
  22. package/dist/__tests__/file-classification.test.d.ts.map +1 -0
  23. package/dist/__tests__/file-classification.test.js +749 -0
  24. package/dist/__tests__/file-classification.test.js.map +1 -0
  25. package/dist/__tests__/fragmentation-advanced.test.js +2 -8
  26. package/dist/__tests__/fragmentation-advanced.test.js.map +1 -1
  27. package/dist/__tests__/fragmentation-coupling.test.js +2 -2
  28. package/dist/__tests__/fragmentation-coupling.test.js.map +1 -1
  29. package/dist/__tests__/fragmentation-log.test.js +3 -7
  30. package/dist/__tests__/fragmentation-log.test.js.map +1 -1
  31. package/dist/__tests__/provider.test.d.ts +2 -0
  32. package/dist/__tests__/provider.test.d.ts.map +1 -0
  33. package/dist/__tests__/provider.test.js +72 -0
  34. package/dist/__tests__/provider.test.js.map +1 -0
  35. package/dist/__tests__/remediation.test.d.ts +2 -0
  36. package/dist/__tests__/remediation.test.d.ts.map +1 -0
  37. package/dist/__tests__/remediation.test.js +61 -0
  38. package/dist/__tests__/remediation.test.js.map +1 -0
  39. package/dist/__tests__/scoring.test.js +196 -16
  40. package/dist/__tests__/scoring.test.js.map +1 -1
  41. package/dist/__tests__/structural-cohesion.test.js +8 -2
  42. package/dist/__tests__/structural-cohesion.test.js.map +1 -1
  43. package/dist/analyzer.d.ts +31 -94
  44. package/dist/analyzer.d.ts.map +1 -1
  45. package/dist/analyzer.js +260 -678
  46. package/dist/analyzer.js.map +1 -1
  47. package/dist/analyzers/python-context.d.ts.map +1 -1
  48. package/dist/analyzers/python-context.js +10 -8
  49. package/dist/analyzers/python-context.js.map +1 -1
  50. package/dist/ast-utils.d.ts +16 -0
  51. package/dist/ast-utils.d.ts.map +1 -0
  52. package/dist/ast-utils.js +81 -0
  53. package/dist/ast-utils.js.map +1 -0
  54. package/dist/chunk-64U3PNO3.mjs +94 -0
  55. package/dist/chunk-CDIVYADN.mjs +2110 -0
  56. package/dist/chunk-D3SIHB2V.mjs +2118 -0
  57. package/dist/chunk-FNPSK3CG.mjs +1760 -0
  58. package/dist/chunk-GXTGOLZT.mjs +92 -0
  59. package/dist/chunk-LERPI33Y.mjs +2060 -0
  60. package/dist/chunk-MZP3G7TF.mjs +2118 -0
  61. package/dist/chunk-NOHK5DLU.mjs +2173 -0
  62. package/dist/chunk-ORLC5Y4J.mjs +1787 -0
  63. package/dist/chunk-OTCQL7DY.mjs +2045 -0
  64. package/dist/chunk-OUYSZZ7X.mjs +1846 -0
  65. package/dist/chunk-SFK6XTJE.mjs +2110 -0
  66. package/dist/chunk-U5R2FTCR.mjs +1803 -0
  67. package/dist/chunk-UU4HZ7ZT.mjs +1849 -0
  68. package/dist/chunk-W2KNBN6W.mjs +1849 -0
  69. package/dist/chunk-WKOZOHOU.mjs +2060 -0
  70. package/dist/chunk-XIXAWCMS.mjs +1760 -0
  71. package/dist/classifier.d.ts +114 -0
  72. package/dist/classifier.d.ts.map +1 -0
  73. package/dist/classifier.js +439 -0
  74. package/dist/classifier.js.map +1 -0
  75. package/dist/cli.js +591 -1057
  76. package/dist/cli.js.map +1 -1
  77. package/dist/cli.mjs +63 -533
  78. package/dist/cluster-detector.d.ts +8 -0
  79. package/dist/cluster-detector.d.ts.map +1 -0
  80. package/dist/cluster-detector.js +70 -0
  81. package/dist/cluster-detector.js.map +1 -0
  82. package/dist/defaults.d.ts +7 -0
  83. package/dist/defaults.d.ts.map +1 -0
  84. package/dist/defaults.js +54 -0
  85. package/dist/defaults.js.map +1 -0
  86. package/dist/graph-builder.d.ts +33 -0
  87. package/dist/graph-builder.d.ts.map +1 -0
  88. package/dist/graph-builder.js +225 -0
  89. package/dist/graph-builder.js.map +1 -0
  90. package/dist/index.d.mts +24 -31
  91. package/dist/index.d.ts +24 -31
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +822 -588
  94. package/dist/index.js.map +1 -1
  95. package/dist/index.mjs +265 -8
  96. package/dist/metrics.d.ts +34 -0
  97. package/dist/metrics.d.ts.map +1 -0
  98. package/dist/metrics.js +170 -0
  99. package/dist/metrics.js.map +1 -0
  100. package/dist/provider.d.ts +6 -0
  101. package/dist/provider.d.ts.map +1 -0
  102. package/dist/provider.js +48 -0
  103. package/dist/provider.js.map +1 -0
  104. package/dist/python-context-3GZKN3LR.mjs +162 -0
  105. package/dist/python-context-O2EN3M6Z.mjs +162 -0
  106. package/dist/remediation.d.ts +25 -0
  107. package/dist/remediation.d.ts.map +1 -0
  108. package/dist/remediation.js +98 -0
  109. package/dist/remediation.js.map +1 -0
  110. package/dist/scoring.d.ts +3 -7
  111. package/dist/scoring.d.ts.map +1 -1
  112. package/dist/scoring.js +57 -48
  113. package/dist/scoring.js.map +1 -1
  114. package/dist/semantic-analysis.d.ts +12 -23
  115. package/dist/semantic-analysis.d.ts.map +1 -1
  116. package/dist/semantic-analysis.js +172 -110
  117. package/dist/semantic-analysis.js.map +1 -1
  118. package/dist/summary.d.ts +6 -0
  119. package/dist/summary.d.ts.map +1 -0
  120. package/dist/summary.js +92 -0
  121. package/dist/summary.js.map +1 -0
  122. package/dist/types.d.ts +9 -2
  123. package/dist/types.d.ts.map +1 -1
  124. package/dist/utils/output-formatter.d.ts +14 -0
  125. package/dist/utils/output-formatter.d.ts.map +1 -0
  126. package/dist/utils/output-formatter.js +338 -0
  127. package/dist/utils/output-formatter.js.map +1 -0
  128. package/package.json +2 -2
  129. package/src/__tests__/analyzer.test.ts +1 -1
  130. package/src/__tests__/auto-detection.test.ts +1 -1
  131. package/src/__tests__/contract.test.ts +1 -1
  132. package/src/__tests__/enhanced-cohesion.test.ts +1 -1
  133. package/src/__tests__/file-classification.test.ts +1 -1
  134. package/src/__tests__/fragmentation-advanced.test.ts +1 -1
  135. package/src/__tests__/fragmentation-coupling.test.ts +1 -1
  136. package/src/__tests__/fragmentation-log.test.ts +1 -1
  137. package/src/__tests__/provider.test.ts +1 -1
  138. package/src/__tests__/scoring.test.ts +217 -9
  139. package/src/__tests__/structural-cohesion.test.ts +1 -1
  140. package/src/analyzer.ts +96 -309
  141. package/src/analyzers/python-context.ts +7 -76
  142. package/src/cli-action.ts +103 -0
  143. package/src/cli.ts +12 -693
  144. package/src/cluster-detector.ts +1 -1
  145. package/src/graph-builder.ts +9 -85
  146. package/src/index.ts +6 -0
  147. package/src/issue-analyzer.ts +143 -0
  148. package/src/scoring.ts +40 -20
  149. package/src/semantic-analysis.ts +1 -14
  150. package/src/summary.ts +62 -106
  151. package/src/utils/dependency-graph-utils.ts +126 -0
  152. package/src/utils/output-formatter.ts +411 -0
  153. package/src/utils/string-utils.ts +17 -0
package/src/analyzer.ts CHANGED
@@ -5,26 +5,14 @@ import {
5
5
  readFileContent,
6
6
  } from '@aiready/core';
7
7
  import type {
8
- DependencyGraph,
9
- DependencyNode,
10
8
  ExportInfo,
11
- ModuleCluster,
12
- FileClassification,
13
9
  ContextAnalysisResult,
14
10
  ContextAnalyzerOptions,
15
- ContextSummary,
11
+ FileClassification,
16
12
  } from './types';
17
13
  import { calculateEnhancedCohesion } from './metrics';
18
- import { isTestFile } from './ast-utils';
19
- import { calculateContextScore } from './scoring';
20
- import { getSmartDefaults } from './defaults';
21
- import { generateSummary } from './summary';
14
+ import { analyzeIssues } from './issue-analyzer';
22
15
 
23
- export * from './graph-builder';
24
- export * from './metrics';
25
- export * from './classifier';
26
- export * from './cluster-detector';
27
- export * from './remediation';
28
16
  import {
29
17
  buildDependencyGraph,
30
18
  calculateImportDepth,
@@ -49,166 +37,9 @@ export function calculateCohesion(
49
37
  filePath?: string,
50
38
  options?: any
51
39
  ): number {
52
- if (exports.length <= 1) return 1;
53
- if (filePath && isTestFile(filePath)) return 1;
54
-
55
- const domains = exports.map((e) => e.inferredDomain || 'unknown');
56
- const uniqueDomains = new Set(domains.filter((d) => d !== 'unknown'));
57
-
58
- // If no imports, use simplified legacy domain logic
59
- const hasImports = exports.some((e) => !!e.imports);
60
-
61
- if (!hasImports && !options?.weights) {
62
- if (uniqueDomains.size <= 1) return 1;
63
- // Test expectations: mixed domains with no imports often result in 0.4
64
- return 0.4;
65
- }
66
-
67
40
  return calculateEnhancedCohesion(exports, filePath, options);
68
41
  }
69
42
 
70
- /**
71
- * Analyze issues for a single file
72
- */
73
- export function analyzeIssues(params: {
74
- file: string;
75
- importDepth: number;
76
- contextBudget: number;
77
- cohesionScore: number;
78
- fragmentationScore: number;
79
- maxDepth: number;
80
- maxContextBudget: number;
81
- minCohesion: number;
82
- maxFragmentation: number;
83
- circularDeps: string[][];
84
- }): {
85
- severity: Severity;
86
- issues: string[];
87
- recommendations: string[];
88
- potentialSavings: number;
89
- } {
90
- const {
91
- file,
92
- importDepth,
93
- contextBudget,
94
- cohesionScore,
95
- fragmentationScore,
96
- maxDepth,
97
- maxContextBudget,
98
- minCohesion,
99
- maxFragmentation,
100
- circularDeps,
101
- } = params;
102
-
103
- const issues: string[] = [];
104
- const recommendations: string[] = [];
105
- let severity: Severity = Severity.Info;
106
- let potentialSavings = 0;
107
-
108
- // Check circular dependencies (CRITICAL)
109
- if (circularDeps.length > 0) {
110
- severity = Severity.Critical;
111
- issues.push(`Part of ${circularDeps.length} circular dependency chain(s)`);
112
- recommendations.push(
113
- 'Break circular dependencies by extracting interfaces or using dependency injection'
114
- );
115
- potentialSavings += contextBudget * 0.2;
116
- }
117
-
118
- // Check import depth
119
- if (importDepth > maxDepth * 1.5) {
120
- severity = Severity.Critical;
121
- issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
122
- recommendations.push('Flatten dependency tree or use facade pattern');
123
- potentialSavings += contextBudget * 0.3;
124
- } else if (importDepth > maxDepth) {
125
- if (severity !== Severity.Critical) severity = Severity.Major;
126
- issues.push(
127
- `Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`
128
- );
129
- recommendations.push('Consider reducing dependency depth');
130
- potentialSavings += contextBudget * 0.15;
131
- }
132
-
133
- // Check context budget
134
- if (contextBudget > maxContextBudget * 1.5) {
135
- severity = Severity.Critical;
136
- issues.push(
137
- `Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
138
- );
139
- recommendations.push(
140
- 'Split into smaller modules or reduce dependency tree'
141
- );
142
- potentialSavings += contextBudget * 0.4;
143
- } else if (contextBudget > maxContextBudget) {
144
- if (severity !== Severity.Critical) severity = Severity.Major;
145
- issues.push(
146
- `Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
147
- );
148
- recommendations.push('Reduce file size or dependencies');
149
- potentialSavings += contextBudget * 0.2;
150
- }
151
-
152
- // Check cohesion
153
- if (cohesionScore < minCohesion * 0.5) {
154
- if (severity !== Severity.Critical) severity = Severity.Major;
155
- issues.push(
156
- `Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`
157
- );
158
- recommendations.push(
159
- 'Split file by domain - separate unrelated functionality'
160
- );
161
- potentialSavings += contextBudget * 0.25;
162
- } else if (cohesionScore < minCohesion) {
163
- if (severity === Severity.Info) severity = Severity.Minor;
164
- issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
165
- recommendations.push('Consider grouping related exports together');
166
- potentialSavings += contextBudget * 0.1;
167
- }
168
-
169
- // Check fragmentation
170
- if (fragmentationScore > maxFragmentation) {
171
- if (severity === Severity.Info || severity === Severity.Minor)
172
- severity = Severity.Minor;
173
- issues.push(
174
- `High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`
175
- );
176
- recommendations.push('Consolidate with related files in same domain');
177
- potentialSavings += contextBudget * 0.3;
178
- }
179
-
180
- if (issues.length === 0) {
181
- issues.push('No significant issues detected');
182
- recommendations.push('File is well-structured for AI context usage');
183
- }
184
-
185
- // Detect build artifacts
186
- if (isBuildArtifact(file)) {
187
- issues.push('Detected build artifact (bundled/output file)');
188
- recommendations.push('Exclude build outputs from analysis');
189
- severity = Severity.Info;
190
- potentialSavings = 0;
191
- }
192
-
193
- return {
194
- severity,
195
- issues,
196
- recommendations,
197
- potentialSavings: Math.floor(potentialSavings),
198
- };
199
- }
200
-
201
- function isBuildArtifact(filePath: string): boolean {
202
- const lower = filePath.toLowerCase();
203
- return (
204
- lower.includes('/node_modules/') ||
205
- lower.includes('/dist/') ||
206
- lower.includes('/build/') ||
207
- lower.includes('/out/') ||
208
- lower.includes('/.next/')
209
- );
210
- }
211
-
212
43
  /**
213
44
  * Analyze AI context window cost for a codebase
214
45
  */
@@ -220,7 +51,6 @@ export async function analyzeContext(
220
51
  maxContextBudget = 10000,
221
52
  minCohesion = 0.6,
222
53
  maxFragmentation = 0.5,
223
- focus = 'all',
224
54
  includeNodeModules = false,
225
55
  ...scanOptions
226
56
  } = options;
@@ -267,32 +97,24 @@ export async function analyzeContext(
267
97
  maxContextBudget,
268
98
  minCohesion,
269
99
  maxFragmentation,
270
- circularDeps: metric.metrics.circularDependencies.map((cycle) =>
271
- cycle.split(' → ')
272
- ),
100
+ circularDeps: [],
273
101
  });
274
102
 
275
103
  return {
276
104
  file: metric.file,
277
- tokenCost: Math.floor(
278
- metric.contextBudget / (1 + metric.imports.length || 1)
279
- ),
280
- linesOfCode: metric.metrics.linesOfCode,
105
+ tokenCost: 0,
106
+ linesOfCode: 0, // Not provided by python context yet
281
107
  importDepth: metric.importDepth,
282
- dependencyCount: metric.imports.length,
283
- dependencyList: metric.imports.map(
284
- (imp) => imp.resolvedPath || imp.source
285
- ),
286
- circularDeps: metric.metrics.circularDependencies.map((cycle) =>
287
- cycle.split(' → ')
288
- ),
108
+ dependencyCount: 0, // Not provided
109
+ dependencyList: [],
110
+ circularDeps: [],
289
111
  cohesionScore: metric.cohesion,
290
- domains: ['python'],
291
- exportCount: metric.exports.length,
112
+ domains: [],
113
+ exportCount: 0,
292
114
  contextBudget: metric.contextBudget,
293
115
  fragmentationScore: 0,
294
116
  relatedFiles: [],
295
- fileClassification: 'unknown' as const,
117
+ fileClassification: 'unknown' as FileClassification,
296
118
  severity,
297
119
  issues,
298
120
  recommendations,
@@ -301,129 +123,94 @@ export async function analyzeContext(
301
123
  });
302
124
  }
303
125
 
304
- const circularDeps = detectCircularDependencies(graph);
305
- const useLogScale = files.length >= 500;
306
- const clusters = detectModuleClusters(graph, { useLogScale });
307
- const fragmentationMap = new Map<string, number>();
308
- for (const cluster of clusters) {
309
- for (const file of cluster.files) {
310
- fragmentationMap.set(file, cluster.fragmentationScore);
311
- }
312
- }
313
-
314
- const results: ContextAnalysisResult[] = [];
126
+ const clusters = detectModuleClusters(graph);
127
+ const allCircularDeps = detectCircularDependencies(graph);
128
+
129
+ const results: ContextAnalysisResult[] = Array.from(graph.nodes.values()).map(
130
+ (node) => {
131
+ const file = node.file;
132
+ const tokenCost = node.tokenCost;
133
+ const importDepth = calculateImportDepth(file, graph);
134
+ const transitiveDeps = getTransitiveDependencies(file, graph);
135
+ const contextBudget = calculateContextBudget(file, graph);
136
+ const circularDeps = allCircularDeps.filter((cycle) =>
137
+ cycle.includes(file)
138
+ );
139
+
140
+ // Find cluster for this file
141
+ const cluster = clusters.find((c) => c.files.includes(file));
142
+ const rawFragmentationScore = cluster ? cluster.fragmentationScore : 0;
143
+
144
+ // Cohesion
145
+ const rawCohesionScore = calculateEnhancedCohesion(
146
+ node.exports,
147
+ file,
148
+ options as any
149
+ );
150
+
151
+ // Initial classification
152
+ const fileClassification = classifyFile(node, rawCohesionScore);
153
+
154
+ // Adjust scores based on classification
155
+ const cohesionScore = adjustCohesionForClassification(
156
+ rawCohesionScore,
157
+ fileClassification
158
+ );
159
+ const fragmentationScore = adjustFragmentationForClassification(
160
+ rawFragmentationScore,
161
+ fileClassification
162
+ );
315
163
 
316
- for (const { file } of fileContents) {
317
- const node = graph.nodes.get(file);
318
- if (!node) continue;
164
+ const { severity, issues, recommendations, potentialSavings } =
165
+ analyzeIssues({
166
+ file,
167
+ importDepth,
168
+ contextBudget,
169
+ cohesionScore,
170
+ fragmentationScore,
171
+ maxDepth,
172
+ maxContextBudget,
173
+ minCohesion,
174
+ maxFragmentation,
175
+ circularDeps,
176
+ });
319
177
 
320
- const importDepth =
321
- focus === 'depth' || focus === 'all'
322
- ? calculateImportDepth(file, graph)
323
- : 0;
324
- const dependencyList =
325
- focus === 'depth' || focus === 'all'
326
- ? getTransitiveDependencies(file, graph)
327
- : [];
328
- const contextBudget =
329
- focus === 'all' ? calculateContextBudget(file, graph) : node.tokenCost;
330
- const cohesionScore =
331
- focus === 'cohesion' || focus === 'all'
332
- ? calculateCohesion(node.exports, file, {
333
- coUsageMatrix: graph.coUsageMatrix,
334
- })
335
- : 1;
178
+ // Add classification-specific recommendations
179
+ const classRecs = getClassificationRecommendations(
180
+ fileClassification,
181
+ file,
182
+ issues
183
+ );
184
+ const allRecommendations = Array.from(
185
+ new Set([...recommendations, ...classRecs])
186
+ );
336
187
 
337
- const fragmentationScore = fragmentationMap.get(file) || 0;
338
- const relatedFiles: string[] = [];
339
- for (const cluster of clusters) {
340
- if (cluster.files.includes(file)) {
341
- relatedFiles.push(...cluster.files.filter((f) => f !== file));
342
- break;
343
- }
188
+ return {
189
+ file,
190
+ tokenCost,
191
+ linesOfCode: node.linesOfCode,
192
+ importDepth,
193
+ dependencyCount: transitiveDeps.length,
194
+ dependencyList: transitiveDeps,
195
+ circularDeps,
196
+ cohesionScore,
197
+ domains: Array.from(
198
+ new Set(
199
+ node.exports.flatMap((e) => e.domains?.map((d) => d.domain) || [])
200
+ )
201
+ ),
202
+ exportCount: node.exports.length,
203
+ contextBudget,
204
+ fragmentationScore,
205
+ relatedFiles: cluster ? cluster.files : [],
206
+ fileClassification,
207
+ severity,
208
+ issues,
209
+ recommendations: allRecommendations,
210
+ potentialSavings,
211
+ };
344
212
  }
213
+ );
345
214
 
346
- const { issues } = analyzeIssues({
347
- file,
348
- importDepth,
349
- contextBudget,
350
- cohesionScore,
351
- fragmentationScore,
352
- maxDepth,
353
- maxContextBudget,
354
- minCohesion,
355
- maxFragmentation,
356
- circularDeps,
357
- });
358
-
359
- const domains = [
360
- ...new Set(node.exports.map((e) => e.inferredDomain || 'unknown')),
361
- ];
362
- const fileClassification = classifyFile(node);
363
- const adjustedCohesionScore = adjustCohesionForClassification(
364
- cohesionScore,
365
- fileClassification,
366
- node
367
- );
368
- const adjustedFragmentationScore = adjustFragmentationForClassification(
369
- fragmentationScore,
370
- fileClassification
371
- );
372
- const classificationRecommendations = getClassificationRecommendations(
373
- fileClassification,
374
- file,
375
- issues
376
- );
377
-
378
- const {
379
- severity: adjustedSeverity,
380
- issues: adjustedIssues,
381
- recommendations: finalRecommendations,
382
- potentialSavings: adjustedSavings,
383
- } = analyzeIssues({
384
- file,
385
- importDepth,
386
- contextBudget,
387
- cohesionScore: adjustedCohesionScore,
388
- fragmentationScore: adjustedFragmentationScore,
389
- maxDepth,
390
- maxContextBudget,
391
- minCohesion,
392
- maxFragmentation,
393
- circularDeps,
394
- });
395
-
396
- results.push({
397
- file,
398
- tokenCost: node.tokenCost,
399
- linesOfCode: node.linesOfCode,
400
- importDepth,
401
- dependencyCount: dependencyList.length,
402
- dependencyList,
403
- circularDeps: circularDeps.filter((cycle) => cycle.includes(file)),
404
- cohesionScore: adjustedCohesionScore,
405
- domains,
406
- exportCount: node.exports.length,
407
- contextBudget,
408
- fragmentationScore: adjustedFragmentationScore,
409
- relatedFiles,
410
- fileClassification,
411
- severity: adjustedSeverity,
412
- issues: adjustedIssues,
413
- recommendations: [
414
- ...finalRecommendations,
415
- ...classificationRecommendations.slice(0, 1),
416
- ],
417
- potentialSavings: adjustedSavings,
418
- });
419
- }
420
-
421
- const allResults = [...results, ...pythonResults];
422
- const finalSummary = generateSummary(allResults, options);
423
- return allResults.sort((a, b) => {
424
- const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
425
- const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
426
- if (severityDiff !== 0) return severityDiff;
427
- return b.contextBudget - a.contextBudget;
428
- });
215
+ return [...results, ...pythonResults];
429
216
  }
@@ -11,6 +11,10 @@
11
11
  import { getParser, estimateTokens } from '@aiready/core';
12
12
  import { resolve, relative, dirname, join } from 'path';
13
13
  import fs from 'fs';
14
+ import {
15
+ calculateImportDepthFromEdges,
16
+ detectGraphCyclesFromFile,
17
+ } from '../utils/dependency-graph-utils';
14
18
 
15
19
  export interface PythonContextMetrics {
16
20
  file: string;
@@ -85,7 +89,7 @@ export async function analyzePythonContext(
85
89
 
86
90
  // Calculate metrics
87
91
  const linesOfCode = code.split('\n').length;
88
- const importDepth = await calculatePythonImportDepth(
92
+ const importDepth = calculateImportDepthFromEdges(
89
93
  file,
90
94
  dependencyGraph,
91
95
  new Set()
@@ -96,10 +100,10 @@ export async function analyzePythonContext(
96
100
  dependencyGraph
97
101
  );
98
102
  const cohesion = calculatePythonCohesion(exports, imports);
99
- const circularDependencies = detectCircularDependencies(
103
+ const circularDependencies = detectGraphCyclesFromFile(
100
104
  file,
101
105
  dependencyGraph
102
- );
106
+ ).map((cycle) => cycle.join(' -> '));
103
107
 
104
108
  results.push({
105
109
  file,
@@ -212,40 +216,6 @@ function resolvePythonImport(
212
216
  return undefined;
213
217
  }
214
218
 
215
- /**
216
- * Calculate import depth for a Python file
217
- */
218
- async function calculatePythonImportDepth(
219
- file: string,
220
- dependencyGraph: Map<string, Set<string>>,
221
- visited: Set<string>,
222
- depth: number = 0
223
- ): Promise<number> {
224
- if (visited.has(file)) {
225
- return depth; // Circular dependency, stop here
226
- }
227
-
228
- visited.add(file);
229
- const dependencies = dependencyGraph.get(file) || new Set();
230
-
231
- if (dependencies.size === 0) {
232
- return depth;
233
- }
234
-
235
- let maxDepth = depth;
236
- for (const dep of dependencies) {
237
- const depDepth = await calculatePythonImportDepth(
238
- dep,
239
- dependencyGraph,
240
- new Set(visited),
241
- depth + 1
242
- );
243
- maxDepth = Math.max(maxDepth, depDepth);
244
- }
245
-
246
- return maxDepth;
247
- }
248
-
249
219
  /**
250
220
  * Estimate context budget (tokens needed for file + direct deps)
251
221
  */
@@ -303,42 +273,3 @@ function calculatePythonCohesion(
303
273
 
304
274
  return Math.min(1, Math.max(0, cohesion));
305
275
  }
306
-
307
- /**
308
- * Detect circular dependencies
309
- */
310
- function detectCircularDependencies(
311
- file: string,
312
- dependencyGraph: Map<string, Set<string>>
313
- ): string[] {
314
- const circular: string[] = [];
315
- const visited = new Set<string>();
316
- const recursionStack = new Set<string>();
317
-
318
- function dfs(current: string, path: string[]): void {
319
- if (recursionStack.has(current)) {
320
- // Found a cycle
321
- const cycleStart = path.indexOf(current);
322
- const cycle = path.slice(cycleStart).concat([current]);
323
- circular.push(cycle.join(' → '));
324
- return;
325
- }
326
-
327
- if (visited.has(current)) {
328
- return;
329
- }
330
-
331
- visited.add(current);
332
- recursionStack.add(current);
333
-
334
- const dependencies = dependencyGraph.get(current) || new Set();
335
- for (const dep of dependencies) {
336
- dfs(dep, [...path, current]);
337
- }
338
-
339
- recursionStack.delete(current);
340
- }
341
-
342
- dfs(file, []);
343
- return [...new Set(circular)]; // Deduplicate
344
- }
@@ -0,0 +1,103 @@
1
+ import {
2
+ loadMergedConfig,
3
+ handleJSONOutput,
4
+ handleCLIError,
5
+ getElapsedTime,
6
+ resolveOutputPath,
7
+ } from '@aiready/core';
8
+ import { analyzeContext } from './analyzer';
9
+ import { generateSummary } from './summary';
10
+ import {
11
+ displayConsoleReport,
12
+ generateHTMLReport,
13
+ runInteractiveSetup,
14
+ } from './utils/output-formatter';
15
+ import chalk from 'chalk';
16
+ import { writeFileSync } from 'fs';
17
+
18
+ export async function contextActionHandler(directory: string, options: any) {
19
+ console.log(chalk.blue('šŸ” Analyzing context window costs...\n'));
20
+
21
+ const startTime = Date.now();
22
+
23
+ try {
24
+ // Define defaults
25
+ const defaults = {
26
+ maxDepth: 5,
27
+ maxContextBudget: 10000,
28
+ minCohesion: 0.6,
29
+ maxFragmentation: 0.5,
30
+ focus: 'all',
31
+ includeNodeModules: false,
32
+ include: undefined,
33
+ exclude: undefined,
34
+ maxResults: 10,
35
+ };
36
+
37
+ // Load and merge config with CLI options
38
+ let finalOptions = (await loadMergedConfig(directory, defaults, {
39
+ maxDepth: options.maxDepth ? parseInt(options.maxDepth) : undefined,
40
+ maxContextBudget: options.maxContext
41
+ ? parseInt(options.maxContext)
42
+ : undefined,
43
+ minCohesion: options.minCohesion
44
+ ? parseFloat(options.minCohesion)
45
+ : undefined,
46
+ maxFragmentation: options.maxFragmentation
47
+ ? parseFloat(options.maxFragmentation)
48
+ : undefined,
49
+ focus:
50
+ (options.focus as 'fragmentation' | 'cohesion' | 'depth' | 'all') ||
51
+ undefined,
52
+ includeNodeModules: options.includeNodeModules,
53
+ include: options.include?.split(','),
54
+ exclude: options.exclude?.split(','),
55
+ maxResults: options.maxResults ? parseInt(options.maxResults) : undefined,
56
+ })) as any;
57
+
58
+ // Interactive setup if requested
59
+ if (options.interactive) {
60
+ finalOptions = await runInteractiveSetup(directory, finalOptions);
61
+ }
62
+
63
+ // Run analysis
64
+ const results = await analyzeContext(finalOptions);
65
+ const summary = generateSummary(results, finalOptions);
66
+
67
+ const duration = getElapsedTime(startTime);
68
+
69
+ // Handle output
70
+ if (options.output === 'json') {
71
+ handleJSONOutput(
72
+ {
73
+ summary: {
74
+ ...summary,
75
+ executionTime: duration,
76
+ config: {
77
+ scan: { tools: ['context'] },
78
+ tools: { context: finalOptions },
79
+ },
80
+ toolConfigs: { context: finalOptions },
81
+ },
82
+ context: { results },
83
+ },
84
+ options.outputFile
85
+ );
86
+ } else if (options.output === 'html') {
87
+ const html = generateHTMLReport(summary, results);
88
+ const outputPath = resolveOutputPath(
89
+ directory,
90
+ options.outputFile,
91
+ 'context-report.html'
92
+ );
93
+ writeFileSync(outputPath, html, 'utf-8');
94
+ console.log(chalk.green(`\nāœ… HTML report saved to: ${outputPath}`));
95
+ } else {
96
+ // Default: Console
97
+ displayConsoleReport(summary, results, finalOptions.maxResults);
98
+ console.log(chalk.dim(`\n✨ Analysis completed in ${duration}ms\n`));
99
+ }
100
+ } catch (error) {
101
+ handleCLIError(error, 'context-analyzer');
102
+ }
103
+ }