@aiready/context-analyzer 0.21.22 → 0.21.23
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 +26 -25
- package/.turbo/turbo-lint.log +5 -5
- package/.turbo/turbo-test.log +104 -41
- package/coverage/clover.xml +2615 -1242
- package/coverage/coverage-final.json +30 -13
- package/coverage/dist/chunk-64U3PNO3.mjs.html +367 -0
- package/coverage/dist/chunk-J3MUOWHC.mjs.html +5326 -0
- package/coverage/dist/index.html +146 -0
- package/coverage/{classifier.ts.html → dist/index.mjs.html} +537 -912
- package/coverage/index.html +84 -189
- package/coverage/src/analyzer.ts.html +88 -0
- package/coverage/src/analyzers/index.html +116 -0
- package/coverage/src/analyzers/python-context.ts.html +910 -0
- package/coverage/{ast-utils.ts.html → src/ast-utils.ts.html} +84 -54
- package/coverage/src/classifier.ts.html +892 -0
- package/coverage/src/classify/classification-patterns.ts.html +307 -0
- package/coverage/src/classify/file-classifiers.ts.html +973 -0
- package/coverage/src/classify/index.html +131 -0
- package/coverage/{cluster-detector.ts.html → src/cluster-detector.ts.html} +154 -91
- package/coverage/{defaults.ts.html → src/defaults.ts.html} +74 -65
- package/coverage/{graph-builder.ts.html → src/graph-builder.ts.html} +268 -229
- package/coverage/src/index.html +341 -0
- package/coverage/{index.ts.html → src/index.ts.html} +70 -13
- package/coverage/{scoring.ts.html → src/issue-analyzer.ts.html} +201 -261
- package/coverage/src/mapper.ts.html +439 -0
- package/coverage/{metrics.ts.html → src/metrics.ts.html} +201 -132
- package/coverage/src/orchestrator.ts.html +493 -0
- package/coverage/{provider.ts.html → src/provider.ts.html} +21 -21
- package/coverage/{remediation.ts.html → src/remediation.ts.html} +112 -52
- package/coverage/src/report/console-report.ts.html +415 -0
- package/coverage/src/report/html-report.ts.html +361 -0
- package/coverage/src/report/index.html +146 -0
- package/coverage/src/report/interactive-setup.ts.html +373 -0
- package/coverage/src/scoring.ts.html +895 -0
- package/coverage/src/semantic/co-usage.ts.html +340 -0
- package/coverage/src/semantic/consolidation.ts.html +223 -0
- package/coverage/src/semantic/domain-inference.ts.html +859 -0
- package/coverage/src/semantic/index.html +161 -0
- package/coverage/src/semantic/type-graph.ts.html +163 -0
- package/coverage/{summary.ts.html → src/summary.ts.html} +155 -275
- package/coverage/{types.ts.html → src/types.ts.html} +133 -31
- package/coverage/src/utils/dependency-graph-utils.ts.html +463 -0
- package/coverage/src/utils/index.html +131 -0
- package/coverage/src/utils/string-utils.ts.html +148 -0
- package/dist/chunk-J3MUOWHC.mjs +1747 -0
- package/dist/cli.js +59 -171
- package/dist/cli.mjs +1 -1
- package/dist/index.js +55 -167
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/__tests__/consolidation.test.ts +247 -0
- package/src/__tests__/defaults.test.ts +121 -0
- package/src/__tests__/domain-inference.test.ts +420 -0
- package/src/__tests__/issue-analyzer.test.ts +155 -0
- package/src/__tests__/orchestrator.test.ts +143 -0
- package/src/__tests__/python-context.test.ts +98 -0
- package/src/__tests__/report/console-report.test.ts +292 -0
- package/src/__tests__/report/html-report.test.ts +232 -0
- package/src/report/html-report.ts +58 -174
- package/coverage/analyzer.ts.html +0 -1369
- package/coverage/semantic-analysis.ts.html +0 -1201
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
calculateDomainConfidence,
|
|
4
|
+
inferDomainFromSemantics,
|
|
5
|
+
extractExports,
|
|
6
|
+
inferDomain,
|
|
7
|
+
} from '../semantic/domain-inference';
|
|
8
|
+
import type { DependencyGraph, DependencyNode } from '../types';
|
|
9
|
+
|
|
10
|
+
describe('calculateDomainConfidence', () => {
|
|
11
|
+
it('should return 0 for no signals', () => {
|
|
12
|
+
const confidence = calculateDomainConfidence({
|
|
13
|
+
coUsage: false,
|
|
14
|
+
typeReference: false,
|
|
15
|
+
exportName: false,
|
|
16
|
+
importPath: false,
|
|
17
|
+
folderStructure: false,
|
|
18
|
+
});
|
|
19
|
+
expect(confidence).toBe(0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should return weighted confidence for single signal', () => {
|
|
23
|
+
const confidence = calculateDomainConfidence({
|
|
24
|
+
coUsage: true,
|
|
25
|
+
typeReference: false,
|
|
26
|
+
exportName: false,
|
|
27
|
+
importPath: false,
|
|
28
|
+
folderStructure: false,
|
|
29
|
+
});
|
|
30
|
+
expect(confidence).toBe(0.35);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should return weighted confidence for multiple signals', () => {
|
|
34
|
+
const confidence = calculateDomainConfidence({
|
|
35
|
+
coUsage: true,
|
|
36
|
+
typeReference: true,
|
|
37
|
+
exportName: true,
|
|
38
|
+
importPath: false,
|
|
39
|
+
folderStructure: false,
|
|
40
|
+
});
|
|
41
|
+
// coUsage(0.35) + typeReference(0.3) + exportName(0.15) = 0.8, but max is 1
|
|
42
|
+
expect(confidence).toBeGreaterThan(0.7);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return 1 for all signals', () => {
|
|
46
|
+
const confidence = calculateDomainConfidence({
|
|
47
|
+
coUsage: true,
|
|
48
|
+
typeReference: true,
|
|
49
|
+
exportName: true,
|
|
50
|
+
importPath: true,
|
|
51
|
+
folderStructure: true,
|
|
52
|
+
});
|
|
53
|
+
// Sum is 1.0 but function may cap or floor
|
|
54
|
+
expect(confidence).toBeGreaterThanOrEqual(0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('inferDomainFromSemantics', () => {
|
|
59
|
+
const createMockGraph = (
|
|
60
|
+
nodes: Map<string, DependencyNode>
|
|
61
|
+
): DependencyGraph => ({
|
|
62
|
+
nodes,
|
|
63
|
+
edges: new Map(),
|
|
64
|
+
coUsageMatrix: new Map(),
|
|
65
|
+
typeGraph: new Map(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return empty array for no co-usage or type refs', () => {
|
|
69
|
+
const nodes = new Map<string, DependencyNode>();
|
|
70
|
+
const graph = createMockGraph(nodes);
|
|
71
|
+
const coUsageMatrix = new Map<string, Map<string, number>>();
|
|
72
|
+
const typeGraph = new Map<string, Set<string>>();
|
|
73
|
+
|
|
74
|
+
const assignments = inferDomainFromSemantics(
|
|
75
|
+
'fileA.ts',
|
|
76
|
+
'testExport',
|
|
77
|
+
graph,
|
|
78
|
+
coUsageMatrix,
|
|
79
|
+
typeGraph
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(assignments).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should find domain from strong co-usage', () => {
|
|
86
|
+
const nodes = new Map<string, DependencyNode>([
|
|
87
|
+
[
|
|
88
|
+
'fileA.ts',
|
|
89
|
+
{
|
|
90
|
+
file: 'fileA.ts',
|
|
91
|
+
imports: [],
|
|
92
|
+
exports: [],
|
|
93
|
+
tokenCost: 100,
|
|
94
|
+
linesOfCode: 50,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
'fileB.ts',
|
|
99
|
+
{
|
|
100
|
+
file: 'fileB.ts',
|
|
101
|
+
imports: [],
|
|
102
|
+
exports: [
|
|
103
|
+
{ name: 'UserService', type: 'class', inferredDomain: 'user' },
|
|
104
|
+
],
|
|
105
|
+
tokenCost: 100,
|
|
106
|
+
linesOfCode: 50,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
]);
|
|
110
|
+
const graph = createMockGraph(nodes);
|
|
111
|
+
|
|
112
|
+
const coUsageMatrix = new Map<string, Map<string, number>>();
|
|
113
|
+
coUsageMatrix.set('fileA.ts', new Map([['fileB.ts', 5]]));
|
|
114
|
+
|
|
115
|
+
const typeGraph = new Map<string, Set<string>>();
|
|
116
|
+
|
|
117
|
+
const assignments = inferDomainFromSemantics(
|
|
118
|
+
'fileA.ts',
|
|
119
|
+
'testExport',
|
|
120
|
+
graph,
|
|
121
|
+
coUsageMatrix,
|
|
122
|
+
typeGraph
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(assignments.length).toBeGreaterThan(0);
|
|
126
|
+
expect(assignments[0].domain).toBe('user');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should find domain from type references', () => {
|
|
130
|
+
const nodes = new Map<string, DependencyNode>([
|
|
131
|
+
[
|
|
132
|
+
'fileA.ts',
|
|
133
|
+
{
|
|
134
|
+
file: 'fileA.ts',
|
|
135
|
+
imports: [],
|
|
136
|
+
exports: [],
|
|
137
|
+
tokenCost: 100,
|
|
138
|
+
linesOfCode: 50,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
'fileB.ts',
|
|
143
|
+
{
|
|
144
|
+
file: 'fileB.ts',
|
|
145
|
+
imports: [],
|
|
146
|
+
exports: [
|
|
147
|
+
{ name: 'UserType', type: 'interface', inferredDomain: 'user' },
|
|
148
|
+
],
|
|
149
|
+
tokenCost: 100,
|
|
150
|
+
linesOfCode: 50,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
]);
|
|
154
|
+
const graph = createMockGraph(nodes);
|
|
155
|
+
|
|
156
|
+
const coUsageMatrix = new Map<string, Map<string, number>>();
|
|
157
|
+
|
|
158
|
+
const typeGraph = new Map<string, Set<string>>();
|
|
159
|
+
typeGraph.set('UserType', new Set(['fileB.ts']));
|
|
160
|
+
|
|
161
|
+
const assignments = inferDomainFromSemantics(
|
|
162
|
+
'fileA.ts',
|
|
163
|
+
'testExport',
|
|
164
|
+
graph,
|
|
165
|
+
coUsageMatrix,
|
|
166
|
+
typeGraph,
|
|
167
|
+
['UserType']
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect(assignments.length).toBeGreaterThan(0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should filter out low confidence assignments', () => {
|
|
174
|
+
const nodes = new Map<string, DependencyNode>([
|
|
175
|
+
[
|
|
176
|
+
'fileA.ts',
|
|
177
|
+
{
|
|
178
|
+
file: 'fileA.ts',
|
|
179
|
+
imports: [],
|
|
180
|
+
exports: [],
|
|
181
|
+
tokenCost: 100,
|
|
182
|
+
linesOfCode: 50,
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
[
|
|
186
|
+
'fileB.ts',
|
|
187
|
+
{
|
|
188
|
+
file: 'fileB.ts',
|
|
189
|
+
imports: [],
|
|
190
|
+
exports: [{ name: 'test', type: 'const', inferredDomain: 'unknown' }],
|
|
191
|
+
tokenCost: 100,
|
|
192
|
+
linesOfCode: 50,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
]);
|
|
196
|
+
const graph = createMockGraph(nodes);
|
|
197
|
+
|
|
198
|
+
const coUsageMatrix = new Map<string, Map<string, number>>();
|
|
199
|
+
coUsageMatrix.set('fileA.ts', new Map([['fileB.ts', 2]])); // Below threshold
|
|
200
|
+
|
|
201
|
+
const typeGraph = new Map<string, Set<string>>();
|
|
202
|
+
|
|
203
|
+
const assignments = inferDomainFromSemantics(
|
|
204
|
+
'fileA.ts',
|
|
205
|
+
'testExport',
|
|
206
|
+
graph,
|
|
207
|
+
coUsageMatrix,
|
|
208
|
+
typeGraph
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Should be filtered out due to low confidence
|
|
212
|
+
expect(assignments.length).toBe(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should sort assignments by confidence descending', () => {
|
|
216
|
+
const nodes = new Map<string, DependencyNode>([
|
|
217
|
+
[
|
|
218
|
+
'fileA.ts',
|
|
219
|
+
{
|
|
220
|
+
file: 'fileA.ts',
|
|
221
|
+
imports: [],
|
|
222
|
+
exports: [],
|
|
223
|
+
tokenCost: 100,
|
|
224
|
+
linesOfCode: 50,
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
[
|
|
228
|
+
'fileB.ts',
|
|
229
|
+
{
|
|
230
|
+
file: 'fileB.ts',
|
|
231
|
+
imports: [],
|
|
232
|
+
exports: [
|
|
233
|
+
{ name: 'UserService', type: 'class', inferredDomain: 'user' },
|
|
234
|
+
],
|
|
235
|
+
tokenCost: 100,
|
|
236
|
+
linesOfCode: 50,
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
[
|
|
240
|
+
'fileC.ts',
|
|
241
|
+
{
|
|
242
|
+
file: 'fileC.ts',
|
|
243
|
+
imports: [],
|
|
244
|
+
exports: [
|
|
245
|
+
{ name: 'AuthService', type: 'class', inferredDomain: 'auth' },
|
|
246
|
+
],
|
|
247
|
+
tokenCost: 100,
|
|
248
|
+
linesOfCode: 50,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
]);
|
|
252
|
+
const graph = createMockGraph(nodes);
|
|
253
|
+
|
|
254
|
+
const coUsageMatrix = new Map<string, Map<string, number>>();
|
|
255
|
+
coUsageMatrix.set(
|
|
256
|
+
'fileA.ts',
|
|
257
|
+
new Map([
|
|
258
|
+
['fileB.ts', 5],
|
|
259
|
+
['fileC.ts', 5],
|
|
260
|
+
])
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const typeGraph = new Map<string, Set<string>>();
|
|
264
|
+
|
|
265
|
+
const assignments = inferDomainFromSemantics(
|
|
266
|
+
'fileA.ts',
|
|
267
|
+
'testExport',
|
|
268
|
+
graph,
|
|
269
|
+
coUsageMatrix,
|
|
270
|
+
typeGraph
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
expect(assignments.length).toBeGreaterThan(0);
|
|
274
|
+
for (let i = 0; i < assignments.length - 1; i++) {
|
|
275
|
+
expect(assignments[i].confidence).toBeGreaterThanOrEqual(
|
|
276
|
+
assignments[i + 1].confidence
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('extractExports', () => {
|
|
283
|
+
it('should extract function exports', () => {
|
|
284
|
+
const content = `
|
|
285
|
+
export function foo() {}
|
|
286
|
+
export function bar() {}
|
|
287
|
+
`;
|
|
288
|
+
const exports = extractExports(content);
|
|
289
|
+
|
|
290
|
+
expect(exports.length).toBe(2);
|
|
291
|
+
expect(exports[0].name).toBe('foo');
|
|
292
|
+
expect(exports[0].type).toBe('function');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should extract class exports', () => {
|
|
296
|
+
const content = `
|
|
297
|
+
export class MyClass {}
|
|
298
|
+
`;
|
|
299
|
+
const exports = extractExports(content);
|
|
300
|
+
|
|
301
|
+
expect(exports.length).toBe(1);
|
|
302
|
+
expect(exports[0].name).toBe('MyClass');
|
|
303
|
+
expect(exports[0].type).toBe('class');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should extract const exports', () => {
|
|
307
|
+
const content = `
|
|
308
|
+
export const foo = 1;
|
|
309
|
+
export const bar = 2;
|
|
310
|
+
`;
|
|
311
|
+
const exports = extractExports(content);
|
|
312
|
+
|
|
313
|
+
expect(exports.length).toBe(2);
|
|
314
|
+
expect(exports[0].type).toBe('const');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should extract type exports', () => {
|
|
318
|
+
const content = `
|
|
319
|
+
export type Foo = string;
|
|
320
|
+
`;
|
|
321
|
+
const exports = extractExports(content);
|
|
322
|
+
|
|
323
|
+
expect(exports.length).toBe(1);
|
|
324
|
+
expect(exports[0].type).toBe('type');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should extract interface exports', () => {
|
|
328
|
+
const content = `
|
|
329
|
+
export interface User {}
|
|
330
|
+
`;
|
|
331
|
+
const exports = extractExports(content);
|
|
332
|
+
|
|
333
|
+
expect(exports.length).toBe(1);
|
|
334
|
+
expect(exports[0].type).toBe('interface');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should extract default exports', () => {
|
|
338
|
+
const content = `
|
|
339
|
+
export default function() {}
|
|
340
|
+
`;
|
|
341
|
+
const exports = extractExports(content);
|
|
342
|
+
|
|
343
|
+
expect(exports.length).toBe(1);
|
|
344
|
+
expect(exports[0].type).toBe('default');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should infer domain from export name', () => {
|
|
348
|
+
const content = `
|
|
349
|
+
export function getUser() {}
|
|
350
|
+
`;
|
|
351
|
+
const exports = extractExports(content, 'src/user-service.ts');
|
|
352
|
+
|
|
353
|
+
expect(exports[0].inferredDomain).toBe('user');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should return empty for no exports', () => {
|
|
357
|
+
const content = `
|
|
358
|
+
const foo = 1;
|
|
359
|
+
function bar() {}
|
|
360
|
+
`;
|
|
361
|
+
const exports = extractExports(content);
|
|
362
|
+
|
|
363
|
+
expect(exports).toEqual([]);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('inferDomain', () => {
|
|
368
|
+
it('should return unknown for unrecognized names', () => {
|
|
369
|
+
const domain = inferDomain('xyz123');
|
|
370
|
+
expect(domain).toBe('unknown');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should infer domain from name tokens', () => {
|
|
374
|
+
const domain = inferDomain('getUser');
|
|
375
|
+
expect(domain).toBe('user');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should infer domain from name keywords', () => {
|
|
379
|
+
const domain = inferDomain('UserService');
|
|
380
|
+
expect(domain).toBe('user');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should infer domain from import paths', () => {
|
|
384
|
+
const domain = inferDomain('test', undefined, undefined, [
|
|
385
|
+
'./user/api',
|
|
386
|
+
'./auth/login',
|
|
387
|
+
]);
|
|
388
|
+
expect(domain).toBe('user');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should infer domain from file path', () => {
|
|
392
|
+
const domain = inferDomain('test', 'src/auth/login.ts');
|
|
393
|
+
expect(domain).toBe('auth');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should use custom domain keywords', () => {
|
|
397
|
+
const domain = inferDomain('customFunc', undefined, {
|
|
398
|
+
domainKeywords: ['custom'],
|
|
399
|
+
});
|
|
400
|
+
expect(domain).toBe('custom');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('should return domain from recognized keywords', () => {
|
|
404
|
+
const domain = inferDomain('getUser');
|
|
405
|
+
// Should match 'user' from keyword list
|
|
406
|
+
expect(domain).toBe('user');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should handle camelCase splitting', () => {
|
|
410
|
+
const domain = inferDomain('getUserById');
|
|
411
|
+
// Should find 'user' in the tokens
|
|
412
|
+
expect(domain).toBe('user');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should handle singularization', () => {
|
|
416
|
+
const domain = inferDomain('users', 'src/users/controller.ts');
|
|
417
|
+
// Should singularize and match
|
|
418
|
+
expect(domain).toBe('user');
|
|
419
|
+
});
|
|
420
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { analyzeIssues, isBuildArtifact } from '../issue-analyzer';
|
|
3
|
+
import { Severity } from '@aiready/core';
|
|
4
|
+
|
|
5
|
+
describe('analyzeIssues', () => {
|
|
6
|
+
const baseParams = {
|
|
7
|
+
file: 'src/test.ts',
|
|
8
|
+
importDepth: 2,
|
|
9
|
+
contextBudget: 10000,
|
|
10
|
+
cohesionScore: 0.8,
|
|
11
|
+
fragmentationScore: 0.3,
|
|
12
|
+
maxDepth: 5,
|
|
13
|
+
maxContextBudget: 25000,
|
|
14
|
+
minCohesion: 0.6,
|
|
15
|
+
maxFragmentation: 0.5,
|
|
16
|
+
circularDeps: [] as string[][],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
it('should return info severity for no issues', () => {
|
|
20
|
+
const result = analyzeIssues(baseParams);
|
|
21
|
+
|
|
22
|
+
expect(result.severity).toBe(Severity.Info);
|
|
23
|
+
expect(result.issues).toContain('No significant issues detected');
|
|
24
|
+
expect(result.potentialSavings).toBe(0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should detect circular dependencies as critical', () => {
|
|
28
|
+
const result = analyzeIssues({
|
|
29
|
+
...baseParams,
|
|
30
|
+
circularDeps: [['a.ts', 'b.ts', 'a.ts']],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(result.severity).toBe(Severity.Critical);
|
|
34
|
+
expect(result.issues[0]).toContain('1 circular dependency chain(s)');
|
|
35
|
+
expect(result.potentialSavings).toBeGreaterThan(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should detect high import depth as critical when 50% over limit', () => {
|
|
39
|
+
const result = analyzeIssues({
|
|
40
|
+
...baseParams,
|
|
41
|
+
importDepth: 8, // > 5 * 1.5
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(result.severity).toBe(Severity.Critical);
|
|
45
|
+
expect(result.issues[0]).toContain('exceeds limit by 50%');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should detect import depth over limit as major', () => {
|
|
49
|
+
const result = analyzeIssues({
|
|
50
|
+
...baseParams,
|
|
51
|
+
importDepth: 6, // > 5
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(result.severity).toBe(Severity.Major);
|
|
55
|
+
expect(result.issues[0]).toContain('exceeds recommended maximum');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should detect high context budget as critical when 50% over limit', () => {
|
|
59
|
+
const result = analyzeIssues({
|
|
60
|
+
...baseParams,
|
|
61
|
+
contextBudget: 40000, // > 25000 * 1.5
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result.severity).toBe(Severity.Critical);
|
|
65
|
+
expect(result.issues[0]).toContain('50% over limit');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should detect context budget over limit as major', () => {
|
|
69
|
+
const result = analyzeIssues({
|
|
70
|
+
...baseParams,
|
|
71
|
+
contextBudget: 30000, // > 25000
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(result.severity).toBe(Severity.Major);
|
|
75
|
+
expect(result.issues[0]).toContain('exceeds');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should detect very low cohesion as major', () => {
|
|
79
|
+
const result = analyzeIssues({
|
|
80
|
+
...baseParams,
|
|
81
|
+
cohesionScore: 0.2, // < 0.6 * 0.5
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(result.severity).toBe(Severity.Major);
|
|
85
|
+
expect(result.issues[0]).toContain('Very low cohesion');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should detect low cohesion as minor', () => {
|
|
89
|
+
const result = analyzeIssues({
|
|
90
|
+
...baseParams,
|
|
91
|
+
cohesionScore: 0.4, // < 0.6
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(result.severity).toBe(Severity.Minor);
|
|
95
|
+
expect(result.issues[0]).toContain('Low cohesion');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should detect high fragmentation', () => {
|
|
99
|
+
const result = analyzeIssues({
|
|
100
|
+
...baseParams,
|
|
101
|
+
fragmentationScore: 0.8, // > 0.5
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(result.issues[0]).toContain('High fragmentation');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should handle build artifact files and override severity to info', () => {
|
|
108
|
+
// Use values that don't trigger any other issues, and a path that matches /dist/
|
|
109
|
+
const result = analyzeIssues({
|
|
110
|
+
...baseParams,
|
|
111
|
+
file: '/project/dist/bundle.js',
|
|
112
|
+
importDepth: 1,
|
|
113
|
+
contextBudget: 5000,
|
|
114
|
+
cohesionScore: 0.9,
|
|
115
|
+
fragmentationScore: 0.1,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(result.severity).toBe(Severity.Info);
|
|
119
|
+
expect(result.issues).toContain(
|
|
120
|
+
'Detected build artifact (bundled/output file)'
|
|
121
|
+
);
|
|
122
|
+
expect(result.potentialSavings).toBe(0);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('isBuildArtifact', () => {
|
|
127
|
+
it('should detect node_modules', () => {
|
|
128
|
+
expect(isBuildArtifact('/project/node_modules/pkg/index.js')).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should detect dist folder', () => {
|
|
132
|
+
expect(isBuildArtifact('/project/dist/bundle.js')).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should detect build folder', () => {
|
|
136
|
+
expect(isBuildArtifact('/project/build/output.js')).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should detect out folder', () => {
|
|
140
|
+
expect(isBuildArtifact('/project/out/main.js')).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should detect .next folder', () => {
|
|
144
|
+
expect(isBuildArtifact('/project/.next/server.js')).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should not flag regular source files', () => {
|
|
148
|
+
expect(isBuildArtifact('/project/src/index.ts')).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should be case insensitive', () => {
|
|
152
|
+
expect(isBuildArtifact('/project/Node_Modules/pkg/index.js')).toBe(true);
|
|
153
|
+
expect(isBuildArtifact('/project/DIST/bundle.js')).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { analyzeContext, calculateCohesion } from '../orchestrator';
|
|
3
|
+
import * as core from '@aiready/core';
|
|
4
|
+
|
|
5
|
+
// Mock the core module
|
|
6
|
+
vi.mock('@aiready/core', async () => {
|
|
7
|
+
const actual = await vi.importActual('@aiready/core');
|
|
8
|
+
return {
|
|
9
|
+
...actual,
|
|
10
|
+
scanFiles: vi.fn(),
|
|
11
|
+
readFileContent: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('analyzeContext', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should analyze TypeScript files and return results', async () => {
|
|
21
|
+
const mockFiles = ['src/file1.ts', 'src/file2.ts'];
|
|
22
|
+
const mockContents: Record<string, string> = {
|
|
23
|
+
'src/file1.ts': `
|
|
24
|
+
import { b } from './file2';
|
|
25
|
+
export const a = 1;
|
|
26
|
+
`,
|
|
27
|
+
'src/file2.ts': `
|
|
28
|
+
export const b = 2;
|
|
29
|
+
`,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
vi.mocked(core.scanFiles).mockResolvedValue(mockFiles);
|
|
33
|
+
vi.mocked(core.readFileContent).mockImplementation(async (path: string) => {
|
|
34
|
+
return mockContents[path] || '';
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const results = await analyzeContext({
|
|
38
|
+
rootDir: '.',
|
|
39
|
+
maxDepth: 5,
|
|
40
|
+
maxContextBudget: 25000,
|
|
41
|
+
minCohesion: 0.6,
|
|
42
|
+
maxFragmentation: 0.5,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(results.length).toBe(2);
|
|
46
|
+
expect(results[0].file).toBeDefined();
|
|
47
|
+
expect(results[0].importDepth).toBeDefined();
|
|
48
|
+
expect(results[0].contextBudget).toBeDefined();
|
|
49
|
+
expect(core.scanFiles).toHaveBeenCalled();
|
|
50
|
+
expect(core.readFileContent).toHaveBeenCalledTimes(2);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should handle empty file list', async () => {
|
|
54
|
+
vi.mocked(core.scanFiles).mockResolvedValue([]);
|
|
55
|
+
|
|
56
|
+
const results = await analyzeContext({
|
|
57
|
+
rootDir: '.',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(results).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should filter Python files when not present', async () => {
|
|
64
|
+
const mockFiles = ['src/file1.ts', 'src/file2.py'];
|
|
65
|
+
vi.mocked(core.scanFiles).mockResolvedValue(mockFiles);
|
|
66
|
+
vi.mocked(core.readFileContent).mockResolvedValue('export const a = 1;');
|
|
67
|
+
|
|
68
|
+
const results = await analyzeContext({
|
|
69
|
+
rootDir: '.',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Should only process TypeScript files
|
|
73
|
+
expect(results.length).toBe(1);
|
|
74
|
+
expect(results[0].file).toBe('src/file1.ts');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should use default options when not provided', async () => {
|
|
78
|
+
vi.mocked(core.scanFiles).mockResolvedValue([]);
|
|
79
|
+
vi.mocked(core.readFileContent).mockResolvedValue('');
|
|
80
|
+
|
|
81
|
+
const results = await analyzeContext({
|
|
82
|
+
rootDir: '.',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(results).toEqual([]);
|
|
86
|
+
expect(core.scanFiles).toHaveBeenCalledWith(
|
|
87
|
+
expect.objectContaining({
|
|
88
|
+
rootDir: '.',
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle includeNodeModules option', async () => {
|
|
94
|
+
const mockFiles = ['node_modules/pkg/index.js', 'src/file.ts'];
|
|
95
|
+
vi.mocked(core.scanFiles).mockResolvedValue(mockFiles);
|
|
96
|
+
vi.mocked(core.readFileContent).mockResolvedValue('export const a = 1;');
|
|
97
|
+
|
|
98
|
+
const results = await analyzeContext({
|
|
99
|
+
rootDir: '.',
|
|
100
|
+
includeNodeModules: true,
|
|
101
|
+
exclude: ['**/node_modules/**'],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// When includeNodeModules is true, node_modules should be processed
|
|
105
|
+
expect(results.length).toBe(2);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('calculateCohesion', () => {
|
|
110
|
+
it('should delegate to calculateEnhancedCohesion', () => {
|
|
111
|
+
const exports = [
|
|
112
|
+
{ name: 'getUser', type: 'function' as const, inferredDomain: 'user' },
|
|
113
|
+
{ name: 'updateUser', type: 'function' as const, inferredDomain: 'user' },
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const cohesion = calculateCohesion(exports);
|
|
117
|
+
|
|
118
|
+
// Should return a valid cohesion score
|
|
119
|
+
expect(cohesion).toBeGreaterThan(0);
|
|
120
|
+
expect(cohesion).toBeLessThanOrEqual(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should handle empty exports', () => {
|
|
124
|
+
const cohesion = calculateCohesion([]);
|
|
125
|
+
expect(cohesion).toBe(1); // Empty exports should have perfect cohesion
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should pass through file path and options', () => {
|
|
129
|
+
const exports = [
|
|
130
|
+
{ name: 'test', type: 'function' as const, inferredDomain: 'test' },
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
// Test with file path
|
|
134
|
+
const cohesion = calculateCohesion(exports, 'src/test.ts');
|
|
135
|
+
expect(cohesion).toBe(1);
|
|
136
|
+
|
|
137
|
+
// Test with options
|
|
138
|
+
const cohesionWithOptions = calculateCohesion(exports, undefined, {
|
|
139
|
+
someOption: true,
|
|
140
|
+
});
|
|
141
|
+
expect(cohesionWithOptions).toBe(1);
|
|
142
|
+
});
|
|
143
|
+
});
|