@aiready/context-analyzer 0.21.9 → 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.
@@ -52,7 +52,10 @@ function resolveImport(
52
52
  }
53
53
 
54
54
  /**
55
- * Auto-detect domain keywords from workspace folder structure
55
+ * Auto-detect domain keywords from workspace folder structure.
56
+ *
57
+ * @param files - Array of file contents to analyze for folder patterns.
58
+ * @returns Array of singularized domain keywords.
56
59
  */
57
60
  export function extractDomainKeywordsFromPaths(files: FileContent[]): string[] {
58
61
  const folderNames = new Set<string>();
@@ -96,7 +99,11 @@ export function extractDomainKeywordsFromPaths(files: FileContent[]): string[] {
96
99
  }
97
100
 
98
101
  /**
99
- * Build a dependency graph from file contents
102
+ * Build a dependency graph from file contents, resolving imports and extracting metadata.
103
+ *
104
+ * @param files - Array of file contents to process.
105
+ * @param options - Optional configuration for domain detection.
106
+ * @returns Complete dependency graph with nodes, edges, and semantic matrices.
100
107
  */
101
108
  export function buildDependencyGraph(
102
109
  files: FileContent[],
@@ -170,7 +177,13 @@ export function buildDependencyGraph(
170
177
  }
171
178
 
172
179
  /**
173
- * Calculate the maximum depth of import tree for a file
180
+ * Calculate the maximum depth of the import tree for a specific file.
181
+ *
182
+ * @param file - File path to start depth calculation from.
183
+ * @param graph - The dependency graph.
184
+ * @param visited - Optional set to track visited nodes during traversal.
185
+ * @param depth - Current recursion depth.
186
+ * @returns Maximum depth of the import chain.
174
187
  */
175
188
  export function calculateImportDepth(
176
189
  file: string,
@@ -182,7 +195,12 @@ export function calculateImportDepth(
182
195
  }
183
196
 
184
197
  /**
185
- * Get all transitive dependencies for a file
198
+ * Retrieve all transitive dependencies for a specific file.
199
+ *
200
+ * @param file - File path to analyze.
201
+ * @param graph - The dependency graph.
202
+ * @param visited - Optional set to track visited nodes.
203
+ * @returns Array of all reachable file paths.
186
204
  */
187
205
  export function getTransitiveDependencies(
188
206
  file: string,
@@ -193,7 +211,11 @@ export function getTransitiveDependencies(
193
211
  }
194
212
 
195
213
  /**
196
- * Calculate total context budget (tokens needed to understand this file)
214
+ * Calculate total context budget (tokens needed to understand this file and its dependencies).
215
+ *
216
+ * @param file - File path to calculate budget for.
217
+ * @param graph - The dependency graph.
218
+ * @returns Total token count including recursive dependencies.
197
219
  */
198
220
  export function calculateContextBudget(
199
221
  file: string,
@@ -216,7 +238,10 @@ export function calculateContextBudget(
216
238
  }
217
239
 
218
240
  /**
219
- * Detect circular dependencies
241
+ * Detect circular dependencies (cycles) within the dependency graph.
242
+ *
243
+ * @param graph - The dependency graph to scan.
244
+ * @returns Array of dependency cycles (each cycle is an array of file paths).
220
245
  */
221
246
  export function detectCircularDependencies(graph: DependencyGraph): string[][] {
222
247
  return detectGraphCycles(graph.edges);
package/src/heuristics.ts CHANGED
@@ -1,7 +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).
69
+ *
70
+ * @param node - The dependency node to analyze.
71
+ * @returns True if the file matches barrel export patterns.
72
+ * @lastUpdated 2026-03-18
5
73
  */
6
74
  export function isBarrelExport(node: DependencyNode): boolean {
7
75
  const { file, exports } = node;
@@ -9,19 +77,24 @@ export function isBarrelExport(node: DependencyNode): boolean {
9
77
 
10
78
  const isIndexFile = fileName === 'index.ts' || fileName === 'index.js';
11
79
  const isSmallAndManyExports =
12
- node.tokenCost < 1000 && (exports || []).length > 5;
80
+ node.tokenCost < BARREL_EXPORT_TOKEN_LIMIT &&
81
+ (exports || []).length > BARREL_EXPORT_MIN_EXPORTS;
13
82
 
14
83
  const isReexportPattern =
15
- (exports || []).length >= 5 &&
16
- (exports || []).every((e) =>
17
- ['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)
18
87
  );
19
88
 
20
89
  return !!isIndexFile || !!isSmallAndManyExports || !!isReexportPattern;
21
90
  }
22
91
 
23
92
  /**
24
- * Detect if a file is primarily type definitions
93
+ * Detect if a file is primarily type definitions.
94
+ *
95
+ * @param node - The dependency node to analyze.
96
+ * @returns True if the file contains primarily types or matches type paths.
97
+ * @lastUpdated 2026-03-18
25
98
  */
26
99
  export function isTypeDefinition(node: DependencyNode): boolean {
27
100
  const { file } = node;
@@ -31,14 +104,20 @@ export function isTypeDefinition(node: DependencyNode): boolean {
31
104
  const hasExports = nodeExports.length > 0;
32
105
  const areAllTypes =
33
106
  hasExports &&
34
- nodeExports.every((e) => e.type === 'type' || e.type === 'interface');
107
+ nodeExports.every(
108
+ (exp: any) => exp.type === 'type' || exp.type === 'interface'
109
+ );
35
110
 
36
111
  const isTypePath = /\/(types|interfaces|models)\//i.test(file);
37
112
  return !!areAllTypes || (isTypePath && hasExports);
38
113
  }
39
114
 
40
115
  /**
41
- * Detect if a file is a utility module
116
+ * Detect if a file is a utility module.
117
+ *
118
+ * @param node - The dependency node to analyze.
119
+ * @returns True if the file path or name suggests a utility/helper role.
120
+ * @lastUpdated 2026-03-18
42
121
  */
43
122
  export function isUtilityModule(node: DependencyNode): boolean {
44
123
  const { file } = node;
@@ -49,118 +128,131 @@ export function isUtilityModule(node: DependencyNode): boolean {
49
128
  }
50
129
 
51
130
  /**
52
- * Detect if a file is a Lambda/API handler
131
+ * Detect if a file is a Lambda/API handler.
132
+ *
133
+ * @param node - The dependency node to analyze.
134
+ * @returns True if the file serves as a coordination point for requests/lambdas.
135
+ * @lastUpdated 2026-03-18
53
136
  */
54
137
  export function isLambdaHandler(node: DependencyNode): boolean {
55
138
  const { file, exports } = node;
56
139
  const fileName = file.split('/').pop()?.toLowerCase() || '';
57
- const handlerPatterns = [
58
- 'handler',
59
- '.handler.',
60
- '-handler.',
61
- 'lambda',
62
- '.lambda.',
63
- '-lambda.',
64
- ];
65
- const isHandlerName = handlerPatterns.some((p) => fileName.includes(p));
140
+
141
+ const isHandlerName = HANDLER_NAME_PATTERNS.some((pattern: string) =>
142
+ fileName.includes(pattern)
143
+ );
66
144
  const isHandlerPath = /\/(handlers|lambdas|lambda|functions)\//i.test(file);
67
145
  const hasHandlerExport = (exports || []).some(
68
- (e) =>
69
- ['handler', 'main', 'lambdahandler'].includes(e.name.toLowerCase()) ||
70
- e.name.toLowerCase().endsWith('handler')
146
+ (exp: any) =>
147
+ ['handler', 'main', 'lambdahandler'].includes(exp.name.toLowerCase()) ||
148
+ exp.name.toLowerCase().endsWith('handler')
71
149
  );
72
150
  return isHandlerName || isHandlerPath || hasHandlerExport;
73
151
  }
74
152
 
75
153
  /**
76
- * Detect if a file is a service file
154
+ * Detect if a file is a service file.
155
+ *
156
+ * @param node - The dependency node to analyze.
157
+ * @returns True if the file orchestrates logic or matches service patterns.
158
+ * @lastUpdated 2026-03-18
77
159
  */
78
160
  export function isServiceFile(node: DependencyNode): boolean {
79
161
  const { file, exports } = node;
80
162
  const fileName = file.split('/').pop()?.toLowerCase() || '';
81
- const servicePatterns = ['service', '.service.', '-service.', '_service.'];
82
- const isServiceName = servicePatterns.some((p) => fileName.includes(p));
163
+
164
+ const isServiceName = SERVICE_NAME_PATTERNS.some((pattern: string) =>
165
+ fileName.includes(pattern)
166
+ );
83
167
  const isServicePath = file.toLowerCase().includes('/services/');
84
- const hasServiceNamedExport = (exports || []).some((e) =>
85
- 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'
86
173
  );
87
- const hasClassExport = (exports || []).some((e) => e.type === 'class');
88
174
  return (
89
175
  isServiceName || isServicePath || (hasServiceNamedExport && hasClassExport)
90
176
  );
91
177
  }
92
178
 
93
179
  /**
94
- * Detect if a file is an email template/layout
180
+ * Detect if a file is an email template/layout.
181
+ *
182
+ * @param node - The dependency node to analyze.
183
+ * @returns True if the file is used for rendering notifications or emails.
184
+ * @lastUpdated 2026-03-18
95
185
  */
96
186
  export function isEmailTemplate(node: DependencyNode): boolean {
97
187
  const { file, exports } = node;
98
188
  const fileName = file.split('/').pop()?.toLowerCase() || '';
99
- const emailPatterns = [
100
- '-email-',
101
- '.email.',
102
- '_email_',
103
- '-template',
104
- '.template.',
105
- '_template',
106
- '-mail.',
107
- '.mail.',
108
- ];
109
- const isEmailName = emailPatterns.some((p) => fileName.includes(p));
189
+
190
+ const isEmailName = EMAIL_NAME_PATTERNS.some((pattern: string) =>
191
+ fileName.includes(pattern)
192
+ );
110
193
  const isEmailPath = /\/(emails|mail|notifications)\//i.test(file);
111
194
  const hasTemplateFunction = (exports || []).some(
112
- (e) =>
113
- e.type === 'function' &&
114
- (e.name.toLowerCase().startsWith('render') ||
115
- e.name.toLowerCase().startsWith('generate'))
195
+ (exp: any) =>
196
+ exp.type === 'function' &&
197
+ (exp.name.toLowerCase().startsWith('render') ||
198
+ exp.name.toLowerCase().startsWith('generate'))
116
199
  );
117
200
  return isEmailPath || isEmailName || hasTemplateFunction;
118
201
  }
119
202
 
120
203
  /**
121
- * Detect if a file is a parser/transformer
204
+ * Detect if a file is a parser/transformer.
205
+ *
206
+ * @param node - The dependency node to analyze.
207
+ * @returns True if the file handles data conversion or serialization.
208
+ * @lastUpdated 2026-03-18
122
209
  */
123
210
  export function isParserFile(node: DependencyNode): boolean {
124
211
  const { file, exports } = node;
125
212
  const fileName = file.split('/').pop()?.toLowerCase() || '';
126
- const parserPatterns = [
127
- 'parser',
128
- '.parser.',
129
- '-parser.',
130
- '_parser.',
131
- 'transform',
132
- 'converter',
133
- 'mapper',
134
- 'serializer',
135
- ];
136
- const isParserName = parserPatterns.some((p) => fileName.includes(p));
213
+
214
+ const isParserName = PARSER_NAME_PATTERNS.some((pattern: string) =>
215
+ fileName.includes(pattern)
216
+ );
137
217
  const isParserPath = /\/(parsers|transformers)\//i.test(file);
138
218
  const hasParseFunction = (exports || []).some(
139
- (e) =>
140
- e.type === 'function' &&
141
- (e.name.toLowerCase().startsWith('parse') ||
142
- e.name.toLowerCase().startsWith('transform'))
219
+ (exp: any) =>
220
+ exp.type === 'function' &&
221
+ (exp.name.toLowerCase().startsWith('parse') ||
222
+ exp.name.toLowerCase().startsWith('transform'))
143
223
  );
144
224
  return isParserName || isParserPath || hasParseFunction;
145
225
  }
146
226
 
147
227
  /**
148
- * Detect if a file is a session/state management file
228
+ * Detect if a file is a session/state management file.
229
+ *
230
+ * @param node - The dependency node to analyze.
231
+ * @returns True if the file manages application state or sessions.
232
+ * @lastUpdated 2026-03-18
149
233
  */
150
234
  export function isSessionFile(node: DependencyNode): boolean {
151
235
  const { file, exports } = node;
152
236
  const fileName = file.split('/').pop()?.toLowerCase() || '';
153
- const sessionPatterns = ['session', 'state', 'context', 'store'];
154
- const isSessionName = sessionPatterns.some((p) => fileName.includes(p));
237
+
238
+ const isSessionName = SESSION_NAME_PATTERNS.some((pattern: string) =>
239
+ fileName.includes(pattern)
240
+ );
155
241
  const isSessionPath = /\/(sessions|state)\//i.test(file);
156
- const hasSessionExport = (exports || []).some((e) =>
157
- ['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
+ )
158
246
  );
159
247
  return isSessionName || isSessionPath || hasSessionExport;
160
248
  }
161
249
 
162
250
  /**
163
- * Detect if a file is a Next.js App Router page
251
+ * Detect if a file is a Next.js App Router page.
252
+ *
253
+ * @param node - The dependency node to analyze.
254
+ * @returns True if the file is a Next.js page or metadata entry.
255
+ * @lastUpdated 2026-03-18
164
256
  */
165
257
  export function isNextJsPage(node: DependencyNode): boolean {
166
258
  const { file, exports } = node;
@@ -172,45 +264,52 @@ export function isNextJsPage(node: DependencyNode): boolean {
172
264
  if (!isInAppDir || (fileName !== 'page.tsx' && fileName !== 'page.ts'))
173
265
  return false;
174
266
 
175
- const hasDefaultExport = (exports || []).some((e) => e.type === 'default');
176
- const nextJsExports = [
177
- 'metadata',
178
- 'generatemetadata',
179
- 'faqjsonld',
180
- 'jsonld',
181
- 'icon',
182
- ];
183
- const hasNextJsExport = (exports || []).some((e) =>
184
- 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())
185
273
  );
186
274
 
187
275
  return hasDefaultExport || hasNextJsExport;
188
276
  }
189
277
 
190
278
  /**
191
- * Detect if a file is a configuration or schema file
279
+ * Detect if a file is a configuration or schema file.
280
+ *
281
+ * @param node - The dependency node to analyze.
282
+ * @returns True if the file matches configuration, setting, or schema patterns.
283
+ * @lastUpdated 2026-03-18
192
284
  */
193
285
  export function isConfigFile(node: DependencyNode): boolean {
194
286
  const { file, exports } = node;
195
287
  const lowerPath = file.toLowerCase();
196
288
  const fileName = file.split('/').pop()?.toLowerCase() || '';
197
289
 
198
- const configPatterns = [
199
- '.config.',
200
- 'tsconfig',
201
- 'jest.config',
202
- 'package.json',
203
- 'aiready.json',
204
- 'next.config',
205
- 'sst.config',
206
- ];
207
- const isConfigName = configPatterns.some((p) => fileName.includes(p));
290
+ const isConfigName = CONFIG_NAME_PATTERNS.some((pattern: string) =>
291
+ fileName.includes(pattern)
292
+ );
208
293
  const isConfigPath = /\/(config|settings|schemas)\//i.test(lowerPath);
209
- const hasSchemaExport = (exports || []).some((e) =>
210
- ['schema', 'config', 'setting'].some((p) =>
211
- 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)
212
297
  )
213
298
  );
214
299
 
215
300
  return isConfigName || isConfigPath || hasSchemaExport;
216
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
+ }