@aiready/context-analyzer 0.21.10 → 0.21.12

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/dist/index.mjs CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  inferDomain,
33
33
  inferDomainFromSemantics,
34
34
  runInteractiveSetup
35
- } from "./chunk-D25B5LZR.mjs";
35
+ } from "./chunk-CAX2MOUZ.mjs";
36
36
  import "./chunk-64U3PNO3.mjs";
37
37
 
38
38
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/context-analyzer",
3
- "version": "0.21.10",
3
+ "version": "0.21.12",
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.23.7"
52
+ "@aiready/core": "0.23.9"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/node": "^24.0.0",
@@ -89,14 +89,11 @@ describe('Cluster Detector', () => {
89
89
  const clusters = detectModuleClusters(graph);
90
90
 
91
91
  expect(clusters[0].domain).toBe('auth');
92
- // fragmentation should be high because they are in different directories
93
- expect(clusters[0].fragmentationScore).toBeGreaterThan(0.5);
94
- expect(
95
- clusters[0].suggestedStructure.consolidationPlan.length
96
- ).toBeGreaterThan(0);
97
- expect(clusters[0].suggestedStructure.consolidationPlan[0]).toContain(
98
- 'Consolidate'
99
- );
92
+ // fragmentation is reduced because files share imports (coupling discount)
93
+ // and are classified as cohesive modules (single domain 'auth')
94
+ // Final score: ~0.24 (raw 1.0 * 0.8 coupling discount * 0.3 cohesive multiplier)
95
+ expect(clusters[0].fragmentationScore).toBeLessThan(0.5);
96
+ expect(clusters[0].suggestedStructure.consolidationPlan.length).toBe(0); // No consolidation needed when fragmentation is low
100
97
  });
101
98
 
102
99
  it('should suggest boundary improvements for large domains', () => {
@@ -31,8 +31,11 @@ describe('fragmentation coupling discount', () => {
31
31
  files.map((f) => f.file),
32
32
  'billing'
33
33
  );
34
- // With no import similarity the coupling discount should be 0 -> fragmentation unchanged
35
- expect(cluster!.fragmentationScore).toBeCloseTo(base, 6);
34
+ // Adjusted fragmentation includes classification multiplier (0.3 for COHESIVE_MODULE)
35
+ const expected = base * 0.3;
36
+
37
+ // Allow small FP tolerance
38
+ expect(cluster!.fragmentationScore).toBeCloseTo(expected, 6);
36
39
  });
37
40
 
38
41
  it('applies up-to-20% discount when files share identical imports', () => {
@@ -60,7 +63,7 @@ describe('fragmentation coupling discount', () => {
60
63
  files.map((f) => f.file),
61
64
  'billing'
62
65
  );
63
- const expected = base * 0.8; // full cohesion => 20% discount
66
+ const expected = base * 0.8 * 0.3; // full cohesion => 20% discount AND 0.3 classification multiplier
64
67
 
65
68
  // Allow small FP tolerance
66
69
  expect(cluster!.fragmentationScore).toBeCloseTo(expected, 6);
package/src/analyzer.ts CHANGED
@@ -1,224 +1 @@
1
- import { scanFiles, readFileContent } from '@aiready/core';
2
- import type {
3
- ExportInfo,
4
- ContextAnalysisResult,
5
- ContextAnalyzerOptions,
6
- FileClassification,
7
- } from './types';
8
- import { calculateEnhancedCohesion } from './metrics';
9
- import { analyzeIssues } from './issue-analyzer';
10
-
11
- import {
12
- buildDependencyGraph,
13
- calculateImportDepth,
14
- getTransitiveDependencies,
15
- calculateContextBudget,
16
- detectCircularDependencies,
17
- } from './graph-builder';
18
- import { detectModuleClusters } from './cluster-detector';
19
- import {
20
- classifyFile,
21
- adjustCohesionForClassification,
22
- adjustFragmentationForClassification,
23
- } from './classifier';
24
- import { getClassificationRecommendations } from './remediation';
25
-
26
- /**
27
- * Calculate cohesion score (how related are exports in a file).
28
- * Legacy wrapper for backward compatibility with exact test expectations.
29
- *
30
- * @param exports - List of exported symbols
31
- * @param filePath - Path to the file being analyzed
32
- * @param options - Additional options for cohesion calculation
33
- * @returns Cohesion score between 0 and 1
34
- */
35
- export function calculateCohesion(
36
- exports: ExportInfo[],
37
- filePath?: string,
38
- options?: any
39
- ): number {
40
- return calculateEnhancedCohesion(exports, filePath, options);
41
- }
42
-
43
- /**
44
- * Analyze AI context window cost for a codebase
45
- */
46
- /**
47
- * Performs deep context analysis of a project.
48
- * Scans files, builds a dependency graph, calculates context budgets,
49
- * and identifies structural issues like high fragmentation or depth.
50
- *
51
- * @param options - Analysis parameters including root directory and focus areas
52
- * @returns Comprehensive analysis results with metrics and identified issues
53
- */
54
- export async function analyzeContext(
55
- options: ContextAnalyzerOptions
56
- ): Promise<ContextAnalysisResult[]> {
57
- const {
58
- maxDepth = 5,
59
- maxContextBudget = 25000,
60
- minCohesion = 0.6,
61
- maxFragmentation = 0.5,
62
- includeNodeModules = false,
63
- ...scanOptions
64
- } = options;
65
-
66
- const files = await scanFiles({
67
- ...scanOptions,
68
- exclude:
69
- includeNodeModules && scanOptions.exclude
70
- ? scanOptions.exclude.filter(
71
- (pattern) => pattern !== '**/node_modules/**'
72
- )
73
- : scanOptions.exclude,
74
- });
75
-
76
- const pythonFiles = files.filter((f) => f.toLowerCase().endsWith('.py'));
77
- const fileContents = await Promise.all(
78
- files.map(async (file) => ({
79
- file,
80
- content: await readFileContent(file),
81
- }))
82
- );
83
-
84
- const graph = buildDependencyGraph(
85
- fileContents.filter((f) => !f.file.toLowerCase().endsWith('.py'))
86
- );
87
-
88
- let pythonResults: ContextAnalysisResult[] = [];
89
- if (pythonFiles.length > 0) {
90
- const { analyzePythonContext } = await import('./analyzers/python-context');
91
- const pythonMetrics = await analyzePythonContext(
92
- pythonFiles,
93
- scanOptions.rootDir || options.rootDir || '.'
94
- );
95
-
96
- pythonResults = pythonMetrics.map((metric) => {
97
- const { severity, issues, recommendations, potentialSavings } =
98
- analyzeIssues({
99
- file: metric.file,
100
- importDepth: metric.importDepth,
101
- contextBudget: metric.contextBudget,
102
- cohesionScore: metric.cohesion,
103
- fragmentationScore: 0,
104
- maxDepth,
105
- maxContextBudget,
106
- minCohesion,
107
- maxFragmentation,
108
- circularDeps: [],
109
- });
110
-
111
- return {
112
- file: metric.file,
113
- tokenCost: 0,
114
- linesOfCode: 0, // Not provided by python context yet
115
- importDepth: metric.importDepth,
116
- dependencyCount: 0, // Not provided
117
- dependencyList: [],
118
- circularDeps: [],
119
- cohesionScore: metric.cohesion,
120
- domains: [],
121
- exportCount: 0,
122
- contextBudget: metric.contextBudget,
123
- fragmentationScore: 0,
124
- relatedFiles: [],
125
- fileClassification: 'unknown' as FileClassification,
126
- severity,
127
- issues,
128
- recommendations,
129
- potentialSavings,
130
- };
131
- });
132
- }
133
-
134
- const clusters = detectModuleClusters(graph);
135
- const allCircularDeps = detectCircularDependencies(graph);
136
-
137
- const results: ContextAnalysisResult[] = Array.from(graph.nodes.values()).map(
138
- (node) => {
139
- const file = node.file;
140
- const tokenCost = node.tokenCost;
141
- const importDepth = calculateImportDepth(file, graph);
142
- const transitiveDeps = getTransitiveDependencies(file, graph);
143
- const contextBudget = calculateContextBudget(file, graph);
144
- const circularDeps = allCircularDeps.filter((cycle) =>
145
- cycle.includes(file)
146
- );
147
-
148
- // Find cluster for this file
149
- const cluster = clusters.find((c) => c.files.includes(file));
150
- const rawFragmentationScore = cluster ? cluster.fragmentationScore : 0;
151
-
152
- // Cohesion
153
- const rawCohesionScore = calculateEnhancedCohesion(
154
- node.exports,
155
- file,
156
- options as any
157
- );
158
-
159
- // Initial classification
160
- const fileClassification = classifyFile(node, rawCohesionScore);
161
-
162
- // Adjust scores based on classification
163
- const cohesionScore = adjustCohesionForClassification(
164
- rawCohesionScore,
165
- fileClassification
166
- );
167
- const fragmentationScore = adjustFragmentationForClassification(
168
- rawFragmentationScore,
169
- fileClassification
170
- );
171
-
172
- const { severity, issues, recommendations, potentialSavings } =
173
- analyzeIssues({
174
- file,
175
- importDepth,
176
- contextBudget,
177
- cohesionScore,
178
- fragmentationScore,
179
- maxDepth,
180
- maxContextBudget,
181
- minCohesion,
182
- maxFragmentation,
183
- circularDeps,
184
- });
185
-
186
- // Add classification-specific recommendations
187
- const classRecs = getClassificationRecommendations(
188
- fileClassification,
189
- file,
190
- issues
191
- );
192
- const allRecommendations = Array.from(
193
- new Set([...recommendations, ...classRecs])
194
- );
195
-
196
- return {
197
- file,
198
- tokenCost,
199
- linesOfCode: node.linesOfCode,
200
- importDepth,
201
- dependencyCount: transitiveDeps.length,
202
- dependencyList: transitiveDeps,
203
- circularDeps,
204
- cohesionScore,
205
- domains: Array.from(
206
- new Set(
207
- node.exports.flatMap((e) => e.domains?.map((d) => d.domain) || [])
208
- )
209
- ),
210
- exportCount: node.exports.length,
211
- contextBudget,
212
- fragmentationScore,
213
- relatedFiles: cluster ? cluster.files : [],
214
- fileClassification,
215
- severity,
216
- issues,
217
- recommendations: allRecommendations,
218
- potentialSavings,
219
- };
220
- }
221
- );
222
-
223
- return [...results, ...pythonResults];
224
- }
1
+ export * from './orchestrator';
package/src/classifier.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  isSessionFile,
11
11
  isUtilityModule,
12
12
  isConfigFile,
13
+ isHubAndSpokeFile,
13
14
  } from './heuristics';
14
15
 
15
16
  /**
@@ -25,6 +26,7 @@ export const Classification = {
25
26
  PARSER: 'parser-file' as const,
26
27
  COHESIVE_MODULE: 'cohesive-module' as const,
27
28
  UTILITY_MODULE: 'utility-module' as const,
29
+ SPOKE_MODULE: 'spoke-module' as const,
28
30
  MIXED_CONCERNS: 'mixed-concerns' as const,
29
31
  UNKNOWN: 'unknown' as const,
30
32
  };
@@ -95,6 +97,11 @@ export function classifyFile(
95
97
  return Classification.COHESIVE_MODULE;
96
98
  }
97
99
 
100
+ // 11. Detect Spoke modules in monorepo
101
+ if (isHubAndSpokeFile(node)) {
102
+ return Classification.SPOKE_MODULE;
103
+ }
104
+
98
105
  // Cohesion and Domain heuristics
99
106
  if (domains.length <= 1 && domains[0] !== 'unknown') {
100
107
  return Classification.COHESIVE_MODULE;
@@ -152,6 +159,8 @@ export function adjustCohesionForClassification(
152
159
  return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
153
160
  case Classification.PARSER:
154
161
  return Math.max(0.7, Math.min(1, baseCohesion + 0.3));
162
+ case Classification.SPOKE_MODULE:
163
+ return Math.max(baseCohesion, 0.6);
155
164
  case Classification.COHESIVE_MODULE:
156
165
  return Math.max(baseCohesion, 0.7);
157
166
  case Classification.MIXED_CONCERNS:
@@ -237,6 +246,8 @@ export function adjustFragmentationForClassification(
237
246
  case Classification.PARSER:
238
247
  case Classification.NEXTJS_PAGE:
239
248
  return baseFragmentation * 0.2;
249
+ case Classification.SPOKE_MODULE:
250
+ return baseFragmentation * 0.15; // Heavily discount intentional monorepo separation
240
251
  case Classification.COHESIVE_MODULE:
241
252
  return baseFragmentation * 0.3;
242
253
  case Classification.MIXED_CONCERNS:
@@ -1,5 +1,9 @@
1
1
  import type { DependencyGraph, ModuleCluster } from './types';
2
2
  import { calculateFragmentation, calculateEnhancedCohesion } from './metrics';
3
+ import {
4
+ classifyFile,
5
+ adjustFragmentationForClassification,
6
+ } from './classifier';
3
7
 
4
8
  /**
5
9
  * Group files by domain to detect module clusters
@@ -73,29 +77,42 @@ export function detectModuleClusters(
73
77
  sharedImportRatio = union.size > 0 ? intersection.size / union.size : 0;
74
78
  }
75
79
 
76
- const fragmentation = calculateFragmentation(files, domain, {
80
+ const rawFragmentation = calculateFragmentation(files, domain, {
77
81
  ...options,
78
82
  sharedImportRatio,
79
83
  });
80
84
 
81
- // Average cohesion for the cluster
85
+ // Average cohesion and adjusted fragmentation for the cluster
82
86
  let totalCohesion = 0;
87
+ let totalAdjustedFragmentation = 0;
88
+
83
89
  files.forEach((f) => {
84
90
  const node = graph.nodes.get(f);
85
- if (node) totalCohesion += calculateEnhancedCohesion(node.exports);
91
+ if (node) {
92
+ const cohesion = calculateEnhancedCohesion(node.exports);
93
+ totalCohesion += cohesion;
94
+
95
+ const classification = classifyFile(node, cohesion);
96
+ totalAdjustedFragmentation += adjustFragmentationForClassification(
97
+ rawFragmentation,
98
+ classification
99
+ );
100
+ }
86
101
  });
102
+
87
103
  const avgCohesion = totalCohesion / files.length;
104
+ const fragmentationScore = totalAdjustedFragmentation / files.length;
88
105
 
89
106
  clusters.push({
90
107
  domain,
91
108
  files,
92
109
  totalTokens,
93
- fragmentationScore: fragmentation,
110
+ fragmentationScore,
94
111
  avgCohesion,
95
112
  suggestedStructure: generateSuggestedStructure(
96
113
  files,
97
114
  totalTokens,
98
- fragmentation
115
+ fragmentationScore
99
116
  ),
100
117
  });
101
118
  }