@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.
- package/.turbo/turbo-build.log +25 -26
- package/.turbo/turbo-test.log +39 -41
- package/dist/chunk-BW463GQB.mjs +1767 -0
- package/dist/chunk-CAX2MOUZ.mjs +1801 -0
- package/dist/chunk-J5TA3AZU.mjs +1795 -0
- package/dist/chunk-UXC6QUZ7.mjs +1801 -0
- package/dist/chunk-WTQJNY4U.mjs +1785 -0
- package/dist/chunk-XBFM2Z4O.mjs +1792 -0
- package/dist/cli.js +319 -243
- package/dist/cli.mjs +38 -24
- package/dist/index.d.mts +141 -32
- package/dist/index.d.ts +141 -32
- package/dist/index.js +340 -239
- package/dist/index.mjs +55 -16
- package/package.json +2 -2
- package/src/__tests__/cluster-detector.test.ts +5 -8
- package/src/__tests__/fragmentation-coupling.test.ts +6 -3
- package/src/analyzer.ts +1 -224
- package/src/ast-utils.ts +12 -2
- package/src/classifier.ts +11 -0
- package/src/cli-definition.ts +61 -0
- package/src/cli.ts +2 -44
- package/src/cluster-detector.ts +22 -5
- package/src/graph-builder.ts +31 -6
- package/src/heuristics.ts +188 -89
- package/src/mapper.ts +118 -0
- package/src/metrics.ts +34 -20
- package/src/orchestrator.ts +136 -0
- package/src/remediation.ts +15 -1
- package/src/scoring.ts +93 -19
- package/src/semantic-analysis.ts +58 -12
- package/src/summary.ts +4 -0
- package/src/types.ts +1 -0
package/src/graph-builder.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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 <
|
|
80
|
+
node.tokenCost < BARREL_EXPORT_TOKEN_LIMIT &&
|
|
81
|
+
(exports || []).length > BARREL_EXPORT_MIN_EXPORTS;
|
|
13
82
|
|
|
14
83
|
const isReexportPattern =
|
|
15
|
-
(exports || []).length >=
|
|
16
|
-
(exports || []).every((
|
|
17
|
-
['const', 'function', 'type', 'interface'].includes(
|
|
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(
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
(
|
|
69
|
-
['handler', 'main', 'lambdahandler'].includes(
|
|
70
|
-
|
|
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
|
-
|
|
82
|
-
const isServiceName =
|
|
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((
|
|
85
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
(
|
|
113
|
-
|
|
114
|
-
(
|
|
115
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
(
|
|
140
|
-
|
|
141
|
-
(
|
|
142
|
-
|
|
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
|
-
|
|
154
|
-
const isSessionName =
|
|
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((
|
|
157
|
-
['session', 'state', 'store'].some((
|
|
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(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
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((
|
|
210
|
-
['schema', 'config', 'setting'].some((
|
|
211
|
-
|
|
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
|
+
}
|