@aiready/context-analyzer 0.21.10 → 0.21.11

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/src/heuristics.ts CHANGED
@@ -1,10 +1,75 @@
1
1
  import { DependencyNode } from './types';
2
2
 
3
3
  /**
4
- * Detect if a file is a barrel export (index.ts)
4
+ * Heuristic patterns for file classification.
5
+ */
6
+ const BARREL_EXPORT_MIN_EXPORTS = 5;
7
+ const BARREL_EXPORT_TOKEN_LIMIT = 1000;
8
+
9
+ const HANDLER_NAME_PATTERNS = [
10
+ 'handler',
11
+ '.handler.',
12
+ '-handler.',
13
+ 'lambda',
14
+ '.lambda.',
15
+ '-lambda.',
16
+ ];
17
+
18
+ const SERVICE_NAME_PATTERNS = [
19
+ 'service',
20
+ '.service.',
21
+ '-service.',
22
+ '_service.',
23
+ ];
24
+
25
+ const EMAIL_NAME_PATTERNS = [
26
+ '-email-',
27
+ '.email.',
28
+ '_email_',
29
+ '-template',
30
+ '.template.',
31
+ '_template',
32
+ '-mail.',
33
+ '.mail.',
34
+ ];
35
+
36
+ const PARSER_NAME_PATTERNS = [
37
+ 'parser',
38
+ '.parser.',
39
+ '-parser.',
40
+ '_parser.',
41
+ 'transform',
42
+ 'converter',
43
+ 'mapper',
44
+ 'serializer',
45
+ ];
46
+
47
+ const SESSION_NAME_PATTERNS = ['session', 'state', 'context', 'store'];
48
+
49
+ const NEXTJS_METADATA_EXPORTS = [
50
+ 'metadata',
51
+ 'generatemetadata',
52
+ 'faqjsonld',
53
+ 'jsonld',
54
+ 'icon',
55
+ ];
56
+
57
+ const CONFIG_NAME_PATTERNS = [
58
+ '.config.',
59
+ 'tsconfig',
60
+ 'jest.config',
61
+ 'package.json',
62
+ 'aiready.json',
63
+ 'next.config',
64
+ 'sst.config',
65
+ ];
66
+
67
+ /**
68
+ * Detect if a file is a barrel export (index.ts).
5
69
  *
6
70
  * @param node - The dependency node to analyze.
7
71
  * @returns True if the file matches barrel export patterns.
72
+ * @lastUpdated 2026-03-18
8
73
  */
9
74
  export function isBarrelExport(node: DependencyNode): boolean {
10
75
  const { file, exports } = node;
@@ -12,22 +77,24 @@ export function isBarrelExport(node: DependencyNode): boolean {
12
77
 
13
78
  const isIndexFile = fileName === 'index.ts' || fileName === 'index.js';
14
79
  const isSmallAndManyExports =
15
- node.tokenCost < 1000 && (exports || []).length > 5;
80
+ node.tokenCost < BARREL_EXPORT_TOKEN_LIMIT &&
81
+ (exports || []).length > BARREL_EXPORT_MIN_EXPORTS;
16
82
 
17
83
  const isReexportPattern =
18
- (exports || []).length >= 5 &&
19
- (exports || []).every((e) =>
20
- ['const', 'function', 'type', 'interface'].includes(e.type)
84
+ (exports || []).length >= BARREL_EXPORT_MIN_EXPORTS &&
85
+ (exports || []).every((exp: any) =>
86
+ ['const', 'function', 'type', 'interface'].includes(exp.type)
21
87
  );
22
88
 
23
89
  return !!isIndexFile || !!isSmallAndManyExports || !!isReexportPattern;
24
90
  }
25
91
 
26
92
  /**
27
- * Detect if a file is primarily type definitions
93
+ * Detect if a file is primarily type definitions.
28
94
  *
29
95
  * @param node - The dependency node to analyze.
30
96
  * @returns True if the file contains primarily types or matches type paths.
97
+ * @lastUpdated 2026-03-18
31
98
  */
32
99
  export function isTypeDefinition(node: DependencyNode): boolean {
33
100
  const { file } = node;
@@ -37,7 +104,9 @@ export function isTypeDefinition(node: DependencyNode): boolean {
37
104
  const hasExports = nodeExports.length > 0;
38
105
  const areAllTypes =
39
106
  hasExports &&
40
- nodeExports.every((e) => e.type === 'type' || e.type === 'interface');
107
+ nodeExports.every(
108
+ (exp: any) => exp.type === 'type' || exp.type === 'interface'
109
+ );
41
110
 
42
111
  const isTypePath = /\/(types|interfaces|models)\//i.test(file);
43
112
  return !!areAllTypes || (isTypePath && hasExports);
@@ -48,6 +117,7 @@ export function isTypeDefinition(node: DependencyNode): boolean {
48
117
  *
49
118
  * @param node - The dependency node to analyze.
50
119
  * @returns True if the file path or name suggests a utility/helper role.
120
+ * @lastUpdated 2026-03-18
51
121
  */
52
122
  export function isUtilityModule(node: DependencyNode): boolean {
53
123
  const { file } = node;
@@ -62,24 +132,20 @@ export function isUtilityModule(node: DependencyNode): boolean {
62
132
  *
63
133
  * @param node - The dependency node to analyze.
64
134
  * @returns True if the file serves as a coordination point for requests/lambdas.
135
+ * @lastUpdated 2026-03-18
65
136
  */
66
137
  export function isLambdaHandler(node: DependencyNode): boolean {
67
138
  const { file, exports } = node;
68
139
  const fileName = file.split('/').pop()?.toLowerCase() || '';
69
- const handlerPatterns = [
70
- 'handler',
71
- '.handler.',
72
- '-handler.',
73
- 'lambda',
74
- '.lambda.',
75
- '-lambda.',
76
- ];
77
- const isHandlerName = handlerPatterns.some((p) => fileName.includes(p));
140
+
141
+ const isHandlerName = HANDLER_NAME_PATTERNS.some((pattern: string) =>
142
+ fileName.includes(pattern)
143
+ );
78
144
  const isHandlerPath = /\/(handlers|lambdas|lambda|functions)\//i.test(file);
79
145
  const hasHandlerExport = (exports || []).some(
80
- (e) =>
81
- ['handler', 'main', 'lambdahandler'].includes(e.name.toLowerCase()) ||
82
- e.name.toLowerCase().endsWith('handler')
146
+ (exp: any) =>
147
+ ['handler', 'main', 'lambdahandler'].includes(exp.name.toLowerCase()) ||
148
+ exp.name.toLowerCase().endsWith('handler')
83
149
  );
84
150
  return isHandlerName || isHandlerPath || hasHandlerExport;
85
151
  }
@@ -89,17 +155,22 @@ export function isLambdaHandler(node: DependencyNode): boolean {
89
155
  *
90
156
  * @param node - The dependency node to analyze.
91
157
  * @returns True if the file orchestrates logic or matches service patterns.
158
+ * @lastUpdated 2026-03-18
92
159
  */
93
160
  export function isServiceFile(node: DependencyNode): boolean {
94
161
  const { file, exports } = node;
95
162
  const fileName = file.split('/').pop()?.toLowerCase() || '';
96
- const servicePatterns = ['service', '.service.', '-service.', '_service.'];
97
- const isServiceName = servicePatterns.some((p) => fileName.includes(p));
163
+
164
+ const isServiceName = SERVICE_NAME_PATTERNS.some((pattern: string) =>
165
+ fileName.includes(pattern)
166
+ );
98
167
  const isServicePath = file.toLowerCase().includes('/services/');
99
- const hasServiceNamedExport = (exports || []).some((e) =>
100
- e.name.toLowerCase().includes('service')
168
+ const hasServiceNamedExport = (exports || []).some((exp: any) =>
169
+ exp.name.toLowerCase().includes('service')
170
+ );
171
+ const hasClassExport = (exports || []).some(
172
+ (exp: any) => exp.type === 'class'
101
173
  );
102
- const hasClassExport = (exports || []).some((e) => e.type === 'class');
103
174
  return (
104
175
  isServiceName || isServicePath || (hasServiceNamedExport && hasClassExport)
105
176
  );
@@ -110,27 +181,21 @@ export function isServiceFile(node: DependencyNode): boolean {
110
181
  *
111
182
  * @param node - The dependency node to analyze.
112
183
  * @returns True if the file is used for rendering notifications or emails.
184
+ * @lastUpdated 2026-03-18
113
185
  */
114
186
  export function isEmailTemplate(node: DependencyNode): boolean {
115
187
  const { file, exports } = node;
116
188
  const fileName = file.split('/').pop()?.toLowerCase() || '';
117
- const emailPatterns = [
118
- '-email-',
119
- '.email.',
120
- '_email_',
121
- '-template',
122
- '.template.',
123
- '_template',
124
- '-mail.',
125
- '.mail.',
126
- ];
127
- const isEmailName = emailPatterns.some((p) => fileName.includes(p));
189
+
190
+ const isEmailName = EMAIL_NAME_PATTERNS.some((pattern: string) =>
191
+ fileName.includes(pattern)
192
+ );
128
193
  const isEmailPath = /\/(emails|mail|notifications)\//i.test(file);
129
194
  const hasTemplateFunction = (exports || []).some(
130
- (e) =>
131
- e.type === 'function' &&
132
- (e.name.toLowerCase().startsWith('render') ||
133
- e.name.toLowerCase().startsWith('generate'))
195
+ (exp: any) =>
196
+ exp.type === 'function' &&
197
+ (exp.name.toLowerCase().startsWith('render') ||
198
+ exp.name.toLowerCase().startsWith('generate'))
134
199
  );
135
200
  return isEmailPath || isEmailName || hasTemplateFunction;
136
201
  }
@@ -140,27 +205,21 @@ export function isEmailTemplate(node: DependencyNode): boolean {
140
205
  *
141
206
  * @param node - The dependency node to analyze.
142
207
  * @returns True if the file handles data conversion or serialization.
208
+ * @lastUpdated 2026-03-18
143
209
  */
144
210
  export function isParserFile(node: DependencyNode): boolean {
145
211
  const { file, exports } = node;
146
212
  const fileName = file.split('/').pop()?.toLowerCase() || '';
147
- const parserPatterns = [
148
- 'parser',
149
- '.parser.',
150
- '-parser.',
151
- '_parser.',
152
- 'transform',
153
- 'converter',
154
- 'mapper',
155
- 'serializer',
156
- ];
157
- const isParserName = parserPatterns.some((p) => fileName.includes(p));
213
+
214
+ const isParserName = PARSER_NAME_PATTERNS.some((pattern: string) =>
215
+ fileName.includes(pattern)
216
+ );
158
217
  const isParserPath = /\/(parsers|transformers)\//i.test(file);
159
218
  const hasParseFunction = (exports || []).some(
160
- (e) =>
161
- e.type === 'function' &&
162
- (e.name.toLowerCase().startsWith('parse') ||
163
- e.name.toLowerCase().startsWith('transform'))
219
+ (exp: any) =>
220
+ exp.type === 'function' &&
221
+ (exp.name.toLowerCase().startsWith('parse') ||
222
+ exp.name.toLowerCase().startsWith('transform'))
164
223
  );
165
224
  return isParserName || isParserPath || hasParseFunction;
166
225
  }
@@ -170,15 +229,20 @@ export function isParserFile(node: DependencyNode): boolean {
170
229
  *
171
230
  * @param node - The dependency node to analyze.
172
231
  * @returns True if the file manages application state or sessions.
232
+ * @lastUpdated 2026-03-18
173
233
  */
174
234
  export function isSessionFile(node: DependencyNode): boolean {
175
235
  const { file, exports } = node;
176
236
  const fileName = file.split('/').pop()?.toLowerCase() || '';
177
- const sessionPatterns = ['session', 'state', 'context', 'store'];
178
- const isSessionName = sessionPatterns.some((p) => fileName.includes(p));
237
+
238
+ const isSessionName = SESSION_NAME_PATTERNS.some((pattern: string) =>
239
+ fileName.includes(pattern)
240
+ );
179
241
  const isSessionPath = /\/(sessions|state)\//i.test(file);
180
- const hasSessionExport = (exports || []).some((e) =>
181
- ['session', 'state', 'store'].some((p) => e.name.toLowerCase().includes(p))
242
+ const hasSessionExport = (exports || []).some((exp: any) =>
243
+ ['session', 'state', 'store'].some((pattern: string) =>
244
+ exp.name.toLowerCase().includes(pattern)
245
+ )
182
246
  );
183
247
  return isSessionName || isSessionPath || hasSessionExport;
184
248
  }
@@ -188,6 +252,7 @@ export function isSessionFile(node: DependencyNode): boolean {
188
252
  *
189
253
  * @param node - The dependency node to analyze.
190
254
  * @returns True if the file is a Next.js page or metadata entry.
255
+ * @lastUpdated 2026-03-18
191
256
  */
192
257
  export function isNextJsPage(node: DependencyNode): boolean {
193
258
  const { file, exports } = node;
@@ -199,16 +264,12 @@ export function isNextJsPage(node: DependencyNode): boolean {
199
264
  if (!isInAppDir || (fileName !== 'page.tsx' && fileName !== 'page.ts'))
200
265
  return false;
201
266
 
202
- const hasDefaultExport = (exports || []).some((e) => e.type === 'default');
203
- const nextJsExports = [
204
- 'metadata',
205
- 'generatemetadata',
206
- 'faqjsonld',
207
- 'jsonld',
208
- 'icon',
209
- ];
210
- const hasNextJsExport = (exports || []).some((e) =>
211
- nextJsExports.includes(e.name.toLowerCase())
267
+ const hasDefaultExport = (exports || []).some(
268
+ (exp: any) => exp.type === 'default'
269
+ );
270
+
271
+ const hasNextJsExport = (exports || []).some((exp: any) =>
272
+ NEXTJS_METADATA_EXPORTS.includes(exp.name.toLowerCase())
212
273
  );
213
274
 
214
275
  return hasDefaultExport || hasNextJsExport;
@@ -219,28 +280,36 @@ export function isNextJsPage(node: DependencyNode): boolean {
219
280
  *
220
281
  * @param node - The dependency node to analyze.
221
282
  * @returns True if the file matches configuration, setting, or schema patterns.
283
+ * @lastUpdated 2026-03-18
222
284
  */
223
285
  export function isConfigFile(node: DependencyNode): boolean {
224
286
  const { file, exports } = node;
225
287
  const lowerPath = file.toLowerCase();
226
288
  const fileName = file.split('/').pop()?.toLowerCase() || '';
227
289
 
228
- const configPatterns = [
229
- '.config.',
230
- 'tsconfig',
231
- 'jest.config',
232
- 'package.json',
233
- 'aiready.json',
234
- 'next.config',
235
- 'sst.config',
236
- ];
237
- const isConfigName = configPatterns.some((p) => fileName.includes(p));
290
+ const isConfigName = CONFIG_NAME_PATTERNS.some((pattern: string) =>
291
+ fileName.includes(pattern)
292
+ );
238
293
  const isConfigPath = /\/(config|settings|schemas)\//i.test(lowerPath);
239
- const hasSchemaExport = (exports || []).some((e) =>
240
- ['schema', 'config', 'setting'].some((p) =>
241
- e.name.toLowerCase().includes(p)
294
+ const hasSchemaExport = (exports || []).some((exp: any) =>
295
+ ['schema', 'config', 'setting'].some((pattern: string) =>
296
+ exp.name.toLowerCase().includes(pattern)
242
297
  )
243
298
  );
244
299
 
245
300
  return isConfigName || isConfigPath || hasSchemaExport;
246
301
  }
302
+
303
+ /**
304
+ * Detect if a file is part of a hub-and-spoke monorepo architecture.
305
+ *
306
+ * Many files spread across multiple packages (spokes) is intentional in
307
+ * AIReady and shouldn't be penalized as heavily for fragmentation.
308
+ *
309
+ * @param node - The dependency node to analyze.
310
+ * @returns True if the file path suggests it belongs to a spoke package.
311
+ */
312
+ export function isHubAndSpokeFile(node: DependencyNode): boolean {
313
+ const { file } = node;
314
+ return /\/packages\/[a-zA-Z0-9-]+\/src\//.test(file);
315
+ }
package/src/mapper.ts ADDED
@@ -0,0 +1,118 @@
1
+ import type {
2
+ ContextAnalysisResult,
3
+ DependencyGraph,
4
+ DependencyNode,
5
+ ModuleCluster,
6
+ } from './types';
7
+ import { calculateEnhancedCohesion } from './metrics';
8
+ import { analyzeIssues } from './issue-analyzer';
9
+ import {
10
+ calculateImportDepth,
11
+ getTransitiveDependencies,
12
+ calculateContextBudget,
13
+ } from './graph-builder';
14
+ import {
15
+ classifyFile,
16
+ adjustCohesionForClassification,
17
+ adjustFragmentationForClassification,
18
+ } from './classifier';
19
+ import { getClassificationRecommendations } from './remediation';
20
+
21
+ export interface MappingOptions {
22
+ maxDepth: number;
23
+ maxContextBudget: number;
24
+ minCohesion: number;
25
+ maxFragmentation: number;
26
+ }
27
+
28
+ /**
29
+ * Maps a single dependency node to a comprehensive ContextAnalysisResult.
30
+ */
31
+ export function mapNodeToResult(
32
+ node: DependencyNode,
33
+ graph: DependencyGraph,
34
+ clusters: ModuleCluster[],
35
+ allCircularDeps: string[][],
36
+ options: MappingOptions
37
+ ): ContextAnalysisResult {
38
+ const file = node.file;
39
+ const tokenCost = node.tokenCost;
40
+ const importDepth = calculateImportDepth(file, graph);
41
+ const transitiveDeps = getTransitiveDependencies(file, graph);
42
+ const contextBudget = calculateContextBudget(file, graph);
43
+ const circularDeps = allCircularDeps.filter((cycle) => cycle.includes(file));
44
+
45
+ // Find cluster for this file
46
+ const cluster = clusters.find((c) => c.files.includes(file));
47
+ const rawFragmentationScore = cluster ? cluster.fragmentationScore : 0;
48
+
49
+ // Cohesion
50
+ const rawCohesionScore = calculateEnhancedCohesion(
51
+ node.exports,
52
+ file,
53
+ options as any
54
+ );
55
+
56
+ // Initial classification
57
+ const fileClassification = classifyFile(node, rawCohesionScore);
58
+
59
+ // Adjust scores based on classification
60
+ const cohesionScore = adjustCohesionForClassification(
61
+ rawCohesionScore,
62
+ fileClassification
63
+ );
64
+ const fragmentationScore = adjustFragmentationForClassification(
65
+ rawFragmentationScore,
66
+ fileClassification
67
+ );
68
+
69
+ const { severity, issues, recommendations, potentialSavings } = analyzeIssues(
70
+ {
71
+ file,
72
+ importDepth,
73
+ contextBudget,
74
+ cohesionScore,
75
+ fragmentationScore,
76
+ maxDepth: options.maxDepth,
77
+ maxContextBudget: options.maxContextBudget,
78
+ minCohesion: options.minCohesion,
79
+ maxFragmentation: options.maxFragmentation,
80
+ circularDeps,
81
+ }
82
+ );
83
+
84
+ // Add classification-specific recommendations
85
+ const classRecs = getClassificationRecommendations(
86
+ fileClassification,
87
+ file,
88
+ issues
89
+ );
90
+ const allRecommendations = Array.from(
91
+ new Set([...recommendations, ...classRecs])
92
+ );
93
+
94
+ return {
95
+ file,
96
+ tokenCost,
97
+ linesOfCode: node.linesOfCode,
98
+ importDepth,
99
+ dependencyCount: transitiveDeps.length,
100
+ dependencyList: transitiveDeps,
101
+ circularDeps,
102
+ cohesionScore,
103
+ domains: Array.from(
104
+ new Set(
105
+ node.exports.flatMap((e) => e.domains?.map((d) => d.domain) || [])
106
+ )
107
+ ),
108
+ exportCount: node.exports.length,
109
+ contextBudget,
110
+ fragmentationScore,
111
+ relatedFiles: cluster ? cluster.files : [],
112
+ fileClassification,
113
+ severity,
114
+ issues,
115
+ recommendations: allRecommendations,
116
+ potentialSavings,
117
+ };
118
+ }
package/src/metrics.ts CHANGED
@@ -32,7 +32,8 @@ export function calculateEnhancedCohesion(
32
32
  // 1. Domain-based cohesion using entropy
33
33
  const domains = exports.map((e) => e.inferredDomain || 'unknown');
34
34
  const domainCounts = new Map<string, number>();
35
- for (const d of domains) domainCounts.set(d, (domainCounts.get(d) || 0) + 1);
35
+ for (const domain of domains)
36
+ domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
36
37
 
37
38
  // IF ALL DOMAINS MATCH, RETURN 1.0 IMMEDIATELY (Legacy test compatibility)
38
39
  if (domainCounts.size === 1 && domains[0] !== 'unknown') {
@@ -40,11 +41,11 @@ export function calculateEnhancedCohesion(
40
41
  }
41
42
 
42
43
  const probs = Array.from(domainCounts.values()).map(
43
- (c) => c / exports.length
44
+ (count) => count / exports.length
44
45
  );
45
46
  let domainEntropy = 0;
46
- for (const p of probs) {
47
- if (p > 0) domainEntropy -= p * Math.log2(p);
47
+ for (const prob of probs) {
48
+ if (prob > 0) domainEntropy -= prob * Math.log2(prob);
48
49
  }
49
50
 
50
51
  const maxEntropy = Math.log2(Math.max(2, domainCounts.size));
@@ -158,7 +159,7 @@ export function calculateFragmentation(
158
159
  if (files.length <= 1) return 0;
159
160
 
160
161
  const directories = new Set(
161
- files.map((f) => f.split('/').slice(0, -1).join('/'))
162
+ files.map((file) => file.split('/').slice(0, -1).join('/'))
162
163
  );
163
164
  const uniqueDirs = directories.size;
164
165
 
@@ -189,15 +190,15 @@ export function calculatePathEntropy(files: string[]): number {
189
190
  if (!files || files.length === 0) return 0;
190
191
 
191
192
  const dirCounts = new Map<string, number>();
192
- for (const f of files) {
193
- const dir = f.split('/').slice(0, -1).join('/') || '.';
193
+ for (const file of files) {
194
+ const dir = file.split('/').slice(0, -1).join('/') || '.';
194
195
  dirCounts.set(dir, (dirCounts.get(dir) || 0) + 1);
195
196
  }
196
197
 
197
198
  const counts = Array.from(dirCounts.values());
198
199
  if (counts.length <= 1) return 0;
199
200
 
200
- const total = counts.reduce((s, v) => s + v, 0);
201
+ const total = counts.reduce((sum, value) => sum + value, 0);
201
202
  let entropy = 0;
202
203
  for (const count of counts) {
203
204
  const prob = count / total;
@@ -217,11 +218,11 @@ export function calculatePathEntropy(files: string[]): number {
217
218
  export function calculateDirectoryDistance(files: string[]): number {
218
219
  if (!files || files.length <= 1) return 0;
219
220
 
220
- const pathSegments = (p: string) => p.split('/').filter(Boolean);
221
- const commonAncestorDepth = (a: string[], b: string[]) => {
222
- const minLen = Math.min(a.length, b.length);
221
+ const pathSegments = (pathStr: string) => pathStr.split('/').filter(Boolean);
222
+ const commonAncestorDepth = (pathA: string[], pathB: string[]) => {
223
+ const minLen = Math.min(pathA.length, pathB.length);
223
224
  let i = 0;
224
- while (i < minLen && a[i] === b[i]) i++;
225
+ while (i < minLen && pathA[i] === pathB[i]) i++;
225
226
  return i;
226
227
  };
227
228
 
@@ -0,0 +1,136 @@
1
+ import { scanFiles, readFileContent } from '@aiready/core';
2
+ import type {
3
+ ContextAnalysisResult,
4
+ ContextAnalyzerOptions,
5
+ FileClassification,
6
+ } from './types';
7
+ import { calculateEnhancedCohesion } from './metrics';
8
+ import { analyzeIssues } from './issue-analyzer';
9
+
10
+ import {
11
+ buildDependencyGraph,
12
+ detectCircularDependencies,
13
+ } from './graph-builder';
14
+ import { detectModuleClusters } from './cluster-detector';
15
+ import { mapNodeToResult } from './mapper';
16
+
17
+ /**
18
+ * Calculate cohesion score (how related are exports in a file).
19
+ * Legacy wrapper for backward compatibility with exact test expectations.
20
+ *
21
+ * @param exports - List of exported symbols
22
+ * @param filePath - Path to the file being analyzed
23
+ * @param options - Additional options for cohesion calculation
24
+ * @returns Cohesion score between 0 and 1
25
+ */
26
+ export function calculateCohesion(
27
+ exports: any[],
28
+ filePath?: string,
29
+ options?: any
30
+ ): number {
31
+ return calculateEnhancedCohesion(exports, filePath, options);
32
+ }
33
+
34
+ /**
35
+ * Performs deep context analysis of a project.
36
+ * Scans files, builds a dependency graph, calculates context budgets,
37
+ * and identifies structural issues like high fragmentation or depth.
38
+ *
39
+ * @param options - Analysis parameters including root directory and focus areas
40
+ * @returns Comprehensive analysis results with metrics and identified issues
41
+ */
42
+ export async function analyzeContext(
43
+ options: ContextAnalyzerOptions
44
+ ): Promise<ContextAnalysisResult[]> {
45
+ const {
46
+ maxDepth = 5,
47
+ maxContextBudget = 25000,
48
+ minCohesion = 0.6,
49
+ maxFragmentation = 0.5,
50
+ includeNodeModules = false,
51
+ ...scanOptions
52
+ } = options;
53
+
54
+ const files = await scanFiles({
55
+ ...scanOptions,
56
+ exclude:
57
+ includeNodeModules && scanOptions.exclude
58
+ ? scanOptions.exclude.filter(
59
+ (pattern) => pattern !== '**/node_modules/**'
60
+ )
61
+ : scanOptions.exclude,
62
+ });
63
+
64
+ const pythonFiles = files.filter((f) => f.toLowerCase().endsWith('.py'));
65
+ const fileContents = await Promise.all(
66
+ files.map(async (file) => ({
67
+ file,
68
+ content: await readFileContent(file),
69
+ }))
70
+ );
71
+
72
+ const graph = buildDependencyGraph(
73
+ fileContents.filter((f) => !f.file.toLowerCase().endsWith('.py'))
74
+ );
75
+
76
+ let pythonResults: ContextAnalysisResult[] = [];
77
+ if (pythonFiles.length > 0) {
78
+ const { analyzePythonContext } = await import('./analyzers/python-context');
79
+ const pythonMetrics = await analyzePythonContext(
80
+ pythonFiles,
81
+ scanOptions.rootDir || options.rootDir || '.'
82
+ );
83
+
84
+ pythonResults = pythonMetrics.map((metric) => {
85
+ const { severity, issues, recommendations, potentialSavings } =
86
+ analyzeIssues({
87
+ file: metric.file,
88
+ importDepth: metric.importDepth,
89
+ contextBudget: metric.contextBudget,
90
+ cohesionScore: metric.cohesion,
91
+ fragmentationScore: 0,
92
+ maxDepth,
93
+ maxContextBudget,
94
+ minCohesion,
95
+ maxFragmentation,
96
+ circularDeps: [],
97
+ });
98
+
99
+ return {
100
+ file: metric.file,
101
+ tokenCost: 0,
102
+ linesOfCode: 0,
103
+ importDepth: metric.importDepth,
104
+ dependencyCount: 0,
105
+ dependencyList: [],
106
+ circularDeps: [],
107
+ cohesionScore: metric.cohesion,
108
+ domains: [],
109
+ exportCount: 0,
110
+ contextBudget: metric.contextBudget,
111
+ fragmentationScore: 0,
112
+ relatedFiles: [],
113
+ fileClassification: 'unknown' as FileClassification,
114
+ severity,
115
+ issues,
116
+ recommendations,
117
+ potentialSavings,
118
+ };
119
+ });
120
+ }
121
+
122
+ const clusters = detectModuleClusters(graph);
123
+ const allCircularDeps = detectCircularDependencies(graph);
124
+
125
+ const results: ContextAnalysisResult[] = Array.from(graph.nodes.values()).map(
126
+ (node) =>
127
+ mapNodeToResult(node, graph, clusters, allCircularDeps, {
128
+ maxDepth,
129
+ maxContextBudget,
130
+ minCohesion,
131
+ maxFragmentation,
132
+ })
133
+ );
134
+
135
+ return [...results, ...pythonResults];
136
+ }