@aiready/context-analyzer 0.6.0 → 0.7.1
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 +10 -10
- package/.turbo/turbo-test.log +11 -6
- package/README.md +5 -3
- package/SEMANTIC-VALIDATION.md +235 -0
- package/dist/chunk-AEK3MZC5.mjs +709 -0
- package/dist/chunk-DMRZMS2U.mjs +964 -0
- package/dist/chunk-HQNHM2X7.mjs +997 -0
- package/dist/chunk-I54HL4FZ.mjs +781 -0
- package/dist/chunk-IRWCPDWD.mjs +779 -0
- package/dist/chunk-PVVCCE6W.mjs +755 -0
- package/dist/chunk-RYIB5CWD.mjs +781 -0
- package/dist/cli.js +234 -16
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +90 -1
- package/dist/index.d.ts +90 -1
- package/dist/index.js +311 -18
- package/dist/index.mjs +17 -3
- package/package.json +2 -2
- package/src/__tests__/auto-detection.test.ts +156 -0
- package/src/analyzer.ts +182 -18
- package/src/index.ts +34 -2
- package/src/semantic-analysis.ts +287 -0
- package/src/types.ts +33 -1
- package/COHESION-IMPROVEMENTS.md +0 -202
package/dist/index.js
CHANGED
|
@@ -21,20 +21,252 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
analyzeContext: () => analyzeContext,
|
|
24
|
+
buildCoUsageMatrix: () => buildCoUsageMatrix,
|
|
25
|
+
buildTypeGraph: () => buildTypeGraph,
|
|
26
|
+
calculateDomainConfidence: () => calculateDomainConfidence,
|
|
27
|
+
findConsolidationCandidates: () => findConsolidationCandidates,
|
|
28
|
+
findSemanticClusters: () => findSemanticClusters,
|
|
24
29
|
generateSummary: () => generateSummary,
|
|
25
|
-
|
|
30
|
+
getCoUsageData: () => getCoUsageData,
|
|
31
|
+
getSmartDefaults: () => getSmartDefaults,
|
|
32
|
+
inferDomainFromSemantics: () => inferDomainFromSemantics
|
|
26
33
|
});
|
|
27
34
|
module.exports = __toCommonJS(index_exports);
|
|
28
35
|
var import_core2 = require("@aiready/core");
|
|
29
36
|
|
|
30
37
|
// src/analyzer.ts
|
|
31
38
|
var import_core = require("@aiready/core");
|
|
39
|
+
|
|
40
|
+
// src/semantic-analysis.ts
|
|
41
|
+
function buildCoUsageMatrix(graph) {
|
|
42
|
+
const coUsageMatrix = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const [sourceFile, node] of graph.nodes) {
|
|
44
|
+
const imports = node.imports;
|
|
45
|
+
for (let i = 0; i < imports.length; i++) {
|
|
46
|
+
const fileA = imports[i];
|
|
47
|
+
if (!coUsageMatrix.has(fileA)) {
|
|
48
|
+
coUsageMatrix.set(fileA, /* @__PURE__ */ new Map());
|
|
49
|
+
}
|
|
50
|
+
for (let j = i + 1; j < imports.length; j++) {
|
|
51
|
+
const fileB = imports[j];
|
|
52
|
+
const fileAUsage = coUsageMatrix.get(fileA);
|
|
53
|
+
fileAUsage.set(fileB, (fileAUsage.get(fileB) || 0) + 1);
|
|
54
|
+
if (!coUsageMatrix.has(fileB)) {
|
|
55
|
+
coUsageMatrix.set(fileB, /* @__PURE__ */ new Map());
|
|
56
|
+
}
|
|
57
|
+
const fileBUsage = coUsageMatrix.get(fileB);
|
|
58
|
+
fileBUsage.set(fileA, (fileBUsage.get(fileA) || 0) + 1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return coUsageMatrix;
|
|
63
|
+
}
|
|
64
|
+
function buildTypeGraph(graph) {
|
|
65
|
+
const typeGraph = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const [file, node] of graph.nodes) {
|
|
67
|
+
for (const exp of node.exports) {
|
|
68
|
+
if (exp.typeReferences) {
|
|
69
|
+
for (const typeRef of exp.typeReferences) {
|
|
70
|
+
if (!typeGraph.has(typeRef)) {
|
|
71
|
+
typeGraph.set(typeRef, /* @__PURE__ */ new Set());
|
|
72
|
+
}
|
|
73
|
+
typeGraph.get(typeRef).add(file);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return typeGraph;
|
|
79
|
+
}
|
|
80
|
+
function findSemanticClusters(coUsageMatrix, minCoUsage = 3) {
|
|
81
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
82
|
+
const visited = /* @__PURE__ */ new Set();
|
|
83
|
+
for (const [file, coUsages] of coUsageMatrix) {
|
|
84
|
+
if (visited.has(file)) continue;
|
|
85
|
+
const cluster = [file];
|
|
86
|
+
visited.add(file);
|
|
87
|
+
for (const [relatedFile, count] of coUsages) {
|
|
88
|
+
if (count >= minCoUsage && !visited.has(relatedFile)) {
|
|
89
|
+
cluster.push(relatedFile);
|
|
90
|
+
visited.add(relatedFile);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (cluster.length > 1) {
|
|
94
|
+
clusters.set(file, cluster);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return clusters;
|
|
98
|
+
}
|
|
99
|
+
function calculateDomainConfidence(signals) {
|
|
100
|
+
const weights = {
|
|
101
|
+
coUsage: 0.35,
|
|
102
|
+
// Strongest signal: actual usage patterns
|
|
103
|
+
typeReference: 0.3,
|
|
104
|
+
// Strong signal: shared types
|
|
105
|
+
exportName: 0.15,
|
|
106
|
+
// Medium signal: identifier semantics
|
|
107
|
+
importPath: 0.1,
|
|
108
|
+
// Weaker signal: path structure
|
|
109
|
+
folderStructure: 0.1
|
|
110
|
+
// Weakest signal: organization convention
|
|
111
|
+
};
|
|
112
|
+
let confidence = 0;
|
|
113
|
+
if (signals.coUsage) confidence += weights.coUsage;
|
|
114
|
+
if (signals.typeReference) confidence += weights.typeReference;
|
|
115
|
+
if (signals.exportName) confidence += weights.exportName;
|
|
116
|
+
if (signals.importPath) confidence += weights.importPath;
|
|
117
|
+
if (signals.folderStructure) confidence += weights.folderStructure;
|
|
118
|
+
return confidence;
|
|
119
|
+
}
|
|
120
|
+
function inferDomainFromSemantics(file, exportName, graph, coUsageMatrix, typeGraph, exportTypeRefs) {
|
|
121
|
+
const assignments = [];
|
|
122
|
+
const domainSignals = /* @__PURE__ */ new Map();
|
|
123
|
+
const coUsages = coUsageMatrix.get(file) || /* @__PURE__ */ new Map();
|
|
124
|
+
const strongCoUsages = Array.from(coUsages.entries()).filter(([_, count]) => count >= 3).map(([coFile]) => coFile);
|
|
125
|
+
for (const coFile of strongCoUsages) {
|
|
126
|
+
const coNode = graph.nodes.get(coFile);
|
|
127
|
+
if (coNode) {
|
|
128
|
+
for (const exp of coNode.exports) {
|
|
129
|
+
if (exp.inferredDomain && exp.inferredDomain !== "unknown") {
|
|
130
|
+
const domain = exp.inferredDomain;
|
|
131
|
+
if (!domainSignals.has(domain)) {
|
|
132
|
+
domainSignals.set(domain, {
|
|
133
|
+
coUsage: false,
|
|
134
|
+
typeReference: false,
|
|
135
|
+
exportName: false,
|
|
136
|
+
importPath: false,
|
|
137
|
+
folderStructure: false
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
domainSignals.get(domain).coUsage = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (exportTypeRefs) {
|
|
146
|
+
for (const typeRef of exportTypeRefs) {
|
|
147
|
+
const filesWithType = typeGraph.get(typeRef);
|
|
148
|
+
if (filesWithType) {
|
|
149
|
+
for (const typeFile of filesWithType) {
|
|
150
|
+
if (typeFile !== file) {
|
|
151
|
+
const typeNode = graph.nodes.get(typeFile);
|
|
152
|
+
if (typeNode) {
|
|
153
|
+
for (const exp of typeNode.exports) {
|
|
154
|
+
if (exp.inferredDomain && exp.inferredDomain !== "unknown") {
|
|
155
|
+
const domain = exp.inferredDomain;
|
|
156
|
+
if (!domainSignals.has(domain)) {
|
|
157
|
+
domainSignals.set(domain, {
|
|
158
|
+
coUsage: false,
|
|
159
|
+
typeReference: false,
|
|
160
|
+
exportName: false,
|
|
161
|
+
importPath: false,
|
|
162
|
+
folderStructure: false
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
domainSignals.get(domain).typeReference = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const [domain, signals] of domainSignals) {
|
|
175
|
+
const confidence = calculateDomainConfidence(signals);
|
|
176
|
+
if (confidence >= 0.3) {
|
|
177
|
+
assignments.push({ domain, confidence, signals });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
assignments.sort((a, b) => b.confidence - a.confidence);
|
|
181
|
+
return assignments;
|
|
182
|
+
}
|
|
183
|
+
function getCoUsageData(file, coUsageMatrix) {
|
|
184
|
+
const coImportedWith = coUsageMatrix.get(file) || /* @__PURE__ */ new Map();
|
|
185
|
+
const sharedImporters = [];
|
|
186
|
+
return {
|
|
187
|
+
file,
|
|
188
|
+
coImportedWith,
|
|
189
|
+
sharedImporters
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function findConsolidationCandidates(graph, coUsageMatrix, typeGraph, minCoUsage = 5, minSharedTypes = 2) {
|
|
193
|
+
const candidates = [];
|
|
194
|
+
for (const [fileA, coUsages] of coUsageMatrix) {
|
|
195
|
+
const nodeA = graph.nodes.get(fileA);
|
|
196
|
+
if (!nodeA) continue;
|
|
197
|
+
for (const [fileB, coUsageCount] of coUsages) {
|
|
198
|
+
if (fileB <= fileA) continue;
|
|
199
|
+
if (coUsageCount < minCoUsage) continue;
|
|
200
|
+
const nodeB = graph.nodes.get(fileB);
|
|
201
|
+
if (!nodeB) continue;
|
|
202
|
+
const typesA = new Set(nodeA.exports.flatMap((e) => e.typeReferences || []));
|
|
203
|
+
const typesB = new Set(nodeB.exports.flatMap((e) => e.typeReferences || []));
|
|
204
|
+
const sharedTypes = Array.from(typesA).filter((t) => typesB.has(t));
|
|
205
|
+
if (sharedTypes.length >= minSharedTypes) {
|
|
206
|
+
const strength = coUsageCount / 10 + sharedTypes.length / 5;
|
|
207
|
+
candidates.push({
|
|
208
|
+
files: [fileA, fileB],
|
|
209
|
+
reason: `High co-usage (${coUsageCount}x) and ${sharedTypes.length} shared types`,
|
|
210
|
+
strength
|
|
211
|
+
});
|
|
212
|
+
} else if (coUsageCount >= minCoUsage * 2) {
|
|
213
|
+
const strength = coUsageCount / 10;
|
|
214
|
+
candidates.push({
|
|
215
|
+
files: [fileA, fileB],
|
|
216
|
+
reason: `Very high co-usage (${coUsageCount}x)`,
|
|
217
|
+
strength
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
candidates.sort((a, b) => b.strength - a.strength);
|
|
223
|
+
return candidates;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/analyzer.ts
|
|
227
|
+
function extractDomainKeywordsFromPaths(files) {
|
|
228
|
+
const folderNames = /* @__PURE__ */ new Set();
|
|
229
|
+
for (const { file } of files) {
|
|
230
|
+
const segments = file.split("/");
|
|
231
|
+
const skipFolders = /* @__PURE__ */ new Set(["src", "lib", "dist", "build", "node_modules", "test", "tests", "__tests__", "spec", "e2e", "scripts", "components", "utils", "helpers", "util", "helper", "api", "apis"]);
|
|
232
|
+
for (const segment of segments) {
|
|
233
|
+
const normalized = segment.toLowerCase();
|
|
234
|
+
if (normalized && !skipFolders.has(normalized) && !normalized.includes(".")) {
|
|
235
|
+
const singular = singularize(normalized);
|
|
236
|
+
folderNames.add(singular);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return Array.from(folderNames);
|
|
241
|
+
}
|
|
242
|
+
function singularize(word) {
|
|
243
|
+
const irregulars = {
|
|
244
|
+
people: "person",
|
|
245
|
+
children: "child",
|
|
246
|
+
men: "man",
|
|
247
|
+
women: "woman"
|
|
248
|
+
};
|
|
249
|
+
if (irregulars[word]) {
|
|
250
|
+
return irregulars[word];
|
|
251
|
+
}
|
|
252
|
+
if (word.endsWith("ies")) {
|
|
253
|
+
return word.slice(0, -3) + "y";
|
|
254
|
+
}
|
|
255
|
+
if (word.endsWith("ses")) {
|
|
256
|
+
return word.slice(0, -2);
|
|
257
|
+
}
|
|
258
|
+
if (word.endsWith("s") && word.length > 3) {
|
|
259
|
+
return word.slice(0, -1);
|
|
260
|
+
}
|
|
261
|
+
return word;
|
|
262
|
+
}
|
|
32
263
|
function buildDependencyGraph(files) {
|
|
33
264
|
const nodes = /* @__PURE__ */ new Map();
|
|
34
265
|
const edges = /* @__PURE__ */ new Map();
|
|
266
|
+
const autoDetectedKeywords = extractDomainKeywordsFromPaths(files);
|
|
35
267
|
for (const { file, content } of files) {
|
|
36
268
|
const imports = extractImportsFromContent(content);
|
|
37
|
-
const exports2 = extractExportsWithAST(content, file);
|
|
269
|
+
const exports2 = extractExportsWithAST(content, file, { domainKeywords: autoDetectedKeywords }, imports);
|
|
38
270
|
const tokenCost = (0, import_core.estimateTokens)(content);
|
|
39
271
|
const linesOfCode = content.split("\n").length;
|
|
40
272
|
nodes.set(file, {
|
|
@@ -46,7 +278,28 @@ function buildDependencyGraph(files) {
|
|
|
46
278
|
});
|
|
47
279
|
edges.set(file, new Set(imports));
|
|
48
280
|
}
|
|
49
|
-
|
|
281
|
+
const graph = { nodes, edges };
|
|
282
|
+
const coUsageMatrix = buildCoUsageMatrix(graph);
|
|
283
|
+
const typeGraph = buildTypeGraph(graph);
|
|
284
|
+
graph.coUsageMatrix = coUsageMatrix;
|
|
285
|
+
graph.typeGraph = typeGraph;
|
|
286
|
+
for (const [file, node] of nodes) {
|
|
287
|
+
for (const exp of node.exports) {
|
|
288
|
+
const semanticAssignments = inferDomainFromSemantics(
|
|
289
|
+
file,
|
|
290
|
+
exp.name,
|
|
291
|
+
graph,
|
|
292
|
+
coUsageMatrix,
|
|
293
|
+
typeGraph,
|
|
294
|
+
exp.typeReferences
|
|
295
|
+
);
|
|
296
|
+
exp.domains = semanticAssignments;
|
|
297
|
+
if (semanticAssignments.length > 0) {
|
|
298
|
+
exp.inferredDomain = semanticAssignments[0].domain;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return graph;
|
|
50
303
|
}
|
|
51
304
|
function extractImportsFromContent(content) {
|
|
52
305
|
const imports = [];
|
|
@@ -62,7 +315,7 @@ function extractImportsFromContent(content) {
|
|
|
62
315
|
let match;
|
|
63
316
|
while ((match = pattern.exec(content)) !== null) {
|
|
64
317
|
const importPath = match[1];
|
|
65
|
-
if (importPath && !importPath.startsWith("
|
|
318
|
+
if (importPath && !importPath.startsWith("node:")) {
|
|
66
319
|
imports.push(importPath);
|
|
67
320
|
}
|
|
68
321
|
}
|
|
@@ -202,7 +455,7 @@ function detectModuleClusters(graph) {
|
|
|
202
455
|
}
|
|
203
456
|
return clusters.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
|
|
204
457
|
}
|
|
205
|
-
function extractExports(content) {
|
|
458
|
+
function extractExports(content, filePath, domainOptions, fileImports) {
|
|
206
459
|
const exports2 = [];
|
|
207
460
|
const patterns = [
|
|
208
461
|
/export\s+function\s+(\w+)/g,
|
|
@@ -225,15 +478,20 @@ function extractExports(content) {
|
|
|
225
478
|
while ((match = pattern.exec(content)) !== null) {
|
|
226
479
|
const name = match[1] || "default";
|
|
227
480
|
const type = types[index];
|
|
228
|
-
const inferredDomain = inferDomain(name);
|
|
481
|
+
const inferredDomain = inferDomain(name, filePath, domainOptions, fileImports);
|
|
229
482
|
exports2.push({ name, type, inferredDomain });
|
|
230
483
|
}
|
|
231
484
|
});
|
|
232
485
|
return exports2;
|
|
233
486
|
}
|
|
234
|
-
function inferDomain(name) {
|
|
487
|
+
function inferDomain(name, filePath, domainOptions, fileImports) {
|
|
235
488
|
const lower = name.toLowerCase();
|
|
236
|
-
const
|
|
489
|
+
const tokens = Array.from(
|
|
490
|
+
new Set(
|
|
491
|
+
lower.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^a-z0-9]+/gi, " ").split(" ").filter(Boolean)
|
|
492
|
+
)
|
|
493
|
+
);
|
|
494
|
+
const defaultKeywords = [
|
|
237
495
|
"authentication",
|
|
238
496
|
"authorization",
|
|
239
497
|
"payment",
|
|
@@ -250,14 +508,11 @@ function inferDomain(name) {
|
|
|
250
508
|
"config",
|
|
251
509
|
"model",
|
|
252
510
|
"view",
|
|
253
|
-
"auth"
|
|
254
|
-
"api",
|
|
255
|
-
"helper",
|
|
256
|
-
"util"
|
|
511
|
+
"auth"
|
|
257
512
|
];
|
|
513
|
+
const domainKeywords = domainOptions?.domainKeywords && domainOptions.domainKeywords.length ? [...domainOptions.domainKeywords, ...defaultKeywords] : defaultKeywords;
|
|
258
514
|
for (const keyword of domainKeywords) {
|
|
259
|
-
|
|
260
|
-
if (wordBoundaryPattern.test(name)) {
|
|
515
|
+
if (tokens.includes(keyword)) {
|
|
261
516
|
return keyword;
|
|
262
517
|
}
|
|
263
518
|
}
|
|
@@ -266,6 +521,37 @@ function inferDomain(name) {
|
|
|
266
521
|
return keyword;
|
|
267
522
|
}
|
|
268
523
|
}
|
|
524
|
+
if (fileImports && fileImports.length > 0) {
|
|
525
|
+
for (const importPath of fileImports) {
|
|
526
|
+
const allSegments = importPath.split("/");
|
|
527
|
+
const relevantSegments = allSegments.filter((s) => {
|
|
528
|
+
if (!s) return false;
|
|
529
|
+
if (s === "." || s === "..") return false;
|
|
530
|
+
if (s.startsWith("@") && s.length === 1) return false;
|
|
531
|
+
return true;
|
|
532
|
+
}).map((s) => s.startsWith("@") ? s.slice(1) : s);
|
|
533
|
+
for (const segment of relevantSegments) {
|
|
534
|
+
const segLower = segment.toLowerCase();
|
|
535
|
+
const singularSegment = singularize(segLower);
|
|
536
|
+
for (const keyword of domainKeywords) {
|
|
537
|
+
if (singularSegment === keyword || segLower === keyword || segLower.includes(keyword)) {
|
|
538
|
+
return keyword;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (filePath) {
|
|
545
|
+
const pathSegments = filePath.toLowerCase().split("/");
|
|
546
|
+
for (const segment of pathSegments) {
|
|
547
|
+
const singularSegment = singularize(segment);
|
|
548
|
+
for (const keyword of domainKeywords) {
|
|
549
|
+
if (singularSegment === keyword || segment === keyword || segment.includes(keyword)) {
|
|
550
|
+
return keyword;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
269
555
|
return "unknown";
|
|
270
556
|
}
|
|
271
557
|
function generateConsolidationPlan(domain, files, targetFiles) {
|
|
@@ -294,18 +580,18 @@ function generateConsolidationPlan(domain, files, targetFiles) {
|
|
|
294
580
|
);
|
|
295
581
|
return plan;
|
|
296
582
|
}
|
|
297
|
-
function extractExportsWithAST(content, filePath) {
|
|
583
|
+
function extractExportsWithAST(content, filePath, domainOptions, fileImports) {
|
|
298
584
|
try {
|
|
299
585
|
const { exports: astExports } = (0, import_core.parseFileExports)(content, filePath);
|
|
300
586
|
return astExports.map((exp) => ({
|
|
301
587
|
name: exp.name,
|
|
302
588
|
type: exp.type,
|
|
303
|
-
inferredDomain: inferDomain(exp.name),
|
|
589
|
+
inferredDomain: inferDomain(exp.name, filePath, domainOptions, fileImports),
|
|
304
590
|
imports: exp.imports,
|
|
305
591
|
dependencies: exp.dependencies
|
|
306
592
|
}));
|
|
307
593
|
} catch (error) {
|
|
308
|
-
return extractExports(content);
|
|
594
|
+
return extractExports(content, filePath, domainOptions, fileImports);
|
|
309
595
|
}
|
|
310
596
|
}
|
|
311
597
|
function calculateEnhancedCohesion(exports2, filePath) {
|
|
@@ -699,6 +985,13 @@ function downgradeSeverity(s) {
|
|
|
699
985
|
// Annotate the CommonJS export names for ESM import in node:
|
|
700
986
|
0 && (module.exports = {
|
|
701
987
|
analyzeContext,
|
|
988
|
+
buildCoUsageMatrix,
|
|
989
|
+
buildTypeGraph,
|
|
990
|
+
calculateDomainConfidence,
|
|
991
|
+
findConsolidationCandidates,
|
|
992
|
+
findSemanticClusters,
|
|
702
993
|
generateSummary,
|
|
703
|
-
|
|
994
|
+
getCoUsageData,
|
|
995
|
+
getSmartDefaults,
|
|
996
|
+
inferDomainFromSemantics
|
|
704
997
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
analyzeContext,
|
|
3
|
+
buildCoUsageMatrix,
|
|
4
|
+
buildTypeGraph,
|
|
5
|
+
calculateDomainConfidence,
|
|
6
|
+
findConsolidationCandidates,
|
|
7
|
+
findSemanticClusters,
|
|
3
8
|
generateSummary,
|
|
4
|
-
|
|
5
|
-
|
|
9
|
+
getCoUsageData,
|
|
10
|
+
getSmartDefaults,
|
|
11
|
+
inferDomainFromSemantics
|
|
12
|
+
} from "./chunk-DMRZMS2U.mjs";
|
|
6
13
|
export {
|
|
7
14
|
analyzeContext,
|
|
15
|
+
buildCoUsageMatrix,
|
|
16
|
+
buildTypeGraph,
|
|
17
|
+
calculateDomainConfidence,
|
|
18
|
+
findConsolidationCandidates,
|
|
19
|
+
findSemanticClusters,
|
|
8
20
|
generateSummary,
|
|
9
|
-
|
|
21
|
+
getCoUsageData,
|
|
22
|
+
getSmartDefaults,
|
|
23
|
+
inferDomainFromSemantics
|
|
10
24
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/context-analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
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",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"commander": "^12.1.0",
|
|
51
51
|
"chalk": "^5.3.0",
|
|
52
52
|
"prompts": "^2.4.2",
|
|
53
|
-
"@aiready/core": "0.
|
|
53
|
+
"@aiready/core": "0.7.1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/node": "^22.10.2",
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildDependencyGraph } from '../analyzer';
|
|
3
|
+
|
|
4
|
+
describe('Auto-detection from folder structure', () => {
|
|
5
|
+
it('should auto-detect domain keywords from folder paths', () => {
|
|
6
|
+
const files = [
|
|
7
|
+
{
|
|
8
|
+
file: 'src/payments/process.ts',
|
|
9
|
+
content: 'export function processPayment() { return 1; }',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
file: 'src/orders/create.ts',
|
|
13
|
+
content: 'export function createOrder() { return 2; }',
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const graph = buildDependencyGraph(files);
|
|
18
|
+
const paymentsNode = graph.nodes.get('src/payments/process.ts');
|
|
19
|
+
const ordersNode = graph.nodes.get('src/orders/create.ts');
|
|
20
|
+
|
|
21
|
+
// Should detect 'payment' from processPayment (now part of auto-detected keywords)
|
|
22
|
+
expect(paymentsNode?.exports[0].inferredDomain).toBe('payment');
|
|
23
|
+
|
|
24
|
+
// Should detect 'order' from createOrder
|
|
25
|
+
expect(ordersNode?.exports[0].inferredDomain).toBe('order');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should detect domains from nested folders', () => {
|
|
29
|
+
const files = [
|
|
30
|
+
{
|
|
31
|
+
file: 'src/api/invoices/handler.ts',
|
|
32
|
+
content: 'export function handleRequest() { return 1; }',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const graph = buildDependencyGraph(files);
|
|
37
|
+
const node = graph.nodes.get('src/api/invoices/handler.ts');
|
|
38
|
+
|
|
39
|
+
// Should detect 'invoice' from path (invoices folder)
|
|
40
|
+
expect(node?.exports[0].inferredDomain).toBe('invoice');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should skip common infrastructure folders', () => {
|
|
44
|
+
const files = [
|
|
45
|
+
{
|
|
46
|
+
file: 'src/utils/helpers/format.ts',
|
|
47
|
+
content: 'export function formatData() { return 1; }',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const graph = buildDependencyGraph(files);
|
|
52
|
+
const node = graph.nodes.get('src/utils/helpers/format.ts');
|
|
53
|
+
|
|
54
|
+
// 'utils' and 'helpers' should be skipped, no domain detected
|
|
55
|
+
expect(node?.exports[0].inferredDomain).toBe('unknown');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should merge auto-detected with custom keywords', () => {
|
|
59
|
+
const files = [
|
|
60
|
+
{
|
|
61
|
+
file: 'src/receipts/scan.ts',
|
|
62
|
+
content: 'export function scanReceipt() { return 1; }',
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const graph = buildDependencyGraph(files, {
|
|
67
|
+
domainKeywords: ['receipt'], // Custom keyword
|
|
68
|
+
});
|
|
69
|
+
const node = graph.nodes.get('src/receipts/scan.ts');
|
|
70
|
+
|
|
71
|
+
// Should detect 'receipt' from both auto-detection AND custom keywords
|
|
72
|
+
expect(node?.exports[0].inferredDomain).toBe('receipt');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('Import-path domain inference', () => {
|
|
77
|
+
it('should infer domain from import paths', () => {
|
|
78
|
+
const files = [
|
|
79
|
+
{
|
|
80
|
+
file: 'src/lib/session.ts',
|
|
81
|
+
content: `
|
|
82
|
+
import { processPayment } from '../payments/processor';
|
|
83
|
+
export function createSession() { return 1; }
|
|
84
|
+
`,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
file: 'src/payments/processor.ts',
|
|
88
|
+
content: 'export function processPayment() { return 2; }',
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const graph = buildDependencyGraph(files);
|
|
93
|
+
const sessionNode = graph.nodes.get('src/lib/session.ts');
|
|
94
|
+
|
|
95
|
+
// session.ts imports from '../payments/...' so should infer 'payment' domain
|
|
96
|
+
expect(sessionNode?.exports[0].inferredDomain).toBe('payment');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should infer domain from absolute import paths', () => {
|
|
100
|
+
const files = [
|
|
101
|
+
{
|
|
102
|
+
file: 'src/components/nav-links.ts',
|
|
103
|
+
content: `
|
|
104
|
+
import { getOrders } from '@/orders/service';
|
|
105
|
+
export function NavLinks() { return 'nav'; }
|
|
106
|
+
`,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
file: 'src/orders/service.ts',
|
|
110
|
+
content: 'export function getOrders() { return []; }',
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const graph = buildDependencyGraph(files);
|
|
115
|
+
const navNode = graph.nodes.get('src/components/nav-links.ts');
|
|
116
|
+
|
|
117
|
+
// nav-links.ts imports from '@/orders/...' so should infer 'order' domain
|
|
118
|
+
expect(navNode?.exports[0].inferredDomain).toBe('order');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should use identifier name first before import-path fallback', () => {
|
|
122
|
+
const files = [
|
|
123
|
+
{
|
|
124
|
+
file: 'src/lib/handler.ts',
|
|
125
|
+
content: `
|
|
126
|
+
import { processPayment } from '../payments/processor';
|
|
127
|
+
export function processInvoice() { return 1; }
|
|
128
|
+
`,
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const graph = buildDependencyGraph(files);
|
|
133
|
+
const node = graph.nodes.get('src/lib/handler.ts');
|
|
134
|
+
|
|
135
|
+
// processInvoice should match 'invoice' from identifier, not 'payment' from imports
|
|
136
|
+
expect(node?.exports[0].inferredDomain).toBe('invoice');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should fall back to import-path when identifier is generic', () => {
|
|
140
|
+
const files = [
|
|
141
|
+
{
|
|
142
|
+
file: 'src/lib/dynamodb.ts',
|
|
143
|
+
content: `
|
|
144
|
+
import { Customer } from '../customers/model';
|
|
145
|
+
export function connect() { return 1; }
|
|
146
|
+
`,
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const graph = buildDependencyGraph(files);
|
|
151
|
+
const node = graph.nodes.get('src/lib/dynamodb.ts');
|
|
152
|
+
|
|
153
|
+
// 'connect' is generic, should infer 'customer' from import path
|
|
154
|
+
expect(node?.exports[0].inferredDomain).toBe('customer');
|
|
155
|
+
});
|
|
156
|
+
});
|