@aiready/context-analyzer 0.22.15 → 0.22.16
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 +17 -17
- package/.turbo/turbo-format-check.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +348 -26
- package/.turbo/turbo-type-check.log +1 -1
- package/dist/chunk-2LEHY2GV.mjs +1287 -0
- package/dist/chunk-P2ZQGQAO.mjs +1282 -0
- package/dist/chunk-QGI23DBA.mjs +1282 -0
- package/dist/chunk-QTB4KYCX.mjs +1260 -0
- package/dist/chunk-RQ5BQLT6.mjs +102 -0
- package/dist/chunk-VYFHSGV6.mjs +1283 -0
- package/dist/chunk-WLXLBWDU.mjs +96 -0
- package/dist/chunk-XDYPMFCH.mjs +1250 -0
- package/dist/cli-action-332WE54N.mjs +95 -0
- package/dist/cli-action-7QXG7LHS.mjs +95 -0
- package/dist/cli-action-BIX6TYXF.mjs +95 -0
- package/dist/cli-action-BUGVCH44.mjs +95 -0
- package/dist/cli-action-RO24U52W.mjs +95 -0
- package/dist/cli-action-WAZ5KM6X.mjs +95 -0
- package/dist/cli-action-XDKINE2R.mjs +95 -0
- package/dist/cli-action-Y6VATXMV.mjs +95 -0
- package/dist/cli.js +75 -27
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +71 -25
- package/dist/index.mjs +3 -3
- package/dist/orchestrator-2KQNMO2L.mjs +10 -0
- package/dist/orchestrator-66ZVNOLR.mjs +10 -0
- package/dist/orchestrator-KM2OJPZD.mjs +10 -0
- package/dist/orchestrator-MKDZPRBA.mjs +10 -0
- package/dist/orchestrator-QSHWWBWS.mjs +10 -0
- package/dist/orchestrator-WFQPMNSD.mjs +10 -0
- package/dist/python-context-H2OLC5JN.mjs +162 -0
- package/dist/python-context-OBP7JD5P.mjs +162 -0
- package/package.json +5 -3
- package/src/__tests__/analyzer.test.ts +4 -3
- package/src/__tests__/issue-analyzer.test.ts +4 -2
- package/src/classify/file-classifiers.ts +14 -13
- package/src/cli-action.ts +6 -3
- package/src/graph-builder.ts +43 -8
- package/src/issue-analyzer.ts +19 -7
- package/src/orchestrator.ts +6 -4
- package/src/semantic/domain-inference.ts +1 -1
- package/src/types.ts +2 -0
- package/src/utils/dependency-graph-utils.ts +22 -13
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
calculateImportDepthFromEdges,
|
|
3
|
+
detectGraphCyclesFromFile
|
|
4
|
+
} from "./chunk-WLXLBWDU.mjs";
|
|
5
|
+
|
|
6
|
+
// src/analyzers/python-context.ts
|
|
7
|
+
import { getParser, estimateTokens } from "@aiready/core";
|
|
8
|
+
import { resolve, relative, dirname, join } from "path";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
async function analyzePythonContext(files, rootDir) {
|
|
11
|
+
const results = [];
|
|
12
|
+
const parser = await getParser("dummy.py");
|
|
13
|
+
if (!parser) {
|
|
14
|
+
console.warn("Python parser not available");
|
|
15
|
+
return results;
|
|
16
|
+
}
|
|
17
|
+
const pythonFiles = files.filter((f) => f.toLowerCase().endsWith(".py"));
|
|
18
|
+
void relative;
|
|
19
|
+
void join;
|
|
20
|
+
const dependencyGraph = await buildPythonDependencyGraph(
|
|
21
|
+
pythonFiles,
|
|
22
|
+
rootDir
|
|
23
|
+
);
|
|
24
|
+
for (const file of pythonFiles) {
|
|
25
|
+
try {
|
|
26
|
+
const code = await fs.promises.readFile(file, "utf-8");
|
|
27
|
+
const result = parser.parse(code, file);
|
|
28
|
+
const imports = result.imports.map((imp) => ({
|
|
29
|
+
source: imp.source,
|
|
30
|
+
specifiers: imp.specifiers,
|
|
31
|
+
isRelative: imp.source.startsWith("."),
|
|
32
|
+
resolvedPath: resolvePythonImport(file, imp.source, rootDir)
|
|
33
|
+
}));
|
|
34
|
+
const exports = result.exports.map((exp) => ({
|
|
35
|
+
name: exp.name,
|
|
36
|
+
type: exp.type
|
|
37
|
+
}));
|
|
38
|
+
const linesOfCode = code.split("\n").length;
|
|
39
|
+
const importDepth = calculateImportDepthFromEdges(
|
|
40
|
+
file,
|
|
41
|
+
dependencyGraph,
|
|
42
|
+
/* @__PURE__ */ new Set()
|
|
43
|
+
);
|
|
44
|
+
const contextBudget = estimateContextBudget(
|
|
45
|
+
code,
|
|
46
|
+
imports,
|
|
47
|
+
dependencyGraph
|
|
48
|
+
);
|
|
49
|
+
const cohesion = calculatePythonCohesion(exports, imports);
|
|
50
|
+
const circularDependencies = detectGraphCyclesFromFile(
|
|
51
|
+
file,
|
|
52
|
+
dependencyGraph
|
|
53
|
+
).map((cycle) => cycle.join(" -> "));
|
|
54
|
+
results.push({
|
|
55
|
+
file,
|
|
56
|
+
importDepth,
|
|
57
|
+
contextBudget,
|
|
58
|
+
cohesion,
|
|
59
|
+
imports,
|
|
60
|
+
exports,
|
|
61
|
+
metrics: {
|
|
62
|
+
linesOfCode,
|
|
63
|
+
importCount: imports.length,
|
|
64
|
+
exportCount: exports.length,
|
|
65
|
+
circularDependencies
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn(`Failed to analyze ${file}:`, error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
async function buildPythonDependencyGraph(files, rootDir) {
|
|
75
|
+
const graph = /* @__PURE__ */ new Map();
|
|
76
|
+
const parser = await getParser("dummy.py");
|
|
77
|
+
if (!parser) return graph;
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
try {
|
|
80
|
+
const code = await fs.promises.readFile(file, "utf-8");
|
|
81
|
+
const result = parser.parse(code, file);
|
|
82
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
83
|
+
for (const imp of result.imports) {
|
|
84
|
+
const resolved = resolvePythonImport(file, imp.source, rootDir);
|
|
85
|
+
if (resolved && files.includes(resolved)) {
|
|
86
|
+
dependencies.add(resolved);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
graph.set(file, dependencies);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
void error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return graph;
|
|
95
|
+
}
|
|
96
|
+
function resolvePythonImport(fromFile, importPath, rootDir) {
|
|
97
|
+
const dir = dirname(fromFile);
|
|
98
|
+
if (importPath.startsWith(".")) {
|
|
99
|
+
const parts = importPath.split(".");
|
|
100
|
+
let upCount = 0;
|
|
101
|
+
while (parts[0] === "") {
|
|
102
|
+
upCount++;
|
|
103
|
+
parts.shift();
|
|
104
|
+
}
|
|
105
|
+
let targetDir = dir;
|
|
106
|
+
for (let i = 0; i < upCount - 1; i++) {
|
|
107
|
+
targetDir = dirname(targetDir);
|
|
108
|
+
}
|
|
109
|
+
const modulePath = parts.join("/");
|
|
110
|
+
const possiblePaths = [
|
|
111
|
+
resolve(targetDir, `${modulePath}.py`),
|
|
112
|
+
resolve(targetDir, modulePath, "__init__.py")
|
|
113
|
+
];
|
|
114
|
+
for (const path of possiblePaths) {
|
|
115
|
+
if (fs.existsSync(path)) {
|
|
116
|
+
return path;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
const modulePath = importPath.replace(/\./g, "/");
|
|
121
|
+
const possiblePaths = [
|
|
122
|
+
resolve(rootDir, `${modulePath}.py`),
|
|
123
|
+
resolve(rootDir, modulePath, "__init__.py")
|
|
124
|
+
];
|
|
125
|
+
for (const path of possiblePaths) {
|
|
126
|
+
if (fs.existsSync(path)) {
|
|
127
|
+
return path;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
function estimateContextBudget(code, imports, dependencyGraph) {
|
|
134
|
+
void dependencyGraph;
|
|
135
|
+
let budget = estimateTokens(code);
|
|
136
|
+
const avgTokensPerDep = 500;
|
|
137
|
+
budget += imports.length * avgTokensPerDep;
|
|
138
|
+
return budget;
|
|
139
|
+
}
|
|
140
|
+
function calculatePythonCohesion(exports, imports) {
|
|
141
|
+
if (exports.length === 0) return 1;
|
|
142
|
+
const exportCount = exports.length;
|
|
143
|
+
const importCount = imports.length;
|
|
144
|
+
let cohesion = 1;
|
|
145
|
+
if (exportCount > 10) {
|
|
146
|
+
cohesion *= 0.6;
|
|
147
|
+
} else if (exportCount > 5) {
|
|
148
|
+
cohesion *= 0.8;
|
|
149
|
+
}
|
|
150
|
+
if (exportCount > 0) {
|
|
151
|
+
const ratio = importCount / exportCount;
|
|
152
|
+
if (ratio > 2) {
|
|
153
|
+
cohesion *= 1.1;
|
|
154
|
+
} else if (ratio < 0.5) {
|
|
155
|
+
cohesion *= 0.9;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return Math.min(1, Math.max(0, cohesion));
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
analyzePythonContext
|
|
162
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
calculateImportDepthFromEdges,
|
|
3
|
+
detectGraphCyclesFromFile
|
|
4
|
+
} from "./chunk-RQ5BQLT6.mjs";
|
|
5
|
+
|
|
6
|
+
// src/analyzers/python-context.ts
|
|
7
|
+
import { getParser, estimateTokens } from "@aiready/core";
|
|
8
|
+
import { resolve, relative, dirname, join } from "path";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
async function analyzePythonContext(files, rootDir) {
|
|
11
|
+
const results = [];
|
|
12
|
+
const parser = await getParser("dummy.py");
|
|
13
|
+
if (!parser) {
|
|
14
|
+
console.warn("Python parser not available");
|
|
15
|
+
return results;
|
|
16
|
+
}
|
|
17
|
+
const pythonFiles = files.filter((f) => f.toLowerCase().endsWith(".py"));
|
|
18
|
+
void relative;
|
|
19
|
+
void join;
|
|
20
|
+
const dependencyGraph = await buildPythonDependencyGraph(
|
|
21
|
+
pythonFiles,
|
|
22
|
+
rootDir
|
|
23
|
+
);
|
|
24
|
+
for (const file of pythonFiles) {
|
|
25
|
+
try {
|
|
26
|
+
const code = await fs.promises.readFile(file, "utf-8");
|
|
27
|
+
const result = parser.parse(code, file);
|
|
28
|
+
const imports = result.imports.map((imp) => ({
|
|
29
|
+
source: imp.source,
|
|
30
|
+
specifiers: imp.specifiers,
|
|
31
|
+
isRelative: imp.source.startsWith("."),
|
|
32
|
+
resolvedPath: resolvePythonImport(file, imp.source, rootDir)
|
|
33
|
+
}));
|
|
34
|
+
const exports = result.exports.map((exp) => ({
|
|
35
|
+
name: exp.name,
|
|
36
|
+
type: exp.type
|
|
37
|
+
}));
|
|
38
|
+
const linesOfCode = code.split("\n").length;
|
|
39
|
+
const importDepth = calculateImportDepthFromEdges(
|
|
40
|
+
file,
|
|
41
|
+
dependencyGraph,
|
|
42
|
+
/* @__PURE__ */ new Set()
|
|
43
|
+
);
|
|
44
|
+
const contextBudget = estimateContextBudget(
|
|
45
|
+
code,
|
|
46
|
+
imports,
|
|
47
|
+
dependencyGraph
|
|
48
|
+
);
|
|
49
|
+
const cohesion = calculatePythonCohesion(exports, imports);
|
|
50
|
+
const circularDependencies = detectGraphCyclesFromFile(
|
|
51
|
+
file,
|
|
52
|
+
dependencyGraph
|
|
53
|
+
).map((cycle) => cycle.join(" -> "));
|
|
54
|
+
results.push({
|
|
55
|
+
file,
|
|
56
|
+
importDepth,
|
|
57
|
+
contextBudget,
|
|
58
|
+
cohesion,
|
|
59
|
+
imports,
|
|
60
|
+
exports,
|
|
61
|
+
metrics: {
|
|
62
|
+
linesOfCode,
|
|
63
|
+
importCount: imports.length,
|
|
64
|
+
exportCount: exports.length,
|
|
65
|
+
circularDependencies
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn(`Failed to analyze ${file}:`, error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
async function buildPythonDependencyGraph(files, rootDir) {
|
|
75
|
+
const graph = /* @__PURE__ */ new Map();
|
|
76
|
+
const parser = await getParser("dummy.py");
|
|
77
|
+
if (!parser) return graph;
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
try {
|
|
80
|
+
const code = await fs.promises.readFile(file, "utf-8");
|
|
81
|
+
const result = parser.parse(code, file);
|
|
82
|
+
const dependencies = /* @__PURE__ */ new Set();
|
|
83
|
+
for (const imp of result.imports) {
|
|
84
|
+
const resolved = resolvePythonImport(file, imp.source, rootDir);
|
|
85
|
+
if (resolved && files.includes(resolved)) {
|
|
86
|
+
dependencies.add(resolved);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
graph.set(file, dependencies);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
void error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return graph;
|
|
95
|
+
}
|
|
96
|
+
function resolvePythonImport(fromFile, importPath, rootDir) {
|
|
97
|
+
const dir = dirname(fromFile);
|
|
98
|
+
if (importPath.startsWith(".")) {
|
|
99
|
+
const parts = importPath.split(".");
|
|
100
|
+
let upCount = 0;
|
|
101
|
+
while (parts[0] === "") {
|
|
102
|
+
upCount++;
|
|
103
|
+
parts.shift();
|
|
104
|
+
}
|
|
105
|
+
let targetDir = dir;
|
|
106
|
+
for (let i = 0; i < upCount - 1; i++) {
|
|
107
|
+
targetDir = dirname(targetDir);
|
|
108
|
+
}
|
|
109
|
+
const modulePath = parts.join("/");
|
|
110
|
+
const possiblePaths = [
|
|
111
|
+
resolve(targetDir, `${modulePath}.py`),
|
|
112
|
+
resolve(targetDir, modulePath, "__init__.py")
|
|
113
|
+
];
|
|
114
|
+
for (const path of possiblePaths) {
|
|
115
|
+
if (fs.existsSync(path)) {
|
|
116
|
+
return path;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
const modulePath = importPath.replace(/\./g, "/");
|
|
121
|
+
const possiblePaths = [
|
|
122
|
+
resolve(rootDir, `${modulePath}.py`),
|
|
123
|
+
resolve(rootDir, modulePath, "__init__.py")
|
|
124
|
+
];
|
|
125
|
+
for (const path of possiblePaths) {
|
|
126
|
+
if (fs.existsSync(path)) {
|
|
127
|
+
return path;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return void 0;
|
|
132
|
+
}
|
|
133
|
+
function estimateContextBudget(code, imports, dependencyGraph) {
|
|
134
|
+
void dependencyGraph;
|
|
135
|
+
let budget = estimateTokens(code);
|
|
136
|
+
const avgTokensPerDep = 500;
|
|
137
|
+
budget += imports.length * avgTokensPerDep;
|
|
138
|
+
return budget;
|
|
139
|
+
}
|
|
140
|
+
function calculatePythonCohesion(exports, imports) {
|
|
141
|
+
if (exports.length === 0) return 1;
|
|
142
|
+
const exportCount = exports.length;
|
|
143
|
+
const importCount = imports.length;
|
|
144
|
+
let cohesion = 1;
|
|
145
|
+
if (exportCount > 10) {
|
|
146
|
+
cohesion *= 0.6;
|
|
147
|
+
} else if (exportCount > 5) {
|
|
148
|
+
cohesion *= 0.8;
|
|
149
|
+
}
|
|
150
|
+
if (exportCount > 0) {
|
|
151
|
+
const ratio = importCount / exportCount;
|
|
152
|
+
if (ratio > 2) {
|
|
153
|
+
cohesion *= 1.1;
|
|
154
|
+
} else if (ratio < 0.5) {
|
|
155
|
+
cohesion *= 0.9;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return Math.min(1, Math.max(0, cohesion));
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
analyzePythonContext
|
|
162
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/context-analyzer",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.16",
|
|
4
4
|
"description": "AI context window cost analysis - detect fragmented code, deep import chains, and expensive context budgets",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"commander": "^14.0.0",
|
|
50
50
|
"chalk": "^5.3.0",
|
|
51
51
|
"prompts": "^2.4.2",
|
|
52
|
-
"@aiready/core": "0.24.
|
|
52
|
+
"@aiready/core": "0.24.19"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^24.0.0",
|
|
@@ -70,6 +70,8 @@
|
|
|
70
70
|
"clean": "rm -rf dist",
|
|
71
71
|
"release": "pnpm build && pnpm publish --no-git-checks",
|
|
72
72
|
"type-check": "tsc --noEmit",
|
|
73
|
-
"format-check": "prettier --check . --ignore-path ../../.prettierignore"
|
|
73
|
+
"format-check": "prettier --check . --ignore-path ../../.prettierignore",
|
|
74
|
+
"format": "prettier --write . --ignore-path ../../.prettierignore",
|
|
75
|
+
"lint:fix": "eslint . --fix"
|
|
74
76
|
}
|
|
75
77
|
}
|
|
@@ -93,9 +93,10 @@ describe('getTransitiveDependencies', () => {
|
|
|
93
93
|
const graph = await buildDependencyGraph(files);
|
|
94
94
|
const deps = getTransitiveDependencies('c.ts', graph);
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
expect(
|
|
98
|
-
expect(
|
|
96
|
+
const depKeys = Array.from(deps.keys());
|
|
97
|
+
expect(depKeys).toContain('b.ts');
|
|
98
|
+
expect(depKeys).toContain('a.ts');
|
|
99
|
+
expect(deps.size).toBe(2);
|
|
99
100
|
});
|
|
100
101
|
});
|
|
101
102
|
|
|
@@ -4,9 +4,11 @@ import { Severity } from '@aiready/core';
|
|
|
4
4
|
|
|
5
5
|
describe('analyzeIssues', () => {
|
|
6
6
|
const baseParams = {
|
|
7
|
-
file: '
|
|
7
|
+
file: 'test.ts',
|
|
8
8
|
importDepth: 2,
|
|
9
|
-
|
|
9
|
+
tokenCost: 1000,
|
|
10
|
+
contextBudget: 5000,
|
|
11
|
+
|
|
10
12
|
cohesionScore: 0.8,
|
|
11
13
|
fragmentationScore: 0.3,
|
|
12
14
|
maxDepth: 5,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DependencyNode } from '../types';
|
|
2
|
+
import type { ExportInfo } from '@aiready/core';
|
|
2
3
|
import {
|
|
3
4
|
BARREL_EXPORT_MIN_EXPORTS,
|
|
4
5
|
BARREL_EXPORT_TOKEN_LIMIT,
|
|
@@ -23,7 +24,7 @@ export function isBoilerplateBarrel(node: DependencyNode): boolean {
|
|
|
23
24
|
if (!exports || exports.length === 0) return false;
|
|
24
25
|
|
|
25
26
|
// 1. Must be purely re-exports
|
|
26
|
-
const isPurelyReexports = exports.every((exp:
|
|
27
|
+
const isPurelyReexports = exports.every((exp: ExportInfo) => !!exp.source);
|
|
27
28
|
if (!isPurelyReexports) return false;
|
|
28
29
|
|
|
29
30
|
// 2. Must be low local token cost (no actual logic)
|
|
@@ -31,7 +32,7 @@ export function isBoilerplateBarrel(node: DependencyNode): boolean {
|
|
|
31
32
|
|
|
32
33
|
// 3. Detect "Architectural Theater"
|
|
33
34
|
// If it re-exports everything from exactly ONE source, it's a pass-through
|
|
34
|
-
const sources = new Set(exports.map((exp:
|
|
35
|
+
const sources = new Set(exports.map((exp: ExportInfo) => exp.source));
|
|
35
36
|
|
|
36
37
|
// Pattern: export * from '../actual'
|
|
37
38
|
const isSingleSourcePassThrough = sources.size === 1;
|
|
@@ -63,7 +64,7 @@ export function isBarrelExport(node: DependencyNode): boolean {
|
|
|
63
64
|
|
|
64
65
|
const isReexportPattern =
|
|
65
66
|
(exports || []).length >= BARREL_EXPORT_MIN_EXPORTS &&
|
|
66
|
-
(exports || []).every((exp:
|
|
67
|
+
(exports || []).every((exp: ExportInfo) =>
|
|
67
68
|
['const', 'function', 'type', 'interface'].includes(exp.type)
|
|
68
69
|
);
|
|
69
70
|
|
|
@@ -86,7 +87,7 @@ export function isTypeDefinition(node: DependencyNode): boolean {
|
|
|
86
87
|
const areAllTypes =
|
|
87
88
|
hasExports &&
|
|
88
89
|
nodeExports.every(
|
|
89
|
-
(exp:
|
|
90
|
+
(exp: ExportInfo) => exp.type === 'type' || exp.type === 'interface'
|
|
90
91
|
);
|
|
91
92
|
|
|
92
93
|
const isTypePath = /\/(types|interfaces|models)\//i.test(file);
|
|
@@ -124,7 +125,7 @@ export function isLambdaHandler(node: DependencyNode): boolean {
|
|
|
124
125
|
);
|
|
125
126
|
const isHandlerPath = /\/(handlers|lambdas|lambda|functions)\//i.test(file);
|
|
126
127
|
const hasHandlerExport = (exports || []).some(
|
|
127
|
-
(exp:
|
|
128
|
+
(exp: ExportInfo) =>
|
|
128
129
|
['handler', 'main', 'lambdahandler'].includes(exp.name.toLowerCase()) ||
|
|
129
130
|
exp.name.toLowerCase().endsWith('handler')
|
|
130
131
|
);
|
|
@@ -146,11 +147,11 @@ export function isServiceFile(node: DependencyNode): boolean {
|
|
|
146
147
|
fileName.includes(pattern)
|
|
147
148
|
);
|
|
148
149
|
const isServicePath = file.toLowerCase().includes('/services/');
|
|
149
|
-
const hasServiceNamedExport = (exports || []).some((exp:
|
|
150
|
+
const hasServiceNamedExport = (exports || []).some((exp: ExportInfo) =>
|
|
150
151
|
exp.name.toLowerCase().includes('service')
|
|
151
152
|
);
|
|
152
153
|
const hasClassExport = (exports || []).some(
|
|
153
|
-
(exp:
|
|
154
|
+
(exp: ExportInfo) => exp.type === 'class'
|
|
154
155
|
);
|
|
155
156
|
return (
|
|
156
157
|
isServiceName || isServicePath || (hasServiceNamedExport && hasClassExport)
|
|
@@ -173,7 +174,7 @@ export function isEmailTemplate(node: DependencyNode): boolean {
|
|
|
173
174
|
);
|
|
174
175
|
const isEmailPath = /\/(emails|mail|notifications)\//i.test(file);
|
|
175
176
|
const hasTemplateFunction = (exports || []).some(
|
|
176
|
-
(exp:
|
|
177
|
+
(exp: ExportInfo) =>
|
|
177
178
|
exp.type === 'function' &&
|
|
178
179
|
(exp.name.toLowerCase().startsWith('render') ||
|
|
179
180
|
exp.name.toLowerCase().startsWith('generate'))
|
|
@@ -197,7 +198,7 @@ export function isParserFile(node: DependencyNode): boolean {
|
|
|
197
198
|
);
|
|
198
199
|
const isParserPath = /\/(parsers|transformers)\//i.test(file);
|
|
199
200
|
const hasParseFunction = (exports || []).some(
|
|
200
|
-
(exp:
|
|
201
|
+
(exp: ExportInfo) =>
|
|
201
202
|
exp.type === 'function' &&
|
|
202
203
|
(exp.name.toLowerCase().startsWith('parse') ||
|
|
203
204
|
exp.name.toLowerCase().startsWith('transform'))
|
|
@@ -220,7 +221,7 @@ export function isSessionFile(node: DependencyNode): boolean {
|
|
|
220
221
|
fileName.includes(pattern)
|
|
221
222
|
);
|
|
222
223
|
const isSessionPath = /\/(sessions|state)\//i.test(file);
|
|
223
|
-
const hasSessionExport = (exports || []).some((exp:
|
|
224
|
+
const hasSessionExport = (exports || []).some((exp: ExportInfo) =>
|
|
224
225
|
['session', 'state', 'store'].some((pattern: string) =>
|
|
225
226
|
exp.name.toLowerCase().includes(pattern)
|
|
226
227
|
)
|
|
@@ -246,10 +247,10 @@ export function isNextJsPage(node: DependencyNode): boolean {
|
|
|
246
247
|
return false;
|
|
247
248
|
|
|
248
249
|
const hasDefaultExport = (exports || []).some(
|
|
249
|
-
(exp:
|
|
250
|
+
(exp: ExportInfo) => exp.type === 'default'
|
|
250
251
|
);
|
|
251
252
|
|
|
252
|
-
const hasNextJsExport = (exports || []).some((exp:
|
|
253
|
+
const hasNextJsExport = (exports || []).some((exp: ExportInfo) =>
|
|
253
254
|
NEXTJS_METADATA_EXPORTS.includes(exp.name.toLowerCase())
|
|
254
255
|
);
|
|
255
256
|
|
|
@@ -272,7 +273,7 @@ export function isConfigFile(node: DependencyNode): boolean {
|
|
|
272
273
|
fileName.includes(pattern)
|
|
273
274
|
);
|
|
274
275
|
const isConfigPath = /\/(config|settings|schemas)\//i.test(lowerPath);
|
|
275
|
-
const hasSchemaExport = (exports || []).some((exp:
|
|
276
|
+
const hasSchemaExport = (exports || []).some((exp: ExportInfo) =>
|
|
276
277
|
['schema', 'config', 'setting'].some((pattern: string) =>
|
|
277
278
|
exp.name.toLowerCase().includes(pattern)
|
|
278
279
|
)
|
package/src/cli-action.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from '@aiready/core';
|
|
8
8
|
import { analyzeContext } from './orchestrator';
|
|
9
9
|
import { generateSummary } from './summary';
|
|
10
|
+
import type { ContextAnalyzerOptions } from './types';
|
|
10
11
|
import chalk from 'chalk';
|
|
11
12
|
import { writeFileSync } from 'fs';
|
|
12
13
|
|
|
@@ -28,7 +29,7 @@ export async function contextActionHandler(directory: string, options: any) {
|
|
|
28
29
|
try {
|
|
29
30
|
// Define defaults
|
|
30
31
|
const defaults = {
|
|
31
|
-
maxDepth:
|
|
32
|
+
maxDepth: 10,
|
|
32
33
|
maxContextBudget: 10000,
|
|
33
34
|
minCohesion: 0.6,
|
|
34
35
|
maxFragmentation: 0.5,
|
|
@@ -68,7 +69,9 @@ export async function contextActionHandler(directory: string, options: any) {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
// Run analysis
|
|
71
|
-
const results = await analyzeContext(
|
|
72
|
+
const results = await analyzeContext(
|
|
73
|
+
finalOptions as ContextAnalyzerOptions
|
|
74
|
+
);
|
|
72
75
|
const summary = generateSummary(results, finalOptions);
|
|
73
76
|
|
|
74
77
|
const duration = getElapsedTime(startTime);
|
|
@@ -104,7 +107,7 @@ export async function contextActionHandler(directory: string, options: any) {
|
|
|
104
107
|
} else {
|
|
105
108
|
// Default: Console (Dynamic Import)
|
|
106
109
|
const { displayConsoleReport } = await import('./report/console-report');
|
|
107
|
-
displayConsoleReport(summary, results,
|
|
110
|
+
displayConsoleReport(summary, results, finalOptions.maxResults);
|
|
108
111
|
console.log(chalk.dim(`\n✨ Analysis completed in ${duration}ms\n`));
|
|
109
112
|
}
|
|
110
113
|
} catch (error) {
|
package/src/graph-builder.ts
CHANGED
|
@@ -23,6 +23,35 @@ function resolveImport(
|
|
|
23
23
|
// If it's not a relative import, we treat it as an external dependency for now
|
|
24
24
|
// (unless it's an absolute path that exists in our set)
|
|
25
25
|
if (!source.startsWith('.') && !source.startsWith('/')) {
|
|
26
|
+
// Ignore standard libraries and external packages we don't control
|
|
27
|
+
const externalIgnores = [
|
|
28
|
+
'react',
|
|
29
|
+
'next',
|
|
30
|
+
'lucide-react',
|
|
31
|
+
'framer-motion',
|
|
32
|
+
'@aws-sdk',
|
|
33
|
+
'stripe',
|
|
34
|
+
'clsx',
|
|
35
|
+
'tailwind-merge',
|
|
36
|
+
'zod',
|
|
37
|
+
'commander',
|
|
38
|
+
'chalk',
|
|
39
|
+
'fs',
|
|
40
|
+
'path',
|
|
41
|
+
'util',
|
|
42
|
+
'child_process',
|
|
43
|
+
'os',
|
|
44
|
+
'crypto',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
externalIgnores.some(
|
|
49
|
+
(pkg) => source === pkg || source.startsWith(`${pkg}/`)
|
|
50
|
+
)
|
|
51
|
+
) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
26
55
|
// Handle monorepo package imports (@aiready/*)
|
|
27
56
|
if (source.startsWith('@aiready/')) {
|
|
28
57
|
const pkgName = source.split('/')[1];
|
|
@@ -144,12 +173,15 @@ export async function buildDependencyGraph(
|
|
|
144
173
|
// 1. Get high-fidelity AST-based imports & exports
|
|
145
174
|
const { imports: astImports } = await parseFileExports(content, file);
|
|
146
175
|
|
|
147
|
-
// 2.
|
|
148
|
-
const
|
|
176
|
+
// 2. Filter out type-only imports (they don't create runtime dependencies)
|
|
177
|
+
const runtimeImports = astImports.filter((i) => !i.isTypeOnly);
|
|
178
|
+
|
|
179
|
+
// 3. Resolve imports to absolute paths in the graph
|
|
180
|
+
const resolvedImports = runtimeImports
|
|
149
181
|
.map((i) => resolveImport(i.source, file, allFilePaths))
|
|
150
182
|
.filter((path): path is string => path !== null);
|
|
151
183
|
|
|
152
|
-
const importSources =
|
|
184
|
+
const importSources = runtimeImports.map((i) => i.source);
|
|
153
185
|
|
|
154
186
|
// 3. Wrap with platform-specific metadata (v0.11+)
|
|
155
187
|
const exports = await extractExportsWithAST(
|
|
@@ -233,7 +265,7 @@ export function getTransitiveDependencies(
|
|
|
233
265
|
file: string,
|
|
234
266
|
graph: DependencyGraph,
|
|
235
267
|
visited = new Set<string>()
|
|
236
|
-
): string
|
|
268
|
+
): Map<string, number> {
|
|
237
269
|
return getTransitiveDependenciesFromEdges(file, graph.edges, visited);
|
|
238
270
|
}
|
|
239
271
|
|
|
@@ -242,7 +274,7 @@ export function getTransitiveDependencies(
|
|
|
242
274
|
*
|
|
243
275
|
* @param file - File path to calculate budget for.
|
|
244
276
|
* @param graph - The dependency graph.
|
|
245
|
-
* @returns Total token count including recursive dependencies.
|
|
277
|
+
* @returns Total token count including recursive dependencies (discounted by depth).
|
|
246
278
|
*/
|
|
247
279
|
export function calculateContextBudget(
|
|
248
280
|
file: string,
|
|
@@ -254,14 +286,17 @@ export function calculateContextBudget(
|
|
|
254
286
|
let totalTokens = node.tokenCost;
|
|
255
287
|
const deps = getTransitiveDependencies(file, graph);
|
|
256
288
|
|
|
257
|
-
for (const dep of deps) {
|
|
289
|
+
for (const [dep, depth] of deps.entries()) {
|
|
258
290
|
const depNode = graph.nodes.get(dep);
|
|
259
291
|
if (depNode) {
|
|
260
|
-
|
|
292
|
+
// Discount token cost by depth (20% reduction per level)
|
|
293
|
+
// This prevents "barrel file" false positives where a facade pulls in the entire project
|
|
294
|
+
const discountFactor = Math.pow(0.8, depth - 1);
|
|
295
|
+
totalTokens += depNode.tokenCost * discountFactor;
|
|
261
296
|
}
|
|
262
297
|
}
|
|
263
298
|
|
|
264
|
-
return totalTokens;
|
|
299
|
+
return Math.round(totalTokens);
|
|
265
300
|
}
|
|
266
301
|
|
|
267
302
|
/**
|
package/src/issue-analyzer.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { isBuildArtifact };
|
|
|
17
17
|
export function analyzeIssues(params: {
|
|
18
18
|
file: string;
|
|
19
19
|
importDepth: number;
|
|
20
|
+
tokenCost: number;
|
|
20
21
|
contextBudget: number;
|
|
21
22
|
cohesionScore: number;
|
|
22
23
|
fragmentationScore: number;
|
|
@@ -34,6 +35,7 @@ export function analyzeIssues(params: {
|
|
|
34
35
|
const {
|
|
35
36
|
file,
|
|
36
37
|
importDepth,
|
|
38
|
+
tokenCost,
|
|
37
39
|
contextBudget,
|
|
38
40
|
cohesionScore,
|
|
39
41
|
fragmentationScore,
|
|
@@ -74,22 +76,32 @@ export function analyzeIssues(params: {
|
|
|
74
76
|
potentialSavings += contextBudget * 0.15;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
// Check
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
// Check direct file size
|
|
80
|
+
const MAX_FILE_TOKENS = 10000;
|
|
81
|
+
if (tokenCost > MAX_FILE_TOKENS) {
|
|
82
|
+
if (severity !== Severity.Critical) severity = Severity.Major;
|
|
80
83
|
issues.push(
|
|
81
|
-
`
|
|
84
|
+
`File is excessively large (${tokenCost.toLocaleString()} tokens)`
|
|
82
85
|
);
|
|
83
86
|
recommendations.push(
|
|
84
|
-
'Split into smaller modules
|
|
87
|
+
'Split file into smaller, single-responsibility modules'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check transitive context budget
|
|
92
|
+
if (contextBudget > maxContextBudget * 1.5) {
|
|
93
|
+
severity = Severity.Critical;
|
|
94
|
+
issues.push(
|
|
95
|
+
`Total context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
|
|
85
96
|
);
|
|
97
|
+
recommendations.push('Reduce dependency tree width or reduce deep imports');
|
|
86
98
|
potentialSavings += contextBudget * 0.4;
|
|
87
99
|
} else if (contextBudget > maxContextBudget) {
|
|
88
100
|
if (severity !== Severity.Critical) severity = Severity.Major;
|
|
89
101
|
issues.push(
|
|
90
|
-
`
|
|
102
|
+
`Total context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
|
|
91
103
|
);
|
|
92
|
-
recommendations.push('
|
|
104
|
+
recommendations.push('Optimize dependency graph and reduce deep imports');
|
|
93
105
|
potentialSavings += contextBudget * 0.2;
|
|
94
106
|
}
|
|
95
107
|
|