@aiready/context-analyzer 0.21.5 → 0.21.7

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 (163) 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 +26 -24
  8. package/.turbo/turbo-lint.log +5 -6
  9. package/.turbo/turbo-test.log +41 -119
  10. package/dist/__tests__/analyzer.test.js +55 -14
  11. package/dist/__tests__/analyzer.test.js.map +1 -1
  12. package/dist/__tests__/cluster-detector.test.d.ts +2 -0
  13. package/dist/__tests__/cluster-detector.test.d.ts.map +1 -0
  14. package/dist/__tests__/cluster-detector.test.js +121 -0
  15. package/dist/__tests__/cluster-detector.test.js.map +1 -0
  16. package/dist/__tests__/contract.test.d.ts +2 -0
  17. package/dist/__tests__/contract.test.d.ts.map +1 -0
  18. package/dist/__tests__/contract.test.js +59 -0
  19. package/dist/__tests__/contract.test.js.map +1 -0
  20. package/dist/__tests__/enhanced-cohesion.test.js +12 -2
  21. package/dist/__tests__/enhanced-cohesion.test.js.map +1 -1
  22. package/dist/__tests__/file-classification.test.d.ts +2 -0
  23. package/dist/__tests__/file-classification.test.d.ts.map +1 -0
  24. package/dist/__tests__/file-classification.test.js +749 -0
  25. package/dist/__tests__/file-classification.test.js.map +1 -0
  26. package/dist/__tests__/fragmentation-advanced.test.js +2 -8
  27. package/dist/__tests__/fragmentation-advanced.test.js.map +1 -1
  28. package/dist/__tests__/fragmentation-coupling.test.js +2 -2
  29. package/dist/__tests__/fragmentation-coupling.test.js.map +1 -1
  30. package/dist/__tests__/fragmentation-log.test.js +3 -7
  31. package/dist/__tests__/fragmentation-log.test.js.map +1 -1
  32. package/dist/__tests__/provider.test.d.ts +2 -0
  33. package/dist/__tests__/provider.test.d.ts.map +1 -0
  34. package/dist/__tests__/provider.test.js +72 -0
  35. package/dist/__tests__/provider.test.js.map +1 -0
  36. package/dist/__tests__/remediation.test.d.ts +2 -0
  37. package/dist/__tests__/remediation.test.d.ts.map +1 -0
  38. package/dist/__tests__/remediation.test.js +61 -0
  39. package/dist/__tests__/remediation.test.js.map +1 -0
  40. package/dist/__tests__/scoring.test.js +196 -16
  41. package/dist/__tests__/scoring.test.js.map +1 -1
  42. package/dist/__tests__/structural-cohesion.test.js +8 -2
  43. package/dist/__tests__/structural-cohesion.test.js.map +1 -1
  44. package/dist/analyzer.d.ts +31 -94
  45. package/dist/analyzer.d.ts.map +1 -1
  46. package/dist/analyzer.js +260 -678
  47. package/dist/analyzer.js.map +1 -1
  48. package/dist/analyzers/python-context.d.ts.map +1 -1
  49. package/dist/analyzers/python-context.js +10 -8
  50. package/dist/analyzers/python-context.js.map +1 -1
  51. package/dist/ast-utils.d.ts +16 -0
  52. package/dist/ast-utils.d.ts.map +1 -0
  53. package/dist/ast-utils.js +81 -0
  54. package/dist/ast-utils.js.map +1 -0
  55. package/dist/chunk-2HE27YEV.mjs +1739 -0
  56. package/dist/chunk-64U3PNO3.mjs +94 -0
  57. package/dist/chunk-CDIVYADN.mjs +2110 -0
  58. package/dist/chunk-D25B5LZR.mjs +1739 -0
  59. package/dist/chunk-D3SIHB2V.mjs +2118 -0
  60. package/dist/chunk-FNPSK3CG.mjs +1760 -0
  61. package/dist/chunk-GXTGOLZT.mjs +92 -0
  62. package/dist/chunk-KDUUZQBK.mjs +1692 -0
  63. package/dist/chunk-KWIS5FQP.mjs +1739 -0
  64. package/dist/chunk-LERPI33Y.mjs +2060 -0
  65. package/dist/chunk-MZP3G7TF.mjs +2118 -0
  66. package/dist/chunk-NOHK5DLU.mjs +2173 -0
  67. package/dist/chunk-ORLC5Y4J.mjs +1787 -0
  68. package/dist/chunk-OTCQL7DY.mjs +2045 -0
  69. package/dist/chunk-RRB2C34Q.mjs +1738 -0
  70. package/dist/chunk-SFK6XTJE.mjs +2110 -0
  71. package/dist/chunk-U5R2FTCR.mjs +1803 -0
  72. package/dist/chunk-UU4HZ7ZT.mjs +1849 -0
  73. package/dist/chunk-WKOZOHOU.mjs +2060 -0
  74. package/dist/chunk-XIXAWCMS.mjs +1760 -0
  75. package/dist/chunk-XTAXUNQN.mjs +1742 -0
  76. package/dist/classifier.d.ts +114 -0
  77. package/dist/classifier.d.ts.map +1 -0
  78. package/dist/classifier.js +439 -0
  79. package/dist/classifier.js.map +1 -0
  80. package/dist/cli.js +681 -1170
  81. package/dist/cli.js.map +1 -1
  82. package/dist/cli.mjs +63 -533
  83. package/dist/cluster-detector.d.ts +8 -0
  84. package/dist/cluster-detector.d.ts.map +1 -0
  85. package/dist/cluster-detector.js +70 -0
  86. package/dist/cluster-detector.js.map +1 -0
  87. package/dist/defaults.d.ts +7 -0
  88. package/dist/defaults.d.ts.map +1 -0
  89. package/dist/defaults.js +54 -0
  90. package/dist/defaults.js.map +1 -0
  91. package/dist/graph-builder.d.ts +33 -0
  92. package/dist/graph-builder.d.ts.map +1 -0
  93. package/dist/graph-builder.js +225 -0
  94. package/dist/graph-builder.js.map +1 -0
  95. package/dist/index.d.mts +93 -106
  96. package/dist/index.d.ts +93 -106
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +932 -745
  99. package/dist/index.js.map +1 -1
  100. package/dist/index.mjs +262 -28
  101. package/dist/metrics.d.ts +34 -0
  102. package/dist/metrics.d.ts.map +1 -0
  103. package/dist/metrics.js +170 -0
  104. package/dist/metrics.js.map +1 -0
  105. package/dist/provider.d.ts +6 -0
  106. package/dist/provider.d.ts.map +1 -0
  107. package/dist/provider.js +48 -0
  108. package/dist/provider.js.map +1 -0
  109. package/dist/python-context-3GZKN3LR.mjs +162 -0
  110. package/dist/python-context-O2EN3M6Z.mjs +162 -0
  111. package/dist/remediation.d.ts +25 -0
  112. package/dist/remediation.d.ts.map +1 -0
  113. package/dist/remediation.js +98 -0
  114. package/dist/remediation.js.map +1 -0
  115. package/dist/scoring.d.ts +3 -7
  116. package/dist/scoring.d.ts.map +1 -1
  117. package/dist/scoring.js +57 -48
  118. package/dist/scoring.js.map +1 -1
  119. package/dist/semantic-analysis.d.ts +12 -23
  120. package/dist/semantic-analysis.d.ts.map +1 -1
  121. package/dist/semantic-analysis.js +172 -110
  122. package/dist/semantic-analysis.js.map +1 -1
  123. package/dist/summary.d.ts +6 -0
  124. package/dist/summary.d.ts.map +1 -0
  125. package/dist/summary.js +92 -0
  126. package/dist/summary.js.map +1 -0
  127. package/dist/types.d.ts +9 -2
  128. package/dist/types.d.ts.map +1 -1
  129. package/dist/utils/output-formatter.d.ts +14 -0
  130. package/dist/utils/output-formatter.d.ts.map +1 -0
  131. package/dist/utils/output-formatter.js +338 -0
  132. package/dist/utils/output-formatter.js.map +1 -0
  133. package/package.json +2 -2
  134. package/src/__tests__/analyzer.test.ts +1 -1
  135. package/src/__tests__/auto-detection.test.ts +1 -1
  136. package/src/__tests__/contract.test.ts +1 -1
  137. package/src/__tests__/enhanced-cohesion.test.ts +1 -1
  138. package/src/__tests__/file-classification.test.ts +1 -1
  139. package/src/__tests__/fragmentation-advanced.test.ts +1 -1
  140. package/src/__tests__/fragmentation-coupling.test.ts +1 -1
  141. package/src/__tests__/fragmentation-log.test.ts +1 -1
  142. package/src/__tests__/provider.test.ts +1 -1
  143. package/src/__tests__/structural-cohesion.test.ts +1 -1
  144. package/src/analyzer.ts +112 -317
  145. package/src/analyzers/python-context.ts +7 -76
  146. package/src/ast-utils.ts +2 -2
  147. package/src/classifier.ts +13 -328
  148. package/src/cli-action.ts +110 -0
  149. package/src/cli.ts +3 -701
  150. package/src/cluster-detector.ts +28 -1
  151. package/src/defaults.ts +3 -0
  152. package/src/graph-builder.ts +10 -91
  153. package/src/heuristics.ts +216 -0
  154. package/src/index.ts +6 -0
  155. package/src/issue-analyzer.ts +158 -0
  156. package/src/metrics.ts +9 -0
  157. package/src/scoring.ts +3 -5
  158. package/src/semantic-analysis.ts +8 -14
  159. package/src/summary.ts +62 -106
  160. package/src/types.ts +52 -20
  161. package/src/utils/dependency-graph-utils.ts +126 -0
  162. package/src/utils/output-formatter.ts +411 -0
  163. package/src/utils/string-utils.ts +21 -0
@@ -3,6 +3,10 @@ import { calculateFragmentation, calculateEnhancedCohesion } from './metrics';
3
3
 
4
4
  /**
5
5
  * Group files by domain to detect module clusters
6
+ * @param graph - The dependency graph to analyze
7
+ * @param options - Optional configuration options
8
+ * @param options.useLogScale - Whether to use logarithmic scaling for calculations
9
+ * @returns Array of module clusters
6
10
  */
7
11
  export function detectModuleClusters(
8
12
  graph: DependencyGraph,
@@ -20,6 +24,29 @@ export function detectModuleClusters(
20
24
 
21
25
  const clusters: ModuleCluster[] = [];
22
26
 
27
+ const generateSuggestedStructure = (
28
+ files: string[],
29
+ tokens: number,
30
+ fragmentation: number
31
+ ) => {
32
+ const targetFiles = Math.max(1, Math.ceil(tokens / 10000));
33
+ const plan: string[] = [];
34
+
35
+ if (fragmentation > 0.5) {
36
+ plan.push(
37
+ `Consolidate ${files.length} files scattered across multiple directories into ${targetFiles} core module(s)`
38
+ );
39
+ }
40
+
41
+ if (tokens > 20000) {
42
+ plan.push(
43
+ `Domain logic is very large (${Math.round(tokens / 1000)}k tokens). Ensure clear sub-domain boundaries.`
44
+ );
45
+ }
46
+
47
+ return { targetFiles, consolidationPlan: plan };
48
+ };
49
+
23
50
  for (const [domain, files] of domainMap.entries()) {
24
51
  if (files.length < 2 || domain === 'unknown') continue;
25
52
 
@@ -35,7 +62,7 @@ export function detectModuleClusters(
35
62
  (f) => new Set(graph.nodes.get(f)?.imports || [])
36
63
  );
37
64
  let intersection = new Set(allImportSets[0]);
38
- let union = new Set(allImportSets[0]);
65
+ const union = new Set(allImportSets[0]);
39
66
 
40
67
  for (let i = 1; i < allImportSets.length; i++) {
41
68
  const nextSet = allImportSets[i];
package/src/defaults.ts CHANGED
@@ -4,6 +4,9 @@ import type { ContextAnalyzerOptions } from './types';
4
4
  /**
5
5
  * Generate smart defaults for context analysis based on repository size
6
6
  * Automatically tunes thresholds to target ~10 most serious issues
7
+ * @param directory - The root directory to analyze
8
+ * @param userOptions - Partial user-provided options to merge with defaults
9
+ * @returns Complete ContextAnalyzerOptions with smart defaults
7
10
  */
8
11
  export async function getSmartDefaults(
9
12
  directory: string,
@@ -1,4 +1,10 @@
1
- import { estimateTokens, parseFileExports } from '@aiready/core';
1
+ import { estimateTokens, parseFileExports, FileContent } from '@aiready/core';
2
+ import { singularize } from './utils/string-utils';
3
+ import {
4
+ calculateImportDepthFromEdges,
5
+ detectGraphCycles,
6
+ getTransitiveDependenciesFromEdges,
7
+ } from './utils/dependency-graph-utils';
2
8
  import type { DependencyGraph, DependencyNode } from './types';
3
9
  import {
4
10
  buildCoUsageMatrix,
@@ -8,11 +14,6 @@ import {
8
14
  import { extractExportsWithAST } from './ast-utils';
9
15
  import { join, dirname, normalize } from 'path';
10
16
 
11
- interface FileContent {
12
- file: string;
13
- content: string;
14
- }
15
-
16
17
  /**
17
18
  * Resolve an import source to its absolute path considering the importing file's location
18
19
  */
@@ -94,25 +95,6 @@ export function extractDomainKeywordsFromPaths(files: FileContent[]): string[] {
94
95
  return Array.from(folderNames);
95
96
  }
96
97
 
97
- /**
98
- * Simple singularization for common English plurals
99
- */
100
- function singularize(word: string): string {
101
- const irregulars: Record<string, string> = {
102
- people: 'person',
103
- children: 'child',
104
- men: 'man',
105
- women: 'woman',
106
- };
107
-
108
- if (irregulars[word]) return irregulars[word];
109
- if (word.endsWith('ies')) return word.slice(0, -3) + 'y';
110
- if (word.endsWith('ses')) return word.slice(0, -2);
111
- if (word.endsWith('s') && word.length > 3) return word.slice(0, -1);
112
-
113
- return word;
114
- }
115
-
116
98
  /**
117
99
  * Build a dependency graph from file contents
118
100
  */
@@ -196,23 +178,7 @@ export function calculateImportDepth(
196
178
  visited = new Set<string>(),
197
179
  depth = 0
198
180
  ): number {
199
- if (visited.has(file)) return depth;
200
-
201
- const dependencies = graph.edges.get(file);
202
- if (!dependencies || dependencies.size === 0) return depth;
203
-
204
- visited.add(file);
205
- let maxDepth = depth;
206
-
207
- for (const dep of dependencies) {
208
- maxDepth = Math.max(
209
- maxDepth,
210
- calculateImportDepth(dep, graph, visited, depth + 1)
211
- );
212
- }
213
-
214
- visited.delete(file);
215
- return maxDepth;
181
+ return calculateImportDepthFromEdges(file, graph.edges, visited, depth);
216
182
  }
217
183
 
218
184
  /**
@@ -223,19 +189,7 @@ export function getTransitiveDependencies(
223
189
  graph: DependencyGraph,
224
190
  visited = new Set<string>()
225
191
  ): string[] {
226
- if (visited.has(file)) return [];
227
-
228
- visited.add(file);
229
- const dependencies = graph.edges.get(file);
230
- if (!dependencies || dependencies.size === 0) return [];
231
-
232
- const allDeps: string[] = [];
233
- for (const dep of dependencies) {
234
- allDeps.push(dep);
235
- allDeps.push(...getTransitiveDependencies(dep, graph, visited));
236
- }
237
-
238
- return [...new Set(allDeps)];
192
+ return getTransitiveDependenciesFromEdges(file, graph.edges, visited);
239
193
  }
240
194
 
241
195
  /**
@@ -265,40 +219,5 @@ export function calculateContextBudget(
265
219
  * Detect circular dependencies
266
220
  */
267
221
  export function detectCircularDependencies(graph: DependencyGraph): string[][] {
268
- const cycles: string[][] = [];
269
- const visited = new Set<string>();
270
- const recursionStack = new Set<string>();
271
-
272
- function dfs(file: string, path: string[]): void {
273
- if (recursionStack.has(file)) {
274
- const cycleStart = path.indexOf(file);
275
- if (cycleStart !== -1) {
276
- cycles.push([...path.slice(cycleStart), file]);
277
- }
278
- return;
279
- }
280
-
281
- if (visited.has(file)) return;
282
-
283
- visited.add(file);
284
- recursionStack.add(file);
285
- path.push(file);
286
-
287
- const dependencies = graph.edges.get(file);
288
- if (dependencies) {
289
- for (const dep of dependencies) {
290
- dfs(dep, [...path]);
291
- }
292
- }
293
-
294
- recursionStack.delete(file);
295
- }
296
-
297
- for (const file of graph.nodes.keys()) {
298
- if (!visited.has(file)) {
299
- dfs(file, []);
300
- }
301
- }
302
-
303
- return cycles;
222
+ return detectGraphCycles(graph.edges);
304
223
  }
@@ -0,0 +1,216 @@
1
+ import { DependencyNode } from './types';
2
+
3
+ /**
4
+ * Detect if a file is a barrel export (index.ts)
5
+ */
6
+ export function isBarrelExport(node: DependencyNode): boolean {
7
+ const { file, exports } = node;
8
+ const fileName = file.split('/').pop()?.toLowerCase();
9
+
10
+ const isIndexFile = fileName === 'index.ts' || fileName === 'index.js';
11
+ const isSmallAndManyExports =
12
+ node.tokenCost < 1000 && (exports || []).length > 5;
13
+
14
+ const isReexportPattern =
15
+ (exports || []).length >= 5 &&
16
+ (exports || []).every((e) =>
17
+ ['const', 'function', 'type', 'interface'].includes(e.type)
18
+ );
19
+
20
+ return !!isIndexFile || !!isSmallAndManyExports || !!isReexportPattern;
21
+ }
22
+
23
+ /**
24
+ * Detect if a file is primarily type definitions
25
+ */
26
+ export function isTypeDefinition(node: DependencyNode): boolean {
27
+ const { file } = node;
28
+ if (file.endsWith('.d.ts')) return true;
29
+
30
+ const nodeExports = node.exports || [];
31
+ const hasExports = nodeExports.length > 0;
32
+ const areAllTypes =
33
+ hasExports &&
34
+ nodeExports.every((e) => e.type === 'type' || e.type === 'interface');
35
+
36
+ const isTypePath = /\/(types|interfaces|models)\//i.test(file);
37
+ return !!areAllTypes || (isTypePath && hasExports);
38
+ }
39
+
40
+ /**
41
+ * Detect if a file is a utility module
42
+ */
43
+ export function isUtilityModule(node: DependencyNode): boolean {
44
+ const { file } = node;
45
+ const isUtilPath = /\/(utils|helpers|util|helper)\//i.test(file);
46
+ const fileName = file.split('/').pop()?.toLowerCase() || '';
47
+ const isUtilName = /(utils\.|helpers\.|util\.|helper\.)/i.test(fileName);
48
+ return isUtilPath || isUtilName;
49
+ }
50
+
51
+ /**
52
+ * Detect if a file is a Lambda/API handler
53
+ */
54
+ export function isLambdaHandler(node: DependencyNode): boolean {
55
+ const { file, exports } = node;
56
+ 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));
66
+ const isHandlerPath = /\/(handlers|lambdas|lambda|functions)\//i.test(file);
67
+ const hasHandlerExport = (exports || []).some(
68
+ (e) =>
69
+ ['handler', 'main', 'lambdahandler'].includes(e.name.toLowerCase()) ||
70
+ e.name.toLowerCase().endsWith('handler')
71
+ );
72
+ return isHandlerName || isHandlerPath || hasHandlerExport;
73
+ }
74
+
75
+ /**
76
+ * Detect if a file is a service file
77
+ */
78
+ export function isServiceFile(node: DependencyNode): boolean {
79
+ const { file, exports } = node;
80
+ const fileName = file.split('/').pop()?.toLowerCase() || '';
81
+ const servicePatterns = ['service', '.service.', '-service.', '_service.'];
82
+ const isServiceName = servicePatterns.some((p) => fileName.includes(p));
83
+ const isServicePath = file.toLowerCase().includes('/services/');
84
+ const hasServiceNamedExport = (exports || []).some((e) =>
85
+ e.name.toLowerCase().includes('service')
86
+ );
87
+ const hasClassExport = (exports || []).some((e) => e.type === 'class');
88
+ return (
89
+ isServiceName || isServicePath || (hasServiceNamedExport && hasClassExport)
90
+ );
91
+ }
92
+
93
+ /**
94
+ * Detect if a file is an email template/layout
95
+ */
96
+ export function isEmailTemplate(node: DependencyNode): boolean {
97
+ const { file, exports } = node;
98
+ 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));
110
+ const isEmailPath = /\/(emails|mail|notifications)\//i.test(file);
111
+ const hasTemplateFunction = (exports || []).some(
112
+ (e) =>
113
+ e.type === 'function' &&
114
+ (e.name.toLowerCase().startsWith('render') ||
115
+ e.name.toLowerCase().startsWith('generate'))
116
+ );
117
+ return isEmailPath || isEmailName || hasTemplateFunction;
118
+ }
119
+
120
+ /**
121
+ * Detect if a file is a parser/transformer
122
+ */
123
+ export function isParserFile(node: DependencyNode): boolean {
124
+ const { file, exports } = node;
125
+ 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));
137
+ const isParserPath = /\/(parsers|transformers)\//i.test(file);
138
+ const hasParseFunction = (exports || []).some(
139
+ (e) =>
140
+ e.type === 'function' &&
141
+ (e.name.toLowerCase().startsWith('parse') ||
142
+ e.name.toLowerCase().startsWith('transform'))
143
+ );
144
+ return isParserName || isParserPath || hasParseFunction;
145
+ }
146
+
147
+ /**
148
+ * Detect if a file is a session/state management file
149
+ */
150
+ export function isSessionFile(node: DependencyNode): boolean {
151
+ const { file, exports } = node;
152
+ const fileName = file.split('/').pop()?.toLowerCase() || '';
153
+ const sessionPatterns = ['session', 'state', 'context', 'store'];
154
+ const isSessionName = sessionPatterns.some((p) => fileName.includes(p));
155
+ const isSessionPath = /\/(sessions|state)\//i.test(file);
156
+ const hasSessionExport = (exports || []).some((e) =>
157
+ ['session', 'state', 'store'].some((p) => e.name.toLowerCase().includes(p))
158
+ );
159
+ return isSessionName || isSessionPath || hasSessionExport;
160
+ }
161
+
162
+ /**
163
+ * Detect if a file is a Next.js App Router page
164
+ */
165
+ export function isNextJsPage(node: DependencyNode): boolean {
166
+ const { file, exports } = node;
167
+ const lowerPath = file.toLowerCase();
168
+ const fileName = file.split('/').pop()?.toLowerCase() || '';
169
+
170
+ const isInAppDir =
171
+ lowerPath.includes('/app/') || lowerPath.startsWith('app/');
172
+ if (!isInAppDir || (fileName !== 'page.tsx' && fileName !== 'page.ts'))
173
+ return false;
174
+
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())
185
+ );
186
+
187
+ return hasDefaultExport || hasNextJsExport;
188
+ }
189
+
190
+ /**
191
+ * Detect if a file is a configuration or schema file
192
+ */
193
+ export function isConfigFile(node: DependencyNode): boolean {
194
+ const { file, exports } = node;
195
+ const lowerPath = file.toLowerCase();
196
+ const fileName = file.split('/').pop()?.toLowerCase() || '';
197
+
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));
208
+ 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)
212
+ )
213
+ );
214
+
215
+ return isConfigName || isConfigPath || hasSchemaExport;
216
+ }
package/src/index.ts CHANGED
@@ -5,9 +5,15 @@ import { ContextAnalyzerProvider } from './provider';
5
5
  ToolRegistry.register(ContextAnalyzerProvider);
6
6
 
7
7
  export * from './analyzer';
8
+ export * from './graph-builder';
9
+ export * from './metrics';
10
+ export * from './classifier';
11
+ export * from './cluster-detector';
12
+ export * from './remediation';
8
13
  export * from './scoring';
9
14
  export * from './defaults';
10
15
  export * from './summary';
11
16
  export * from './types';
12
17
  export * from './semantic-analysis';
13
18
  export { ContextAnalyzerProvider };
19
+ export * from './utils/output-formatter';
@@ -0,0 +1,158 @@
1
+ import { Severity } from '@aiready/core';
2
+
3
+ /**
4
+ * Internal issue analysis logic
5
+ */
6
+ /**
7
+ * Analyzes architectural issues related to AI context windows.
8
+ * Detects problems like high import depth, large context budgets,
9
+ * circular dependencies, and low cohesion.
10
+ *
11
+ * @param params - Combined metrics and graph data for a module
12
+ * @returns List of identified issues with severity and suggestions
13
+ */
14
+ export function analyzeIssues(params: {
15
+ file: string;
16
+ importDepth: number;
17
+ contextBudget: number;
18
+ cohesionScore: number;
19
+ fragmentationScore: number;
20
+ maxDepth: number;
21
+ maxContextBudget: number;
22
+ minCohesion: number;
23
+ maxFragmentation: number;
24
+ circularDeps: string[][];
25
+ }): {
26
+ severity: Severity;
27
+ issues: string[];
28
+ recommendations: string[];
29
+ potentialSavings: number;
30
+ } {
31
+ const {
32
+ file,
33
+ importDepth,
34
+ contextBudget,
35
+ cohesionScore,
36
+ fragmentationScore,
37
+ maxDepth,
38
+ maxContextBudget,
39
+ minCohesion,
40
+ maxFragmentation,
41
+ circularDeps,
42
+ } = params;
43
+
44
+ const issues: string[] = [];
45
+ const recommendations: string[] = [];
46
+ let severity: Severity = Severity.Info;
47
+ let potentialSavings = 0;
48
+
49
+ // Check circular dependencies (CRITICAL)
50
+ if (circularDeps.length > 0) {
51
+ severity = Severity.Critical;
52
+ issues.push(`Part of ${circularDeps.length} circular dependency chain(s)`);
53
+ recommendations.push(
54
+ 'Break circular dependencies by extracting interfaces or using dependency injection'
55
+ );
56
+ potentialSavings += contextBudget * 0.2;
57
+ }
58
+
59
+ // Check import depth
60
+ if (importDepth > maxDepth * 1.5) {
61
+ severity = Severity.Critical;
62
+ issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
63
+ recommendations.push('Flatten dependency tree or use facade pattern');
64
+ potentialSavings += contextBudget * 0.3;
65
+ } else if (importDepth > maxDepth) {
66
+ if (severity !== Severity.Critical) severity = Severity.Major;
67
+ issues.push(
68
+ `Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`
69
+ );
70
+ recommendations.push('Consider reducing dependency depth');
71
+ potentialSavings += contextBudget * 0.15;
72
+ }
73
+
74
+ // Check context budget
75
+ if (contextBudget > maxContextBudget * 1.5) {
76
+ severity = Severity.Critical;
77
+ issues.push(
78
+ `Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
79
+ );
80
+ recommendations.push(
81
+ 'Split into smaller modules or reduce dependency tree'
82
+ );
83
+ potentialSavings += contextBudget * 0.4;
84
+ } else if (contextBudget > maxContextBudget) {
85
+ if (severity !== Severity.Critical) severity = Severity.Major;
86
+ issues.push(
87
+ `Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
88
+ );
89
+ recommendations.push('Reduce file size or dependencies');
90
+ potentialSavings += contextBudget * 0.2;
91
+ }
92
+
93
+ // Check cohesion
94
+ if (cohesionScore < minCohesion * 0.5) {
95
+ if (severity !== Severity.Critical) severity = Severity.Major;
96
+ issues.push(
97
+ `Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`
98
+ );
99
+ recommendations.push(
100
+ 'Split file by domain - separate unrelated functionality'
101
+ );
102
+ potentialSavings += contextBudget * 0.25;
103
+ } else if (cohesionScore < minCohesion) {
104
+ if (severity === Severity.Info) severity = Severity.Minor;
105
+ issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
106
+ recommendations.push('Consider grouping related exports together');
107
+ potentialSavings += contextBudget * 0.1;
108
+ }
109
+
110
+ // Check fragmentation
111
+ if (fragmentationScore > maxFragmentation) {
112
+ if (severity === Severity.Info || severity === Severity.Minor)
113
+ severity = Severity.Minor;
114
+ issues.push(
115
+ `High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`
116
+ );
117
+ recommendations.push('Consolidate with related files in same domain');
118
+ potentialSavings += contextBudget * 0.3;
119
+ }
120
+
121
+ if (issues.length === 0) {
122
+ issues.push('No significant issues detected');
123
+ recommendations.push('File is well-structured for AI context usage');
124
+ }
125
+
126
+ // Detect build artifacts
127
+ if (isBuildArtifact(file)) {
128
+ issues.push('Detected build artifact (bundled/output file)');
129
+ recommendations.push('Exclude build outputs from analysis');
130
+ severity = Severity.Info;
131
+ potentialSavings = 0;
132
+ }
133
+
134
+ return {
135
+ severity,
136
+ issues,
137
+ recommendations,
138
+ potentialSavings: Math.floor(potentialSavings),
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Determines if a file path belongs to a build artifact or dependency folder.
144
+ * Helps exclude generated files from analysis to prevent false positives.
145
+ *
146
+ * @param filePath - The path to check
147
+ * @returns True if the file is a build artifact, false otherwise
148
+ */
149
+ export function isBuildArtifact(filePath: string): boolean {
150
+ const lower = filePath.toLowerCase();
151
+ return (
152
+ lower.includes('/node_modules/') ||
153
+ lower.includes('/dist/') ||
154
+ lower.includes('/build/') ||
155
+ lower.includes('/out/') ||
156
+ lower.includes('/.next/')
157
+ );
158
+ }
package/src/metrics.ts CHANGED
@@ -5,6 +5,15 @@ import { isTestFile } from './ast-utils';
5
5
  /**
6
6
  * Calculate cohesion score (how related are exports in a file)
7
7
  */
8
+ /**
9
+ * Calculates a cohesion score (0-1) for a module based on its exports,
10
+ * shared imports, and internal structure. High cohesion indicates
11
+ * a well-focused module that is easy for AI models to reason about.
12
+ *
13
+ * @param exports - Exported symbols and their metadata
14
+ * @param imports - Imported symbols and their sources
15
+ * @returns Cohesion score between 0 and 1
16
+ */
8
17
  export function calculateEnhancedCohesion(
9
18
  exports: ExportInfo[],
10
19
  filePath?: string,
package/src/scoring.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  DEFAULT_COST_CONFIG,
5
5
  type CostConfig,
6
6
  ToolName,
7
+ getRatingSlug,
7
8
  } from '@aiready/core';
8
9
  import type { ToolScoringOutput } from '@aiready/core';
9
10
  import type { ContextSummary } from './types';
@@ -190,9 +191,6 @@ export function calculateContextScore(
190
191
  }
191
192
 
192
193
  export function mapScoreToRating(score: number): string {
193
- if (score >= 90) return 'excellent';
194
- if (score >= 75) return 'good';
195
- if (score >= 60) return 'fair';
196
- if (score >= 40) return 'needs work';
197
- return 'critical';
194
+ // Use core implementation to resolve duplication
195
+ return getRatingSlug(score).replace('-', ' ');
198
196
  }
@@ -5,9 +5,12 @@ import type {
5
5
  DomainSignals,
6
6
  ExportInfo,
7
7
  } from './types';
8
+ import { singularize } from './utils/string-utils';
8
9
 
9
10
  /**
10
11
  * Build co-usage matrix: track which files are imported together
12
+ * @param graph - The dependency graph to analyze
13
+ * @returns Map of file to co-usage counts
11
14
  */
12
15
  export function buildCoUsageMatrix(
13
16
  graph: DependencyGraph
@@ -38,6 +41,8 @@ export function buildCoUsageMatrix(
38
41
 
39
42
  /**
40
43
  * Extract type dependencies from AST exports
44
+ * @param graph - The dependency graph to analyze
45
+ * @returns Map of type references to files that use them
41
46
  */
42
47
  export function buildTypeGraph(
43
48
  graph: DependencyGraph
@@ -60,6 +65,9 @@ export function buildTypeGraph(
60
65
 
61
66
  /**
62
67
  * Find semantic clusters using co-usage patterns
68
+ * @param coUsageMatrix - The co-usage matrix from buildCoUsageMatrix
69
+ * @param minCoUsage - Minimum co-usage count to consider (default: 3)
70
+ * @returns Map of cluster representative files to their cluster members
63
71
  */
64
72
  export function findSemanticClusters(
65
73
  coUsageMatrix: Map<string, Map<string, number>>,
@@ -312,20 +320,6 @@ export function inferDomain(
312
320
  return 'unknown';
313
321
  }
314
322
 
315
- function singularize(word: string): string {
316
- const irregulars: Record<string, string> = {
317
- people: 'person',
318
- children: 'child',
319
- men: 'man',
320
- women: 'woman',
321
- };
322
- if (irregulars[word]) return irregulars[word];
323
- if (word.endsWith('ies')) return word.slice(0, -3) + 'y';
324
- if (word.endsWith('ses')) return word.slice(0, -2);
325
- if (word.endsWith('s') && word.length > 3) return word.slice(0, -1);
326
- return word;
327
- }
328
-
329
323
  export function getCoUsageData(
330
324
  file: string,
331
325
  coUsageMatrix: Map<string, Map<string, number>>