@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.
- package/.aiready/aiready-report-20260314-222254.json +39216 -0
- package/.aiready/aiready-report-20260314-223947.json +3413 -0
- package/.aiready/aiready-report-20260314-224112.json +3413 -0
- package/.aiready/aiready-report-20260314-224302.json +2973 -0
- package/.aiready/aiready-report-20260314-224939.json +3092 -0
- package/.aiready/aiready-report-20260314-225154.json +3092 -0
- package/.turbo/turbo-build.log +26 -24
- package/.turbo/turbo-lint.log +5 -6
- package/.turbo/turbo-test.log +41 -119
- package/dist/__tests__/analyzer.test.js +55 -14
- package/dist/__tests__/analyzer.test.js.map +1 -1
- package/dist/__tests__/cluster-detector.test.d.ts +2 -0
- package/dist/__tests__/cluster-detector.test.d.ts.map +1 -0
- package/dist/__tests__/cluster-detector.test.js +121 -0
- package/dist/__tests__/cluster-detector.test.js.map +1 -0
- package/dist/__tests__/contract.test.d.ts +2 -0
- package/dist/__tests__/contract.test.d.ts.map +1 -0
- package/dist/__tests__/contract.test.js +59 -0
- package/dist/__tests__/contract.test.js.map +1 -0
- package/dist/__tests__/enhanced-cohesion.test.js +12 -2
- package/dist/__tests__/enhanced-cohesion.test.js.map +1 -1
- package/dist/__tests__/file-classification.test.d.ts +2 -0
- package/dist/__tests__/file-classification.test.d.ts.map +1 -0
- package/dist/__tests__/file-classification.test.js +749 -0
- package/dist/__tests__/file-classification.test.js.map +1 -0
- package/dist/__tests__/fragmentation-advanced.test.js +2 -8
- package/dist/__tests__/fragmentation-advanced.test.js.map +1 -1
- package/dist/__tests__/fragmentation-coupling.test.js +2 -2
- package/dist/__tests__/fragmentation-coupling.test.js.map +1 -1
- package/dist/__tests__/fragmentation-log.test.js +3 -7
- package/dist/__tests__/fragmentation-log.test.js.map +1 -1
- package/dist/__tests__/provider.test.d.ts +2 -0
- package/dist/__tests__/provider.test.d.ts.map +1 -0
- package/dist/__tests__/provider.test.js +72 -0
- package/dist/__tests__/provider.test.js.map +1 -0
- package/dist/__tests__/remediation.test.d.ts +2 -0
- package/dist/__tests__/remediation.test.d.ts.map +1 -0
- package/dist/__tests__/remediation.test.js +61 -0
- package/dist/__tests__/remediation.test.js.map +1 -0
- package/dist/__tests__/scoring.test.js +196 -16
- package/dist/__tests__/scoring.test.js.map +1 -1
- package/dist/__tests__/structural-cohesion.test.js +8 -2
- package/dist/__tests__/structural-cohesion.test.js.map +1 -1
- package/dist/analyzer.d.ts +31 -94
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +260 -678
- package/dist/analyzer.js.map +1 -1
- package/dist/analyzers/python-context.d.ts.map +1 -1
- package/dist/analyzers/python-context.js +10 -8
- package/dist/analyzers/python-context.js.map +1 -1
- package/dist/ast-utils.d.ts +16 -0
- package/dist/ast-utils.d.ts.map +1 -0
- package/dist/ast-utils.js +81 -0
- package/dist/ast-utils.js.map +1 -0
- package/dist/chunk-2HE27YEV.mjs +1739 -0
- package/dist/chunk-64U3PNO3.mjs +94 -0
- package/dist/chunk-CDIVYADN.mjs +2110 -0
- package/dist/chunk-D25B5LZR.mjs +1739 -0
- package/dist/chunk-D3SIHB2V.mjs +2118 -0
- package/dist/chunk-FNPSK3CG.mjs +1760 -0
- package/dist/chunk-GXTGOLZT.mjs +92 -0
- package/dist/chunk-KDUUZQBK.mjs +1692 -0
- package/dist/chunk-KWIS5FQP.mjs +1739 -0
- package/dist/chunk-LERPI33Y.mjs +2060 -0
- package/dist/chunk-MZP3G7TF.mjs +2118 -0
- package/dist/chunk-NOHK5DLU.mjs +2173 -0
- package/dist/chunk-ORLC5Y4J.mjs +1787 -0
- package/dist/chunk-OTCQL7DY.mjs +2045 -0
- package/dist/chunk-RRB2C34Q.mjs +1738 -0
- package/dist/chunk-SFK6XTJE.mjs +2110 -0
- package/dist/chunk-U5R2FTCR.mjs +1803 -0
- package/dist/chunk-UU4HZ7ZT.mjs +1849 -0
- package/dist/chunk-WKOZOHOU.mjs +2060 -0
- package/dist/chunk-XIXAWCMS.mjs +1760 -0
- package/dist/chunk-XTAXUNQN.mjs +1742 -0
- package/dist/classifier.d.ts +114 -0
- package/dist/classifier.d.ts.map +1 -0
- package/dist/classifier.js +439 -0
- package/dist/classifier.js.map +1 -0
- package/dist/cli.js +681 -1170
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +63 -533
- package/dist/cluster-detector.d.ts +8 -0
- package/dist/cluster-detector.d.ts.map +1 -0
- package/dist/cluster-detector.js +70 -0
- package/dist/cluster-detector.js.map +1 -0
- package/dist/defaults.d.ts +7 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +54 -0
- package/dist/defaults.js.map +1 -0
- package/dist/graph-builder.d.ts +33 -0
- package/dist/graph-builder.d.ts.map +1 -0
- package/dist/graph-builder.js +225 -0
- package/dist/graph-builder.js.map +1 -0
- package/dist/index.d.mts +93 -106
- package/dist/index.d.ts +93 -106
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +932 -745
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +262 -28
- package/dist/metrics.d.ts +34 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +170 -0
- package/dist/metrics.js.map +1 -0
- package/dist/provider.d.ts +6 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +48 -0
- package/dist/provider.js.map +1 -0
- package/dist/python-context-3GZKN3LR.mjs +162 -0
- package/dist/python-context-O2EN3M6Z.mjs +162 -0
- package/dist/remediation.d.ts +25 -0
- package/dist/remediation.d.ts.map +1 -0
- package/dist/remediation.js +98 -0
- package/dist/remediation.js.map +1 -0
- package/dist/scoring.d.ts +3 -7
- package/dist/scoring.d.ts.map +1 -1
- package/dist/scoring.js +57 -48
- package/dist/scoring.js.map +1 -1
- package/dist/semantic-analysis.d.ts +12 -23
- package/dist/semantic-analysis.d.ts.map +1 -1
- package/dist/semantic-analysis.js +172 -110
- package/dist/semantic-analysis.js.map +1 -1
- package/dist/summary.d.ts +6 -0
- package/dist/summary.d.ts.map +1 -0
- package/dist/summary.js +92 -0
- package/dist/summary.js.map +1 -0
- package/dist/types.d.ts +9 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/output-formatter.d.ts +14 -0
- package/dist/utils/output-formatter.d.ts.map +1 -0
- package/dist/utils/output-formatter.js +338 -0
- package/dist/utils/output-formatter.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/analyzer.test.ts +1 -1
- package/src/__tests__/auto-detection.test.ts +1 -1
- package/src/__tests__/contract.test.ts +1 -1
- package/src/__tests__/enhanced-cohesion.test.ts +1 -1
- package/src/__tests__/file-classification.test.ts +1 -1
- package/src/__tests__/fragmentation-advanced.test.ts +1 -1
- package/src/__tests__/fragmentation-coupling.test.ts +1 -1
- package/src/__tests__/fragmentation-log.test.ts +1 -1
- package/src/__tests__/provider.test.ts +1 -1
- package/src/__tests__/structural-cohesion.test.ts +1 -1
- package/src/analyzer.ts +112 -317
- package/src/analyzers/python-context.ts +7 -76
- package/src/ast-utils.ts +2 -2
- package/src/classifier.ts +13 -328
- package/src/cli-action.ts +110 -0
- package/src/cli.ts +3 -701
- package/src/cluster-detector.ts +28 -1
- package/src/defaults.ts +3 -0
- package/src/graph-builder.ts +10 -91
- package/src/heuristics.ts +216 -0
- package/src/index.ts +6 -0
- package/src/issue-analyzer.ts +158 -0
- package/src/metrics.ts +9 -0
- package/src/scoring.ts +3 -5
- package/src/semantic-analysis.ts +8 -14
- package/src/summary.ts +62 -106
- package/src/types.ts +52 -20
- package/src/utils/dependency-graph-utils.ts +126 -0
- package/src/utils/output-formatter.ts +411 -0
- package/src/utils/string-utils.ts +21 -0
package/src/cluster-detector.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/src/graph-builder.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
}
|
package/src/semantic-analysis.ts
CHANGED
|
@@ -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>>
|