@aiready/context-analyzer 0.9.41 → 0.16.0
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 +21 -20
- package/dist/chunk-4SYIJ7CU.mjs +1538 -0
- package/dist/chunk-4XQVYYPC.mjs +1470 -0
- package/dist/chunk-5CLU3HYU.mjs +1475 -0
- package/dist/chunk-5K73Q3OQ.mjs +1520 -0
- package/dist/chunk-6AVS4KTM.mjs +1536 -0
- package/dist/chunk-6I4552YB.mjs +1467 -0
- package/dist/chunk-6LPITDKG.mjs +1539 -0
- package/dist/chunk-AECWO7NQ.mjs +1539 -0
- package/dist/chunk-AJC3FR6G.mjs +1509 -0
- package/dist/chunk-CVGIDSMN.mjs +1522 -0
- package/dist/chunk-DXG5NIYL.mjs +1527 -0
- package/dist/chunk-G3CCJCBI.mjs +1521 -0
- package/dist/chunk-GFADGYXZ.mjs +1752 -0
- package/dist/chunk-GTRIBVS6.mjs +1467 -0
- package/dist/chunk-H4HWBQU6.mjs +1530 -0
- package/dist/chunk-JH535NPP.mjs +1619 -0
- package/dist/chunk-KGFWKSGJ.mjs +1442 -0
- package/dist/chunk-N2GQWNFG.mjs +1527 -0
- package/dist/chunk-NQA3F2HJ.mjs +1532 -0
- package/dist/chunk-NXXQ2U73.mjs +1467 -0
- package/dist/chunk-QDGPR3L6.mjs +1518 -0
- package/dist/chunk-SAVOSPM3.mjs +1522 -0
- package/dist/chunk-SIX4KMF2.mjs +1468 -0
- package/dist/chunk-SPAM2YJE.mjs +1537 -0
- package/dist/chunk-UG7OPVHB.mjs +1521 -0
- package/dist/chunk-VIJTZPBI.mjs +1470 -0
- package/dist/chunk-W37E7MW5.mjs +1403 -0
- package/dist/chunk-W76FEISE.mjs +1538 -0
- package/dist/chunk-WCFQYXQA.mjs +1532 -0
- package/dist/chunk-XY77XABG.mjs +1545 -0
- package/dist/chunk-YCGDIGOG.mjs +1467 -0
- package/dist/cli.js +768 -1160
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +196 -64
- package/dist/index.d.ts +196 -64
- package/dist/index.js +937 -1209
- package/dist/index.mjs +65 -3
- package/package.json +2 -2
- package/src/__tests__/contract.test.ts +38 -0
- package/src/analyzer.ts +143 -2177
- package/src/ast-utils.ts +94 -0
- package/src/classifier.ts +497 -0
- package/src/cluster-detector.ts +100 -0
- package/src/defaults.ts +59 -0
- package/src/graph-builder.ts +272 -0
- package/src/index.ts +30 -519
- package/src/metrics.ts +231 -0
- package/src/remediation.ts +139 -0
- package/src/scoring.ts +12 -34
- package/src/semantic-analysis.ts +192 -126
- package/src/summary.ts +168 -0
package/dist/cli.js
CHANGED
|
@@ -37,7 +37,7 @@ __export(python_context_exports, {
|
|
|
37
37
|
});
|
|
38
38
|
async function analyzePythonContext(files, rootDir) {
|
|
39
39
|
const results = [];
|
|
40
|
-
const parser = (0,
|
|
40
|
+
const parser = (0, import_core6.getParser)("dummy.py");
|
|
41
41
|
if (!parser) {
|
|
42
42
|
console.warn("Python parser not available");
|
|
43
43
|
return results;
|
|
@@ -101,7 +101,7 @@ async function analyzePythonContext(files, rootDir) {
|
|
|
101
101
|
}
|
|
102
102
|
async function buildPythonDependencyGraph(files, rootDir) {
|
|
103
103
|
const graph = /* @__PURE__ */ new Map();
|
|
104
|
-
const parser = (0,
|
|
104
|
+
const parser = (0, import_core6.getParser)("dummy.py");
|
|
105
105
|
if (!parser) return graph;
|
|
106
106
|
for (const file of files) {
|
|
107
107
|
try {
|
|
@@ -181,7 +181,7 @@ async function calculatePythonImportDepth(file, dependencyGraph, visited, depth
|
|
|
181
181
|
}
|
|
182
182
|
function estimateContextBudget(code, imports, dependencyGraph) {
|
|
183
183
|
void dependencyGraph;
|
|
184
|
-
let budget = (0,
|
|
184
|
+
let budget = (0, import_core6.estimateTokens)(code);
|
|
185
185
|
const avgTokensPerDep = 500;
|
|
186
186
|
budget += imports.length * avgTokensPerDep;
|
|
187
187
|
return budget;
|
|
@@ -231,11 +231,11 @@ function detectCircularDependencies2(file, dependencyGraph) {
|
|
|
231
231
|
dfs(file, []);
|
|
232
232
|
return [...new Set(circular)];
|
|
233
233
|
}
|
|
234
|
-
var
|
|
234
|
+
var import_core6, import_path, import_fs;
|
|
235
235
|
var init_python_context = __esm({
|
|
236
236
|
"src/analyzers/python-context.ts"() {
|
|
237
237
|
"use strict";
|
|
238
|
-
|
|
238
|
+
import_core6 = require("@aiready/core");
|
|
239
239
|
import_path = require("path");
|
|
240
240
|
import_fs = __toESM(require("fs"));
|
|
241
241
|
}
|
|
@@ -245,29 +245,27 @@ var init_python_context = __esm({
|
|
|
245
245
|
var import_commander = require("commander");
|
|
246
246
|
|
|
247
247
|
// src/index.ts
|
|
248
|
-
var
|
|
248
|
+
var import_core7 = require("@aiready/core");
|
|
249
249
|
|
|
250
|
-
// src/
|
|
250
|
+
// src/metrics.ts
|
|
251
|
+
var import_core2 = require("@aiready/core");
|
|
252
|
+
|
|
253
|
+
// src/ast-utils.ts
|
|
251
254
|
var import_core = require("@aiready/core");
|
|
252
255
|
|
|
253
256
|
// src/semantic-analysis.ts
|
|
254
257
|
function buildCoUsageMatrix(graph) {
|
|
255
258
|
const coUsageMatrix = /* @__PURE__ */ new Map();
|
|
256
|
-
for (const [
|
|
257
|
-
void sourceFile;
|
|
259
|
+
for (const [, node] of graph.nodes) {
|
|
258
260
|
const imports = node.imports;
|
|
259
261
|
for (let i = 0; i < imports.length; i++) {
|
|
260
262
|
const fileA = imports[i];
|
|
261
|
-
if (!coUsageMatrix.has(fileA))
|
|
262
|
-
coUsageMatrix.set(fileA, /* @__PURE__ */ new Map());
|
|
263
|
-
}
|
|
263
|
+
if (!coUsageMatrix.has(fileA)) coUsageMatrix.set(fileA, /* @__PURE__ */ new Map());
|
|
264
264
|
for (let j = i + 1; j < imports.length; j++) {
|
|
265
265
|
const fileB = imports[j];
|
|
266
266
|
const fileAUsage = coUsageMatrix.get(fileA);
|
|
267
267
|
fileAUsage.set(fileB, (fileAUsage.get(fileB) || 0) + 1);
|
|
268
|
-
if (!coUsageMatrix.has(fileB))
|
|
269
|
-
coUsageMatrix.set(fileB, /* @__PURE__ */ new Map());
|
|
270
|
-
}
|
|
268
|
+
if (!coUsageMatrix.has(fileB)) coUsageMatrix.set(fileB, /* @__PURE__ */ new Map());
|
|
271
269
|
const fileBUsage = coUsageMatrix.get(fileB);
|
|
272
270
|
fileBUsage.set(fileA, (fileBUsage.get(fileA) || 0) + 1);
|
|
273
271
|
}
|
|
@@ -281,9 +279,7 @@ function buildTypeGraph(graph) {
|
|
|
281
279
|
for (const exp of node.exports) {
|
|
282
280
|
if (exp.typeReferences) {
|
|
283
281
|
for (const typeRef of exp.typeReferences) {
|
|
284
|
-
if (!typeGraph.has(typeRef))
|
|
285
|
-
typeGraph.set(typeRef, /* @__PURE__ */ new Set());
|
|
286
|
-
}
|
|
282
|
+
if (!typeGraph.has(typeRef)) typeGraph.set(typeRef, /* @__PURE__ */ new Set());
|
|
287
283
|
typeGraph.get(typeRef).add(file);
|
|
288
284
|
}
|
|
289
285
|
}
|
|
@@ -291,29 +287,7 @@ function buildTypeGraph(graph) {
|
|
|
291
287
|
}
|
|
292
288
|
return typeGraph;
|
|
293
289
|
}
|
|
294
|
-
function calculateDomainConfidence(signals) {
|
|
295
|
-
const weights = {
|
|
296
|
-
coUsage: 0.35,
|
|
297
|
-
// Strongest signal: actual usage patterns
|
|
298
|
-
typeReference: 0.3,
|
|
299
|
-
// Strong signal: shared types
|
|
300
|
-
exportName: 0.15,
|
|
301
|
-
// Medium signal: identifier semantics
|
|
302
|
-
importPath: 0.1,
|
|
303
|
-
// Weaker signal: path structure
|
|
304
|
-
folderStructure: 0.1
|
|
305
|
-
// Weakest signal: organization convention
|
|
306
|
-
};
|
|
307
|
-
let confidence = 0;
|
|
308
|
-
if (signals.coUsage) confidence += weights.coUsage;
|
|
309
|
-
if (signals.typeReference) confidence += weights.typeReference;
|
|
310
|
-
if (signals.exportName) confidence += weights.exportName;
|
|
311
|
-
if (signals.importPath) confidence += weights.importPath;
|
|
312
|
-
if (signals.folderStructure) confidence += weights.folderStructure;
|
|
313
|
-
return confidence;
|
|
314
|
-
}
|
|
315
290
|
function inferDomainFromSemantics(file, exportName, graph, coUsageMatrix, typeGraph, exportTypeRefs) {
|
|
316
|
-
const assignments = [];
|
|
317
291
|
const domainSignals = /* @__PURE__ */ new Map();
|
|
318
292
|
const coUsages = coUsageMatrix.get(file) || /* @__PURE__ */ new Map();
|
|
319
293
|
const strongCoUsages = Array.from(coUsages.entries()).filter(([, count]) => count >= 3).map(([coFile]) => coFile);
|
|
@@ -342,23 +316,22 @@ function inferDomainFromSemantics(file, exportName, graph, coUsageMatrix, typeGr
|
|
|
342
316
|
const filesWithType = typeGraph.get(typeRef);
|
|
343
317
|
if (filesWithType) {
|
|
344
318
|
for (const typeFile of filesWithType) {
|
|
345
|
-
if (typeFile
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
domainSignals.get(domain).typeReference = true;
|
|
319
|
+
if (typeFile === file) continue;
|
|
320
|
+
const typeNode = graph.nodes.get(typeFile);
|
|
321
|
+
if (typeNode) {
|
|
322
|
+
for (const exp of typeNode.exports) {
|
|
323
|
+
if (exp.inferredDomain && exp.inferredDomain !== "unknown") {
|
|
324
|
+
const domain = exp.inferredDomain;
|
|
325
|
+
if (!domainSignals.has(domain)) {
|
|
326
|
+
domainSignals.set(domain, {
|
|
327
|
+
coUsage: false,
|
|
328
|
+
typeReference: false,
|
|
329
|
+
exportName: false,
|
|
330
|
+
importPath: false,
|
|
331
|
+
folderStructure: false
|
|
332
|
+
});
|
|
361
333
|
}
|
|
334
|
+
domainSignals.get(domain).typeReference = true;
|
|
362
335
|
}
|
|
363
336
|
}
|
|
364
337
|
}
|
|
@@ -366,17 +339,289 @@ function inferDomainFromSemantics(file, exportName, graph, coUsageMatrix, typeGr
|
|
|
366
339
|
}
|
|
367
340
|
}
|
|
368
341
|
}
|
|
342
|
+
const assignments = [];
|
|
369
343
|
for (const [domain, signals] of domainSignals) {
|
|
370
344
|
const confidence = calculateDomainConfidence(signals);
|
|
371
|
-
if (confidence >= 0.3) {
|
|
372
|
-
assignments.push({ domain, confidence, signals });
|
|
373
|
-
}
|
|
345
|
+
if (confidence >= 0.3) assignments.push({ domain, confidence, signals });
|
|
374
346
|
}
|
|
375
347
|
assignments.sort((a, b) => b.confidence - a.confidence);
|
|
376
348
|
return assignments;
|
|
377
349
|
}
|
|
350
|
+
function calculateDomainConfidence(signals) {
|
|
351
|
+
const weights = {
|
|
352
|
+
coUsage: 0.35,
|
|
353
|
+
typeReference: 0.3,
|
|
354
|
+
exportName: 0.15,
|
|
355
|
+
importPath: 0.1,
|
|
356
|
+
folderStructure: 0.1
|
|
357
|
+
};
|
|
358
|
+
let confidence = 0;
|
|
359
|
+
if (signals.coUsage) confidence += weights.coUsage;
|
|
360
|
+
if (signals.typeReference) confidence += weights.typeReference;
|
|
361
|
+
if (signals.exportName) confidence += weights.exportName;
|
|
362
|
+
if (signals.importPath) confidence += weights.importPath;
|
|
363
|
+
if (signals.folderStructure) confidence += weights.folderStructure;
|
|
364
|
+
return confidence;
|
|
365
|
+
}
|
|
366
|
+
function extractExports(content, filePath, domainOptions, fileImports) {
|
|
367
|
+
const exports2 = [];
|
|
368
|
+
const patterns = [
|
|
369
|
+
/export\s+function\s+(\w+)/g,
|
|
370
|
+
/export\s+class\s+(\w+)/g,
|
|
371
|
+
/export\s+const\s+(\w+)/g,
|
|
372
|
+
/export\s+type\s+(\w+)/g,
|
|
373
|
+
/export\s+interface\s+(\w+)/g,
|
|
374
|
+
/export\s+default/g
|
|
375
|
+
];
|
|
376
|
+
const types = [
|
|
377
|
+
"function",
|
|
378
|
+
"class",
|
|
379
|
+
"const",
|
|
380
|
+
"type",
|
|
381
|
+
"interface",
|
|
382
|
+
"default"
|
|
383
|
+
];
|
|
384
|
+
patterns.forEach((pattern, index) => {
|
|
385
|
+
let match;
|
|
386
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
387
|
+
const name = match[1] || "default";
|
|
388
|
+
const type = types[index];
|
|
389
|
+
const inferredDomain = inferDomain(
|
|
390
|
+
name,
|
|
391
|
+
filePath,
|
|
392
|
+
domainOptions,
|
|
393
|
+
fileImports
|
|
394
|
+
);
|
|
395
|
+
exports2.push({ name, type, inferredDomain });
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
return exports2;
|
|
399
|
+
}
|
|
400
|
+
function inferDomain(name, filePath, domainOptions, fileImports) {
|
|
401
|
+
const lower = name.toLowerCase();
|
|
402
|
+
const tokens = Array.from(
|
|
403
|
+
new Set(
|
|
404
|
+
lower.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^a-z0-9]+/gi, " ").split(" ").filter(Boolean)
|
|
405
|
+
)
|
|
406
|
+
);
|
|
407
|
+
const defaultKeywords = [
|
|
408
|
+
"authentication",
|
|
409
|
+
"authorization",
|
|
410
|
+
"payment",
|
|
411
|
+
"invoice",
|
|
412
|
+
"customer",
|
|
413
|
+
"product",
|
|
414
|
+
"order",
|
|
415
|
+
"cart",
|
|
416
|
+
"user",
|
|
417
|
+
"admin",
|
|
418
|
+
"repository",
|
|
419
|
+
"controller",
|
|
420
|
+
"service",
|
|
421
|
+
"config",
|
|
422
|
+
"model",
|
|
423
|
+
"view",
|
|
424
|
+
"auth"
|
|
425
|
+
];
|
|
426
|
+
const domainKeywords = domainOptions?.domainKeywords?.length ? [...domainOptions.domainKeywords, ...defaultKeywords] : defaultKeywords;
|
|
427
|
+
for (const keyword of domainKeywords) {
|
|
428
|
+
if (tokens.includes(keyword)) return keyword;
|
|
429
|
+
}
|
|
430
|
+
for (const keyword of domainKeywords) {
|
|
431
|
+
if (lower.includes(keyword)) return keyword;
|
|
432
|
+
}
|
|
433
|
+
if (fileImports) {
|
|
434
|
+
for (const importPath of fileImports) {
|
|
435
|
+
const segments = importPath.split("/");
|
|
436
|
+
for (const segment of segments) {
|
|
437
|
+
const segLower = segment.toLowerCase();
|
|
438
|
+
const singularSegment = singularize(segLower);
|
|
439
|
+
for (const keyword of domainKeywords) {
|
|
440
|
+
if (singularSegment === keyword || segLower === keyword || segLower.includes(keyword))
|
|
441
|
+
return keyword;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (filePath) {
|
|
447
|
+
const segments = filePath.split("/");
|
|
448
|
+
for (const segment of segments) {
|
|
449
|
+
const segLower = segment.toLowerCase();
|
|
450
|
+
const singularSegment = singularize(segLower);
|
|
451
|
+
for (const keyword of domainKeywords) {
|
|
452
|
+
if (singularSegment === keyword || segLower === keyword) return keyword;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return "unknown";
|
|
457
|
+
}
|
|
458
|
+
function singularize(word) {
|
|
459
|
+
const irregulars = {
|
|
460
|
+
people: "person",
|
|
461
|
+
children: "child",
|
|
462
|
+
men: "man",
|
|
463
|
+
women: "woman"
|
|
464
|
+
};
|
|
465
|
+
if (irregulars[word]) return irregulars[word];
|
|
466
|
+
if (word.endsWith("ies")) return word.slice(0, -3) + "y";
|
|
467
|
+
if (word.endsWith("ses")) return word.slice(0, -2);
|
|
468
|
+
if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
|
|
469
|
+
return word;
|
|
470
|
+
}
|
|
378
471
|
|
|
379
|
-
// src/
|
|
472
|
+
// src/ast-utils.ts
|
|
473
|
+
function extractExportsWithAST(content, filePath, domainOptions, fileImports) {
|
|
474
|
+
try {
|
|
475
|
+
const { exports: astExports } = (0, import_core.parseFileExports)(content, filePath);
|
|
476
|
+
return astExports.map((exp) => ({
|
|
477
|
+
name: exp.name,
|
|
478
|
+
type: exp.type,
|
|
479
|
+
inferredDomain: inferDomain(
|
|
480
|
+
exp.name,
|
|
481
|
+
filePath,
|
|
482
|
+
domainOptions,
|
|
483
|
+
fileImports
|
|
484
|
+
),
|
|
485
|
+
imports: exp.imports,
|
|
486
|
+
dependencies: exp.dependencies,
|
|
487
|
+
typeReferences: exp.typeReferences
|
|
488
|
+
}));
|
|
489
|
+
} catch (error) {
|
|
490
|
+
void error;
|
|
491
|
+
return extractExports(content, filePath, domainOptions, fileImports);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function isTestFile(filePath) {
|
|
495
|
+
const lower = filePath.toLowerCase();
|
|
496
|
+
return lower.includes(".test.") || lower.includes(".spec.") || lower.includes("/__tests__/") || lower.includes("/tests/") || lower.includes("/test/") || lower.includes("test-") || lower.includes("-test") || lower.includes("/__mocks__/") || lower.includes("/mocks/") || lower.includes("/fixtures/") || lower.includes(".mock.") || lower.includes(".fixture.") || lower.includes("/test-utils/");
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/metrics.ts
|
|
500
|
+
function calculateEnhancedCohesion(exports2, filePath, options) {
|
|
501
|
+
if (exports2.length <= 1) return 1;
|
|
502
|
+
if (filePath && isTestFile(filePath)) return 1;
|
|
503
|
+
const domains = exports2.map((e) => e.inferredDomain || "unknown");
|
|
504
|
+
const domainCounts = /* @__PURE__ */ new Map();
|
|
505
|
+
for (const d of domains) domainCounts.set(d, (domainCounts.get(d) || 0) + 1);
|
|
506
|
+
if (domainCounts.size === 1 && domains[0] !== "unknown") {
|
|
507
|
+
if (!options?.weights) return 1;
|
|
508
|
+
}
|
|
509
|
+
const probs = Array.from(domainCounts.values()).map(
|
|
510
|
+
(c) => c / exports2.length
|
|
511
|
+
);
|
|
512
|
+
let domainEntropy = 0;
|
|
513
|
+
for (const p of probs) {
|
|
514
|
+
if (p > 0) domainEntropy -= p * Math.log2(p);
|
|
515
|
+
}
|
|
516
|
+
const maxEntropy = Math.log2(Math.max(2, domainCounts.size));
|
|
517
|
+
const domainScore = 1 - domainEntropy / maxEntropy;
|
|
518
|
+
let importScoreTotal = 0;
|
|
519
|
+
let pairsWithData = 0;
|
|
520
|
+
let anyImportData = false;
|
|
521
|
+
for (let i = 0; i < exports2.length; i++) {
|
|
522
|
+
for (let j = i + 1; j < exports2.length; j++) {
|
|
523
|
+
const exp1Imports = exports2[i].imports;
|
|
524
|
+
const exp2Imports = exports2[j].imports;
|
|
525
|
+
if (exp1Imports || exp2Imports) {
|
|
526
|
+
anyImportData = true;
|
|
527
|
+
const sim = (0, import_core2.calculateImportSimilarity)(
|
|
528
|
+
{ ...exports2[i], imports: exp1Imports || [] },
|
|
529
|
+
{ ...exports2[j], imports: exp2Imports || [] }
|
|
530
|
+
);
|
|
531
|
+
importScoreTotal += sim;
|
|
532
|
+
pairsWithData++;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const avgImportScore = pairsWithData > 0 ? importScoreTotal / pairsWithData : 0;
|
|
537
|
+
let score = 0;
|
|
538
|
+
if (anyImportData) {
|
|
539
|
+
score = domainScore * 0.4 + avgImportScore * 0.6;
|
|
540
|
+
if (score === 0 && domainScore === 0) score = 0.1;
|
|
541
|
+
} else {
|
|
542
|
+
score = domainScore;
|
|
543
|
+
}
|
|
544
|
+
let structuralScore = 0;
|
|
545
|
+
for (const exp of exports2) {
|
|
546
|
+
if (exp.dependencies && exp.dependencies.length > 0) {
|
|
547
|
+
structuralScore += 1;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (structuralScore > 0) {
|
|
551
|
+
score = Math.min(1, score + 0.1);
|
|
552
|
+
}
|
|
553
|
+
if (!options?.weights && !anyImportData && domainCounts.size === 1) return 1;
|
|
554
|
+
return score;
|
|
555
|
+
}
|
|
556
|
+
function calculateFragmentation(files, domain, options) {
|
|
557
|
+
if (files.length <= 1) return 0;
|
|
558
|
+
const directories = new Set(
|
|
559
|
+
files.map((f) => f.split("/").slice(0, -1).join("/"))
|
|
560
|
+
);
|
|
561
|
+
const uniqueDirs = directories.size;
|
|
562
|
+
let score = 0;
|
|
563
|
+
if (options?.useLogScale) {
|
|
564
|
+
if (uniqueDirs <= 1) score = 0;
|
|
565
|
+
else {
|
|
566
|
+
const total = files.length;
|
|
567
|
+
const base = options.logBase || Math.E;
|
|
568
|
+
const num = Math.log(uniqueDirs) / Math.log(base);
|
|
569
|
+
const den = Math.log(total) / Math.log(base);
|
|
570
|
+
score = den > 0 ? num / den : 0;
|
|
571
|
+
}
|
|
572
|
+
} else {
|
|
573
|
+
score = (uniqueDirs - 1) / (files.length - 1);
|
|
574
|
+
}
|
|
575
|
+
if (options?.sharedImportRatio && options.sharedImportRatio > 0.5) {
|
|
576
|
+
const discount = (options.sharedImportRatio - 0.5) * 0.4;
|
|
577
|
+
score = score * (1 - discount);
|
|
578
|
+
}
|
|
579
|
+
return score;
|
|
580
|
+
}
|
|
581
|
+
function calculatePathEntropy(files) {
|
|
582
|
+
if (!files || files.length === 0) return 0;
|
|
583
|
+
const dirCounts = /* @__PURE__ */ new Map();
|
|
584
|
+
for (const f of files) {
|
|
585
|
+
const dir = f.split("/").slice(0, -1).join("/") || ".";
|
|
586
|
+
dirCounts.set(dir, (dirCounts.get(dir) || 0) + 1);
|
|
587
|
+
}
|
|
588
|
+
const counts = Array.from(dirCounts.values());
|
|
589
|
+
if (counts.length <= 1) return 0;
|
|
590
|
+
const total = counts.reduce((s, v) => s + v, 0);
|
|
591
|
+
let entropy = 0;
|
|
592
|
+
for (const count of counts) {
|
|
593
|
+
const prob = count / total;
|
|
594
|
+
entropy -= prob * Math.log2(prob);
|
|
595
|
+
}
|
|
596
|
+
const maxEntropy = Math.log2(counts.length);
|
|
597
|
+
return maxEntropy > 0 ? entropy / maxEntropy : 0;
|
|
598
|
+
}
|
|
599
|
+
function calculateDirectoryDistance(files) {
|
|
600
|
+
if (!files || files.length <= 1) return 0;
|
|
601
|
+
const pathSegments = (p) => p.split("/").filter(Boolean);
|
|
602
|
+
const commonAncestorDepth = (a, b) => {
|
|
603
|
+
const minLen = Math.min(a.length, b.length);
|
|
604
|
+
let i = 0;
|
|
605
|
+
while (i < minLen && a[i] === b[i]) i++;
|
|
606
|
+
return i;
|
|
607
|
+
};
|
|
608
|
+
let totalNormalized = 0;
|
|
609
|
+
let comparisons = 0;
|
|
610
|
+
for (let i = 0; i < files.length; i++) {
|
|
611
|
+
for (let j = i + 1; j < files.length; j++) {
|
|
612
|
+
const segA = pathSegments(files[i]);
|
|
613
|
+
const segB = pathSegments(files[j]);
|
|
614
|
+
const shared = commonAncestorDepth(segA, segB);
|
|
615
|
+
const maxDepth = Math.max(segA.length, segB.length);
|
|
616
|
+
totalNormalized += 1 - (maxDepth > 0 ? shared / maxDepth : 0);
|
|
617
|
+
comparisons++;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return comparisons > 0 ? totalNormalized / comparisons : 0;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/graph-builder.ts
|
|
624
|
+
var import_core3 = require("@aiready/core");
|
|
380
625
|
function extractDomainKeywordsFromPaths(files) {
|
|
381
626
|
const folderNames = /* @__PURE__ */ new Set();
|
|
382
627
|
for (const { file } of files) {
|
|
@@ -404,39 +649,29 @@ function extractDomainKeywordsFromPaths(files) {
|
|
|
404
649
|
for (const segment of segments) {
|
|
405
650
|
const normalized = segment.toLowerCase();
|
|
406
651
|
if (normalized && !skipFolders.has(normalized) && !normalized.includes(".")) {
|
|
407
|
-
|
|
408
|
-
folderNames.add(singular);
|
|
652
|
+
folderNames.add(singularize2(normalized));
|
|
409
653
|
}
|
|
410
654
|
}
|
|
411
655
|
}
|
|
412
656
|
return Array.from(folderNames);
|
|
413
657
|
}
|
|
414
|
-
function
|
|
658
|
+
function singularize2(word) {
|
|
415
659
|
const irregulars = {
|
|
416
660
|
people: "person",
|
|
417
661
|
children: "child",
|
|
418
662
|
men: "man",
|
|
419
663
|
women: "woman"
|
|
420
664
|
};
|
|
421
|
-
if (irregulars[word])
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (word.endsWith("
|
|
425
|
-
return word.slice(0, -3) + "y";
|
|
426
|
-
}
|
|
427
|
-
if (word.endsWith("ses")) {
|
|
428
|
-
return word.slice(0, -2);
|
|
429
|
-
}
|
|
430
|
-
if (word.endsWith("s") && word.length > 3) {
|
|
431
|
-
return word.slice(0, -1);
|
|
432
|
-
}
|
|
665
|
+
if (irregulars[word]) return irregulars[word];
|
|
666
|
+
if (word.endsWith("ies")) return word.slice(0, -3) + "y";
|
|
667
|
+
if (word.endsWith("ses")) return word.slice(0, -2);
|
|
668
|
+
if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
|
|
433
669
|
return word;
|
|
434
670
|
}
|
|
435
671
|
function buildDependencyGraph(files, options) {
|
|
436
672
|
const nodes = /* @__PURE__ */ new Map();
|
|
437
673
|
const edges = /* @__PURE__ */ new Map();
|
|
438
674
|
const autoDetectedKeywords = options?.domainKeywords ?? extractDomainKeywordsFromPaths(files);
|
|
439
|
-
void import_core.calculateImportSimilarity;
|
|
440
675
|
for (const { file, content } of files) {
|
|
441
676
|
const imports = extractImportsFromContent(content);
|
|
442
677
|
const exports2 = extractExportsWithAST(
|
|
@@ -445,15 +680,9 @@ function buildDependencyGraph(files, options) {
|
|
|
445
680
|
{ domainKeywords: autoDetectedKeywords },
|
|
446
681
|
imports
|
|
447
682
|
);
|
|
448
|
-
const tokenCost = (0,
|
|
683
|
+
const tokenCost = (0, import_core3.estimateTokens)(content);
|
|
449
684
|
const linesOfCode = content.split("\n").length;
|
|
450
|
-
nodes.set(file, {
|
|
451
|
-
file,
|
|
452
|
-
imports,
|
|
453
|
-
exports: exports2,
|
|
454
|
-
tokenCost,
|
|
455
|
-
linesOfCode
|
|
456
|
-
});
|
|
685
|
+
nodes.set(file, { file, imports, exports: exports2, tokenCost, linesOfCode });
|
|
457
686
|
edges.set(file, new Set(imports));
|
|
458
687
|
}
|
|
459
688
|
const graph = { nodes, edges };
|
|
@@ -483,11 +712,8 @@ function extractImportsFromContent(content) {
|
|
|
483
712
|
const imports = [];
|
|
484
713
|
const patterns = [
|
|
485
714
|
/import\s+.*?\s+from\s+['"](.+?)['"]/g,
|
|
486
|
-
// import ... from '...'
|
|
487
715
|
/import\s+['"](.+?)['"]/g,
|
|
488
|
-
// import '...'
|
|
489
716
|
/require\(['"](.+?)['"]\)/g
|
|
490
|
-
// require('...')
|
|
491
717
|
];
|
|
492
718
|
for (const pattern of patterns) {
|
|
493
719
|
let match;
|
|
@@ -501,31 +727,25 @@ function extractImportsFromContent(content) {
|
|
|
501
727
|
return [...new Set(imports)];
|
|
502
728
|
}
|
|
503
729
|
function calculateImportDepth(file, graph, visited = /* @__PURE__ */ new Set(), depth = 0) {
|
|
504
|
-
if (visited.has(file))
|
|
505
|
-
return depth;
|
|
506
|
-
}
|
|
730
|
+
if (visited.has(file)) return depth;
|
|
507
731
|
const dependencies = graph.edges.get(file);
|
|
508
|
-
if (!dependencies || dependencies.size === 0)
|
|
509
|
-
return depth;
|
|
510
|
-
}
|
|
732
|
+
if (!dependencies || dependencies.size === 0) return depth;
|
|
511
733
|
visited.add(file);
|
|
512
734
|
let maxDepth = depth;
|
|
513
735
|
for (const dep of dependencies) {
|
|
514
|
-
|
|
515
|
-
|
|
736
|
+
maxDepth = Math.max(
|
|
737
|
+
maxDepth,
|
|
738
|
+
calculateImportDepth(dep, graph, visited, depth + 1)
|
|
739
|
+
);
|
|
516
740
|
}
|
|
517
741
|
visited.delete(file);
|
|
518
742
|
return maxDepth;
|
|
519
743
|
}
|
|
520
744
|
function getTransitiveDependencies(file, graph, visited = /* @__PURE__ */ new Set()) {
|
|
521
|
-
if (visited.has(file))
|
|
522
|
-
return [];
|
|
523
|
-
}
|
|
745
|
+
if (visited.has(file)) return [];
|
|
524
746
|
visited.add(file);
|
|
525
747
|
const dependencies = graph.edges.get(file);
|
|
526
|
-
if (!dependencies || dependencies.size === 0)
|
|
527
|
-
return [];
|
|
528
|
-
}
|
|
748
|
+
if (!dependencies || dependencies.size === 0) return [];
|
|
529
749
|
const allDeps = [];
|
|
530
750
|
for (const dep of dependencies) {
|
|
531
751
|
allDeps.push(dep);
|
|
@@ -558,9 +778,7 @@ function detectCircularDependencies(graph) {
|
|
|
558
778
|
}
|
|
559
779
|
return;
|
|
560
780
|
}
|
|
561
|
-
if (visited.has(file))
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
781
|
+
if (visited.has(file)) return;
|
|
564
782
|
visited.add(file);
|
|
565
783
|
recursionStack.add(file);
|
|
566
784
|
path.push(file);
|
|
@@ -579,404 +797,23 @@ function detectCircularDependencies(graph) {
|
|
|
579
797
|
}
|
|
580
798
|
return cycles;
|
|
581
799
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
function isTestFile(filePath) {
|
|
586
|
-
const lower = filePath.toLowerCase();
|
|
587
|
-
return lower.includes("test") || lower.includes("spec") || lower.includes("mock") || lower.includes("fixture") || lower.includes("__tests__") || lower.includes(".test.") || lower.includes(".spec.");
|
|
588
|
-
}
|
|
589
|
-
function calculateFragmentation(files, domain, options) {
|
|
590
|
-
if (files.length <= 1) return 0;
|
|
591
|
-
const directories = new Set(
|
|
592
|
-
files.map((f) => f.split("/").slice(0, -1).join("/"))
|
|
593
|
-
);
|
|
594
|
-
const uniqueDirs = directories.size;
|
|
595
|
-
if (options?.useLogScale) {
|
|
596
|
-
if (uniqueDirs <= 1) return 0;
|
|
597
|
-
const total = files.length;
|
|
598
|
-
const base = options.logBase || Math.E;
|
|
599
|
-
const num = Math.log(uniqueDirs) / Math.log(base);
|
|
600
|
-
const den = Math.log(total) / Math.log(base);
|
|
601
|
-
return den > 0 ? num / den : 0;
|
|
602
|
-
}
|
|
603
|
-
return (uniqueDirs - 1) / (files.length - 1);
|
|
604
|
-
}
|
|
605
|
-
function calculatePathEntropy(files) {
|
|
606
|
-
if (!files || files.length === 0) return 0;
|
|
607
|
-
const dirCounts = /* @__PURE__ */ new Map();
|
|
608
|
-
for (const f of files) {
|
|
609
|
-
const dir = f.split("/").slice(0, -1).join("/") || ".";
|
|
610
|
-
dirCounts.set(dir, (dirCounts.get(dir) || 0) + 1);
|
|
611
|
-
}
|
|
612
|
-
const counts = Array.from(dirCounts.values());
|
|
613
|
-
if (counts.length <= 1) return 0;
|
|
614
|
-
const total = counts.reduce((s, v) => s + v, 0);
|
|
615
|
-
let entropy = 0;
|
|
616
|
-
for (const count of counts) {
|
|
617
|
-
const prob = count / total;
|
|
618
|
-
entropy -= prob * Math.log2(prob);
|
|
619
|
-
}
|
|
620
|
-
const maxEntropy = Math.log2(counts.length);
|
|
621
|
-
return maxEntropy > 0 ? entropy / maxEntropy : 0;
|
|
622
|
-
}
|
|
623
|
-
function calculateDirectoryDistance(files) {
|
|
624
|
-
if (!files || files.length <= 1) return 0;
|
|
625
|
-
function pathSegments(p) {
|
|
626
|
-
return p.split("/").filter(Boolean);
|
|
627
|
-
}
|
|
628
|
-
function commonAncestorDepth(a, b) {
|
|
629
|
-
const minLen = Math.min(a.length, b.length);
|
|
630
|
-
let i = 0;
|
|
631
|
-
while (i < minLen && a[i] === b[i]) i++;
|
|
632
|
-
return i;
|
|
633
|
-
}
|
|
634
|
-
let totalNormalized = 0;
|
|
635
|
-
let comparisons = 0;
|
|
636
|
-
for (let i = 0; i < files.length; i++) {
|
|
637
|
-
for (let j = i + 1; j < files.length; j++) {
|
|
638
|
-
const segA = pathSegments(files[i]);
|
|
639
|
-
const segB = pathSegments(files[j]);
|
|
640
|
-
const shared = commonAncestorDepth(segA, segB);
|
|
641
|
-
const maxDepth = Math.max(segA.length, segB.length);
|
|
642
|
-
const normalizedShared = maxDepth > 0 ? shared / maxDepth : 0;
|
|
643
|
-
totalNormalized += 1 - normalizedShared;
|
|
644
|
-
comparisons++;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
return comparisons > 0 ? totalNormalized / comparisons : 0;
|
|
648
|
-
}
|
|
649
|
-
function detectModuleClusters(graph, options) {
|
|
650
|
-
const domainMap = /* @__PURE__ */ new Map();
|
|
651
|
-
for (const [file, node] of graph.nodes.entries()) {
|
|
652
|
-
const domains = node.exports.map((e) => e.inferredDomain || "unknown");
|
|
653
|
-
const primaryDomain = domains[0] || "unknown";
|
|
654
|
-
if (!domainMap.has(primaryDomain)) {
|
|
655
|
-
domainMap.set(primaryDomain, []);
|
|
656
|
-
}
|
|
657
|
-
domainMap.get(primaryDomain).push(file);
|
|
658
|
-
}
|
|
659
|
-
const clusters = [];
|
|
660
|
-
for (const [domain, files] of domainMap.entries()) {
|
|
661
|
-
if (files.length < 2) continue;
|
|
662
|
-
const totalTokens = files.reduce((sum, file) => {
|
|
663
|
-
const node = graph.nodes.get(file);
|
|
664
|
-
return sum + (node?.tokenCost || 0);
|
|
665
|
-
}, 0);
|
|
666
|
-
const baseFragmentation = calculateFragmentation(files, domain, {
|
|
667
|
-
useLogScale: !!options?.useLogScale
|
|
668
|
-
});
|
|
669
|
-
let importSimilarityTotal = 0;
|
|
670
|
-
let importComparisons = 0;
|
|
671
|
-
for (let i = 0; i < files.length; i++) {
|
|
672
|
-
for (let j = i + 1; j < files.length; j++) {
|
|
673
|
-
const f1 = files[i];
|
|
674
|
-
const f2 = files[j];
|
|
675
|
-
const n1 = graph.nodes.get(f1)?.imports || [];
|
|
676
|
-
const n2 = graph.nodes.get(f2)?.imports || [];
|
|
677
|
-
const similarity = n1.length === 0 && n2.length === 0 ? 0 : calculateJaccardSimilarity(n1, n2);
|
|
678
|
-
importSimilarityTotal += similarity;
|
|
679
|
-
importComparisons++;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
const importCohesion = importComparisons > 0 ? importSimilarityTotal / importComparisons : 0;
|
|
683
|
-
const couplingDiscountFactor = 1 - 0.2 * importCohesion;
|
|
684
|
-
const fragmentationScore = baseFragmentation * couplingDiscountFactor;
|
|
685
|
-
const pathEntropy = calculatePathEntropy(files);
|
|
686
|
-
const directoryDistance = calculateDirectoryDistance(files);
|
|
687
|
-
const avgCohesion = files.reduce((sum, file) => {
|
|
688
|
-
const node = graph.nodes.get(file);
|
|
689
|
-
return sum + (node ? calculateCohesion(node.exports, file, {
|
|
690
|
-
coUsageMatrix: graph.coUsageMatrix
|
|
691
|
-
}) : 0);
|
|
692
|
-
}, 0) / files.length;
|
|
693
|
-
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
694
|
-
const consolidationPlan = generateConsolidationPlan(
|
|
695
|
-
domain,
|
|
696
|
-
files,
|
|
697
|
-
targetFiles
|
|
698
|
-
);
|
|
699
|
-
clusters.push({
|
|
700
|
-
domain,
|
|
701
|
-
files,
|
|
702
|
-
totalTokens,
|
|
703
|
-
fragmentationScore,
|
|
704
|
-
pathEntropy,
|
|
705
|
-
directoryDistance,
|
|
706
|
-
importCohesion,
|
|
707
|
-
avgCohesion,
|
|
708
|
-
suggestedStructure: {
|
|
709
|
-
targetFiles,
|
|
710
|
-
consolidationPlan
|
|
711
|
-
}
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
return clusters.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
|
|
715
|
-
}
|
|
716
|
-
function extractExports(content, filePath, domainOptions, fileImports) {
|
|
717
|
-
const exports2 = [];
|
|
718
|
-
const patterns = [
|
|
719
|
-
/export\s+function\s+(\w+)/g,
|
|
720
|
-
/export\s+class\s+(\w+)/g,
|
|
721
|
-
/export\s+const\s+(\w+)/g,
|
|
722
|
-
/export\s+type\s+(\w+)/g,
|
|
723
|
-
/export\s+interface\s+(\w+)/g,
|
|
724
|
-
/export\s+default/g
|
|
725
|
-
];
|
|
726
|
-
const types = [
|
|
727
|
-
"function",
|
|
728
|
-
"class",
|
|
729
|
-
"const",
|
|
730
|
-
"type",
|
|
731
|
-
"interface",
|
|
732
|
-
"default"
|
|
733
|
-
];
|
|
734
|
-
patterns.forEach((pattern, index) => {
|
|
735
|
-
let match;
|
|
736
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
737
|
-
const name = match[1] || "default";
|
|
738
|
-
const type = types[index];
|
|
739
|
-
const inferredDomain = inferDomain(
|
|
740
|
-
name,
|
|
741
|
-
filePath,
|
|
742
|
-
domainOptions,
|
|
743
|
-
fileImports
|
|
744
|
-
);
|
|
745
|
-
exports2.push({ name, type, inferredDomain });
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
return exports2;
|
|
749
|
-
}
|
|
750
|
-
function inferDomain(name, filePath, domainOptions, fileImports) {
|
|
751
|
-
const lower = name.toLowerCase();
|
|
752
|
-
const tokens = Array.from(
|
|
753
|
-
new Set(
|
|
754
|
-
lower.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^a-z0-9]+/gi, " ").split(" ").filter(Boolean)
|
|
755
|
-
)
|
|
756
|
-
);
|
|
757
|
-
const defaultKeywords = [
|
|
758
|
-
"authentication",
|
|
759
|
-
"authorization",
|
|
760
|
-
"payment",
|
|
761
|
-
"invoice",
|
|
762
|
-
"customer",
|
|
763
|
-
"product",
|
|
764
|
-
"order",
|
|
765
|
-
"cart",
|
|
766
|
-
"user",
|
|
767
|
-
"admin",
|
|
768
|
-
"repository",
|
|
769
|
-
"controller",
|
|
770
|
-
"service",
|
|
771
|
-
"config",
|
|
772
|
-
"model",
|
|
773
|
-
"view",
|
|
774
|
-
"auth"
|
|
775
|
-
];
|
|
776
|
-
const domainKeywords = domainOptions?.domainKeywords && domainOptions.domainKeywords.length ? [...domainOptions.domainKeywords, ...defaultKeywords] : defaultKeywords;
|
|
777
|
-
for (const keyword of domainKeywords) {
|
|
778
|
-
if (tokens.includes(keyword)) {
|
|
779
|
-
return keyword;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
for (const keyword of domainKeywords) {
|
|
783
|
-
if (lower.includes(keyword)) {
|
|
784
|
-
return keyword;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
if (fileImports && fileImports.length > 0) {
|
|
788
|
-
for (const importPath of fileImports) {
|
|
789
|
-
const allSegments = importPath.split("/");
|
|
790
|
-
const relevantSegments = allSegments.filter((s) => {
|
|
791
|
-
if (!s) return false;
|
|
792
|
-
if (s === "." || s === "..") return false;
|
|
793
|
-
if (s.startsWith("@") && s.length === 1) return false;
|
|
794
|
-
return true;
|
|
795
|
-
}).map((s) => s.startsWith("@") ? s.slice(1) : s);
|
|
796
|
-
for (const segment of relevantSegments) {
|
|
797
|
-
const segLower = segment.toLowerCase();
|
|
798
|
-
const singularSegment = singularize(segLower);
|
|
799
|
-
for (const keyword of domainKeywords) {
|
|
800
|
-
if (singularSegment === keyword || segLower === keyword || segLower.includes(keyword)) {
|
|
801
|
-
return keyword;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
if (filePath) {
|
|
808
|
-
const pathSegments = filePath.toLowerCase().split("/");
|
|
809
|
-
for (const segment of pathSegments) {
|
|
810
|
-
const singularSegment = singularize(segment);
|
|
811
|
-
for (const keyword of domainKeywords) {
|
|
812
|
-
if (singularSegment === keyword || segment === keyword || segment.includes(keyword)) {
|
|
813
|
-
return keyword;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
return "unknown";
|
|
819
|
-
}
|
|
820
|
-
function generateConsolidationPlan(domain, files, targetFiles) {
|
|
821
|
-
const plan = [];
|
|
822
|
-
if (files.length <= targetFiles) {
|
|
823
|
-
return [`No consolidation needed for ${domain}`];
|
|
824
|
-
}
|
|
825
|
-
plan.push(
|
|
826
|
-
`Consolidate ${files.length} ${domain} files into ${targetFiles} cohesive file(s):`
|
|
827
|
-
);
|
|
828
|
-
const dirGroups = /* @__PURE__ */ new Map();
|
|
829
|
-
for (const file of files) {
|
|
830
|
-
const dir = file.split("/").slice(0, -1).join("/");
|
|
831
|
-
if (!dirGroups.has(dir)) {
|
|
832
|
-
dirGroups.set(dir, []);
|
|
833
|
-
}
|
|
834
|
-
dirGroups.get(dir).push(file);
|
|
835
|
-
}
|
|
836
|
-
plan.push(`1. Create unified ${domain} module file`);
|
|
837
|
-
plan.push(
|
|
838
|
-
`2. Move related functionality from ${files.length} scattered files`
|
|
839
|
-
);
|
|
840
|
-
plan.push(`3. Update imports in dependent files`);
|
|
841
|
-
plan.push(
|
|
842
|
-
`4. Remove old files after consolidation (verify with tests first)`
|
|
843
|
-
);
|
|
844
|
-
return plan;
|
|
845
|
-
}
|
|
846
|
-
function extractExportsWithAST(content, filePath, domainOptions, fileImports) {
|
|
847
|
-
try {
|
|
848
|
-
const { exports: astExports } = (0, import_core.parseFileExports)(content, filePath);
|
|
849
|
-
return astExports.map((exp) => ({
|
|
850
|
-
name: exp.name,
|
|
851
|
-
type: exp.type,
|
|
852
|
-
inferredDomain: inferDomain(
|
|
853
|
-
exp.name,
|
|
854
|
-
filePath,
|
|
855
|
-
domainOptions,
|
|
856
|
-
fileImports
|
|
857
|
-
),
|
|
858
|
-
imports: exp.imports,
|
|
859
|
-
dependencies: exp.dependencies
|
|
860
|
-
}));
|
|
861
|
-
} catch (error) {
|
|
862
|
-
void error;
|
|
863
|
-
return extractExports(content, filePath, domainOptions, fileImports);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
function calculateEnhancedCohesion(exports2, filePath, options) {
|
|
867
|
-
if (exports2.length === 0) return 1;
|
|
868
|
-
if (exports2.length === 1) return 1;
|
|
869
|
-
if (filePath && isTestFile(filePath)) {
|
|
870
|
-
return 1;
|
|
871
|
-
}
|
|
872
|
-
const domainCohesion = calculateDomainCohesion(exports2);
|
|
873
|
-
const hasImportData = exports2.some((e) => e.imports && e.imports.length > 0);
|
|
874
|
-
const importCohesion = hasImportData ? calculateImportBasedCohesion(exports2) : void 0;
|
|
875
|
-
const coUsageMatrix = options?.coUsageMatrix;
|
|
876
|
-
const structuralCohesion = filePath && coUsageMatrix ? calculateStructuralCohesionFromCoUsage(filePath, coUsageMatrix) : void 0;
|
|
877
|
-
const defaultWeights = {
|
|
878
|
-
importBased: 0.5,
|
|
879
|
-
structural: 0.3,
|
|
880
|
-
domainBased: 0.2
|
|
881
|
-
};
|
|
882
|
-
const weights = { ...defaultWeights, ...options?.weights || {} };
|
|
883
|
-
const signals = [];
|
|
884
|
-
if (importCohesion !== void 0)
|
|
885
|
-
signals.push({ score: importCohesion, weight: weights.importBased });
|
|
886
|
-
if (structuralCohesion !== void 0)
|
|
887
|
-
signals.push({ score: structuralCohesion, weight: weights.structural });
|
|
888
|
-
signals.push({ score: domainCohesion, weight: weights.domainBased });
|
|
889
|
-
const totalWeight = signals.reduce((s, el) => s + el.weight, 0);
|
|
890
|
-
if (totalWeight === 0) return domainCohesion;
|
|
891
|
-
const combined = signals.reduce(
|
|
892
|
-
(sum, el) => sum + el.score * (el.weight / totalWeight),
|
|
893
|
-
0
|
|
894
|
-
);
|
|
895
|
-
return combined;
|
|
896
|
-
}
|
|
897
|
-
function calculateStructuralCohesionFromCoUsage(file, coUsageMatrix) {
|
|
898
|
-
if (!coUsageMatrix) return 1;
|
|
899
|
-
const coUsages = coUsageMatrix.get(file);
|
|
900
|
-
if (!coUsages || coUsages.size === 0) return 1;
|
|
901
|
-
let total = 0;
|
|
902
|
-
for (const count of coUsages.values()) total += count;
|
|
903
|
-
if (total === 0) return 1;
|
|
904
|
-
const probs = [];
|
|
905
|
-
for (const count of coUsages.values()) {
|
|
906
|
-
if (count > 0) probs.push(count / total);
|
|
907
|
-
}
|
|
908
|
-
if (probs.length <= 1) return 1;
|
|
909
|
-
let entropy = 0;
|
|
910
|
-
for (const prob of probs) {
|
|
911
|
-
entropy -= prob * Math.log2(prob);
|
|
912
|
-
}
|
|
913
|
-
const maxEntropy = Math.log2(probs.length);
|
|
914
|
-
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
915
|
-
}
|
|
916
|
-
function calculateImportBasedCohesion(exports2) {
|
|
917
|
-
const exportsWithImports = exports2.filter(
|
|
918
|
-
(e) => e.imports && e.imports.length > 0
|
|
919
|
-
);
|
|
920
|
-
if (exportsWithImports.length < 2) {
|
|
921
|
-
return 1;
|
|
922
|
-
}
|
|
923
|
-
let totalSimilarity = 0;
|
|
924
|
-
let comparisons = 0;
|
|
925
|
-
for (let i = 0; i < exportsWithImports.length; i++) {
|
|
926
|
-
for (let j = i + 1; j < exportsWithImports.length; j++) {
|
|
927
|
-
const exp1 = exportsWithImports[i];
|
|
928
|
-
const exp2 = exportsWithImports[j];
|
|
929
|
-
const similarity = calculateJaccardSimilarity(exp1.imports, exp2.imports);
|
|
930
|
-
totalSimilarity += similarity;
|
|
931
|
-
comparisons++;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
return comparisons > 0 ? totalSimilarity / comparisons : 1;
|
|
935
|
-
}
|
|
936
|
-
function calculateJaccardSimilarity(arr1, arr2) {
|
|
937
|
-
if (arr1.length === 0 && arr2.length === 0) return 1;
|
|
938
|
-
if (arr1.length === 0 || arr2.length === 0) return 0;
|
|
939
|
-
const set1 = new Set(arr1);
|
|
940
|
-
const set2 = new Set(arr2);
|
|
941
|
-
const intersection = new Set([...set1].filter((x) => set2.has(x)));
|
|
942
|
-
const union = /* @__PURE__ */ new Set([...set1, ...set2]);
|
|
943
|
-
return intersection.size / union.size;
|
|
944
|
-
}
|
|
945
|
-
function calculateDomainCohesion(exports2) {
|
|
946
|
-
const domains = exports2.map((e) => e.inferredDomain || "unknown");
|
|
947
|
-
const domainCounts = /* @__PURE__ */ new Map();
|
|
948
|
-
for (const domain of domains) {
|
|
949
|
-
domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
|
|
950
|
-
}
|
|
951
|
-
const total = domains.length;
|
|
952
|
-
let entropy = 0;
|
|
953
|
-
for (const domainCount of domainCounts.values()) {
|
|
954
|
-
const prob = domainCount / total;
|
|
955
|
-
if (prob > 0) {
|
|
956
|
-
entropy -= prob * Math.log2(prob);
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
const maxEntropy = Math.log2(total);
|
|
960
|
-
return maxEntropy > 0 ? 1 - entropy / maxEntropy : 1;
|
|
961
|
-
}
|
|
962
|
-
function classifyFile(node, cohesionScore, domains) {
|
|
963
|
-
const { exports: exports2, imports, linesOfCode, file } = node;
|
|
964
|
-
void imports;
|
|
965
|
-
void linesOfCode;
|
|
800
|
+
|
|
801
|
+
// src/classifier.ts
|
|
802
|
+
function classifyFile(node, cohesionScore = 1, domains = []) {
|
|
966
803
|
if (isBarrelExport(node)) {
|
|
967
804
|
return "barrel-export";
|
|
968
805
|
}
|
|
969
|
-
if (
|
|
806
|
+
if (isTypeDefinition(node)) {
|
|
970
807
|
return "type-definition";
|
|
971
808
|
}
|
|
972
|
-
if (
|
|
973
|
-
return "
|
|
809
|
+
if (isNextJsPage(node)) {
|
|
810
|
+
return "nextjs-page";
|
|
974
811
|
}
|
|
975
812
|
if (isLambdaHandler(node)) {
|
|
976
813
|
return "lambda-handler";
|
|
977
814
|
}
|
|
978
|
-
if (
|
|
979
|
-
return "
|
|
815
|
+
if (isServiceFile(node)) {
|
|
816
|
+
return "service-file";
|
|
980
817
|
}
|
|
981
818
|
if (isEmailTemplate(node)) {
|
|
982
819
|
return "email-template";
|
|
@@ -984,224 +821,53 @@ function classifyFile(node, cohesionScore, domains) {
|
|
|
984
821
|
if (isParserFile(node)) {
|
|
985
822
|
return "parser-file";
|
|
986
823
|
}
|
|
987
|
-
if (isServiceFile(node)) {
|
|
988
|
-
return "service-file";
|
|
989
|
-
}
|
|
990
824
|
if (isSessionFile(node)) {
|
|
991
|
-
return "cohesive-module";
|
|
992
|
-
}
|
|
993
|
-
if (isNextJsPage(node)) {
|
|
994
|
-
return "nextjs-page";
|
|
995
|
-
}
|
|
996
|
-
if (isUtilityFile(node)) {
|
|
825
|
+
if (cohesionScore >= 0.25 && domains.length <= 1) return "cohesive-module";
|
|
997
826
|
return "utility-module";
|
|
998
827
|
}
|
|
999
|
-
if (
|
|
828
|
+
if (isUtilityModule(node)) {
|
|
1000
829
|
return "utility-module";
|
|
1001
830
|
}
|
|
1002
|
-
|
|
1003
|
-
const hasSingleDomain = uniqueDomains.length <= 1;
|
|
1004
|
-
if (hasSingleDomain) {
|
|
831
|
+
if (isConfigFile(node)) {
|
|
1005
832
|
return "cohesive-module";
|
|
1006
833
|
}
|
|
1007
|
-
if (
|
|
834
|
+
if (domains.length <= 1 && domains[0] !== "unknown") {
|
|
1008
835
|
return "cohesive-module";
|
|
1009
836
|
}
|
|
1010
|
-
|
|
1011
|
-
const hasLowCohesion = cohesionScore < 0.4;
|
|
1012
|
-
if (hasMultipleDomains && hasLowCohesion) {
|
|
837
|
+
if (domains.length > 1 && cohesionScore < 0.4) {
|
|
1013
838
|
return "mixed-concerns";
|
|
1014
839
|
}
|
|
1015
|
-
if (cohesionScore >= 0.
|
|
840
|
+
if (cohesionScore >= 0.7) {
|
|
1016
841
|
return "cohesive-module";
|
|
1017
842
|
}
|
|
1018
843
|
return "unknown";
|
|
1019
844
|
}
|
|
1020
845
|
function isBarrelExport(node) {
|
|
1021
|
-
const { file, exports: exports2, imports, linesOfCode } = node;
|
|
1022
|
-
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1023
|
-
const isIndexFile = fileName === "index.ts" || fileName === "index.js" || fileName === "index.tsx" || fileName === "index.jsx";
|
|
1024
|
-
const hasReExports = exports2.length > 0 && imports.length > 0;
|
|
1025
|
-
const highExportToLinesRatio = exports2.length > 3 && linesOfCode < exports2.length * 5;
|
|
1026
|
-
const sparseCode = linesOfCode > 0 && linesOfCode < 50 && exports2.length >= 2;
|
|
1027
|
-
if (isIndexFile && hasReExports) {
|
|
1028
|
-
return true;
|
|
1029
|
-
}
|
|
1030
|
-
if (highExportToLinesRatio && imports.length >= exports2.length * 0.5) {
|
|
1031
|
-
return true;
|
|
1032
|
-
}
|
|
1033
|
-
if (sparseCode && imports.length > 0) {
|
|
1034
|
-
return true;
|
|
1035
|
-
}
|
|
1036
|
-
return false;
|
|
1037
|
-
}
|
|
1038
|
-
function isTypeDefinitionFile(node) {
|
|
1039
|
-
const { file, exports: exports2 } = node;
|
|
1040
|
-
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1041
|
-
const isTypesFile = fileName?.includes("types") || fileName?.includes(".d.ts") || fileName === "types.ts" || fileName === "interfaces.ts";
|
|
1042
|
-
const lowerPath = file.toLowerCase();
|
|
1043
|
-
const isTypesPath = lowerPath.includes("/types/") || lowerPath.includes("/typings/") || lowerPath.includes("/@types/") || lowerPath.startsWith("types/") || lowerPath.startsWith("typings/");
|
|
1044
|
-
const typeExports = exports2.filter(
|
|
1045
|
-
(e) => e.type === "type" || e.type === "interface"
|
|
1046
|
-
);
|
|
1047
|
-
const runtimeExports = exports2.filter(
|
|
1048
|
-
(e) => e.type === "function" || e.type === "class" || e.type === "const"
|
|
1049
|
-
);
|
|
1050
|
-
const mostlyTypes = exports2.length > 0 && typeExports.length > runtimeExports.length && typeExports.length / exports2.length > 0.7;
|
|
1051
|
-
const pureTypeFile = exports2.length > 0 && typeExports.length === exports2.length;
|
|
1052
|
-
const emptyOrReExportInTypesDir = isTypesPath && exports2.length === 0;
|
|
1053
|
-
return isTypesFile || isTypesPath || mostlyTypes || pureTypeFile || emptyOrReExportInTypesDir;
|
|
1054
|
-
}
|
|
1055
|
-
function isConfigOrSchemaFile(node) {
|
|
1056
|
-
const { file, exports: exports2 } = node;
|
|
1057
|
-
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1058
|
-
const configPatterns = [
|
|
1059
|
-
"config",
|
|
1060
|
-
"schema",
|
|
1061
|
-
"settings",
|
|
1062
|
-
"options",
|
|
1063
|
-
"constants",
|
|
1064
|
-
"env",
|
|
1065
|
-
"environment",
|
|
1066
|
-
".config.",
|
|
1067
|
-
"-config.",
|
|
1068
|
-
"_config."
|
|
1069
|
-
];
|
|
1070
|
-
const isConfigName = configPatterns.some(
|
|
1071
|
-
(pattern) => fileName?.includes(pattern) || fileName?.startsWith(pattern) || fileName?.endsWith(`${pattern}.ts`)
|
|
1072
|
-
);
|
|
1073
|
-
const isConfigPath = file.toLowerCase().includes("/config/") || file.toLowerCase().includes("/schemas/") || file.toLowerCase().includes("/settings/");
|
|
1074
|
-
const hasSchemaExports = exports2.some(
|
|
1075
|
-
(e) => e.name.toLowerCase().includes("table") || e.name.toLowerCase().includes("schema") || e.name.toLowerCase().includes("config") || e.name.toLowerCase().includes("setting")
|
|
1076
|
-
);
|
|
1077
|
-
return isConfigName || isConfigPath || hasSchemaExports;
|
|
1078
|
-
}
|
|
1079
|
-
function isUtilityFile(node) {
|
|
1080
846
|
const { file, exports: exports2 } = node;
|
|
1081
847
|
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
"
|
|
1086
|
-
"helper",
|
|
1087
|
-
"helpers",
|
|
1088
|
-
"common",
|
|
1089
|
-
"shared",
|
|
1090
|
-
"toolbox",
|
|
1091
|
-
"toolkit",
|
|
1092
|
-
".util.",
|
|
1093
|
-
"-util.",
|
|
1094
|
-
"_util.",
|
|
1095
|
-
"-utils.",
|
|
1096
|
-
".utils."
|
|
1097
|
-
];
|
|
1098
|
-
const isUtilityName = utilityPatterns.some(
|
|
1099
|
-
(pattern) => fileName?.includes(pattern)
|
|
848
|
+
const isIndexFile = fileName === "index.ts" || fileName === "index.js";
|
|
849
|
+
const isSmallAndManyExports = node.tokenCost < 1e3 && (exports2 || []).length > 5;
|
|
850
|
+
const isReexportPattern = (exports2 || []).length >= 5 && (exports2 || []).every(
|
|
851
|
+
(e) => e.type === "const" || e.type === "function" || e.type === "type" || e.type === "interface"
|
|
1100
852
|
);
|
|
1101
|
-
|
|
1102
|
-
const hasManySmallExportsInUtilityContext = exports2.length >= 3 && exports2.every((e) => e.type === "function" || e.type === "const") && (isUtilityName || isUtilityPath);
|
|
1103
|
-
return isUtilityName || isUtilityPath || hasManySmallExportsInUtilityContext;
|
|
1104
|
-
}
|
|
1105
|
-
function splitCamelCase(name) {
|
|
1106
|
-
return name.replace(/([A-Z])/g, " $1").trim().toLowerCase().split(/[\s_-]+/).filter(Boolean);
|
|
1107
|
-
}
|
|
1108
|
-
var SKIP_WORDS = /* @__PURE__ */ new Set([
|
|
1109
|
-
"get",
|
|
1110
|
-
"set",
|
|
1111
|
-
"create",
|
|
1112
|
-
"update",
|
|
1113
|
-
"delete",
|
|
1114
|
-
"fetch",
|
|
1115
|
-
"save",
|
|
1116
|
-
"load",
|
|
1117
|
-
"parse",
|
|
1118
|
-
"format",
|
|
1119
|
-
"validate",
|
|
1120
|
-
"convert",
|
|
1121
|
-
"transform",
|
|
1122
|
-
"build",
|
|
1123
|
-
"generate",
|
|
1124
|
-
"render",
|
|
1125
|
-
"send",
|
|
1126
|
-
"receive",
|
|
1127
|
-
"find",
|
|
1128
|
-
"list",
|
|
1129
|
-
"add",
|
|
1130
|
-
"remove",
|
|
1131
|
-
"insert",
|
|
1132
|
-
"upsert",
|
|
1133
|
-
"put",
|
|
1134
|
-
"read",
|
|
1135
|
-
"write",
|
|
1136
|
-
"check",
|
|
1137
|
-
"handle",
|
|
1138
|
-
"process",
|
|
1139
|
-
"compute",
|
|
1140
|
-
"calculate",
|
|
1141
|
-
"init",
|
|
1142
|
-
"reset",
|
|
1143
|
-
"clear",
|
|
1144
|
-
"pending",
|
|
1145
|
-
"active",
|
|
1146
|
-
"current",
|
|
1147
|
-
"new",
|
|
1148
|
-
"old",
|
|
1149
|
-
"all",
|
|
1150
|
-
"by",
|
|
1151
|
-
"with",
|
|
1152
|
-
"from",
|
|
1153
|
-
"to",
|
|
1154
|
-
"and",
|
|
1155
|
-
"or",
|
|
1156
|
-
"is",
|
|
1157
|
-
"has",
|
|
1158
|
-
"in",
|
|
1159
|
-
"on",
|
|
1160
|
-
"of",
|
|
1161
|
-
"the"
|
|
1162
|
-
]);
|
|
1163
|
-
function simpleSingularize(word) {
|
|
1164
|
-
if (word.endsWith("ies") && word.length > 3) return word.slice(0, -3) + "y";
|
|
1165
|
-
if (word.endsWith("ses") && word.length > 4) return word.slice(0, -2);
|
|
1166
|
-
if (word.endsWith("s") && word.length > 3) return word.slice(0, -1);
|
|
1167
|
-
return word;
|
|
1168
|
-
}
|
|
1169
|
-
function extractEntityNouns(name) {
|
|
1170
|
-
return splitCamelCase(name).filter((token) => !SKIP_WORDS.has(token) && token.length > 2).map(simpleSingularize);
|
|
853
|
+
return !!isIndexFile || !!isSmallAndManyExports || !!isReexportPattern;
|
|
1171
854
|
}
|
|
1172
|
-
function
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
const
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
);
|
|
1180
|
-
return
|
|
855
|
+
function isTypeDefinition(node) {
|
|
856
|
+
const { file } = node;
|
|
857
|
+
if (file.endsWith(".d.ts")) return true;
|
|
858
|
+
const nodeExports = node.exports || [];
|
|
859
|
+
const hasExports = nodeExports.length > 0;
|
|
860
|
+
const areAllTypes = hasExports && nodeExports.every((e) => e.type === "type" || e.type === "interface");
|
|
861
|
+
const allTypes = !!areAllTypes;
|
|
862
|
+
const isTypePath = file.toLowerCase().includes("/types/") || file.toLowerCase().includes("/interfaces/") || file.toLowerCase().includes("/models/");
|
|
863
|
+
return allTypes || isTypePath && hasExports;
|
|
1181
864
|
}
|
|
1182
|
-
function
|
|
1183
|
-
const { file
|
|
865
|
+
function isUtilityModule(node) {
|
|
866
|
+
const { file } = node;
|
|
867
|
+
const isUtilPath = file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/") || file.toLowerCase().includes("/util/") || file.toLowerCase().includes("/helper/");
|
|
1184
868
|
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1187
|
-
"database",
|
|
1188
|
-
"repository",
|
|
1189
|
-
"repo",
|
|
1190
|
-
"dao",
|
|
1191
|
-
"firestore",
|
|
1192
|
-
"postgres",
|
|
1193
|
-
"mysql",
|
|
1194
|
-
"mongo",
|
|
1195
|
-
"redis",
|
|
1196
|
-
"sqlite",
|
|
1197
|
-
"supabase",
|
|
1198
|
-
"prisma"
|
|
1199
|
-
];
|
|
1200
|
-
const isDalName = dalPatterns.some((p) => fileName?.includes(p));
|
|
1201
|
-
const isDalPath = file.toLowerCase().includes("/repositories/") || file.toLowerCase().includes("/dao/") || file.toLowerCase().includes("/data/");
|
|
1202
|
-
const hasDalExportPattern = exports2.length >= 1 && exports2.length <= 10 && allExportsShareEntityNoun(exports2);
|
|
1203
|
-
const isUtilityPathLocal = file.toLowerCase().includes("/utils/") || file.toLowerCase().includes("/helpers/");
|
|
1204
|
-
return isDalPath || isDalName && hasDalExportPattern && !isUtilityPathLocal;
|
|
869
|
+
const isUtilName = fileName?.includes("utils.") || fileName?.includes("helpers.") || fileName?.includes("util.") || fileName?.includes("helper.");
|
|
870
|
+
return !!isUtilPath || !!isUtilName;
|
|
1205
871
|
}
|
|
1206
872
|
function isLambdaHandler(node) {
|
|
1207
873
|
const { file, exports: exports2 } = node;
|
|
@@ -1218,11 +884,10 @@ function isLambdaHandler(node) {
|
|
|
1218
884
|
(pattern) => fileName?.includes(pattern)
|
|
1219
885
|
);
|
|
1220
886
|
const isHandlerPath = file.toLowerCase().includes("/handlers/") || file.toLowerCase().includes("/lambdas/") || file.toLowerCase().includes("/lambda/") || file.toLowerCase().includes("/functions/");
|
|
1221
|
-
const hasHandlerExport = exports2.some(
|
|
887
|
+
const hasHandlerExport = (exports2 || []).some(
|
|
1222
888
|
(e) => e.name.toLowerCase() === "handler" || e.name.toLowerCase() === "main" || e.name.toLowerCase() === "lambdahandler" || e.name.toLowerCase().endsWith("handler")
|
|
1223
889
|
);
|
|
1224
|
-
|
|
1225
|
-
return isHandlerName || isHandlerPath || hasHandlerExport || hasSingleEntryInHandlerContext;
|
|
890
|
+
return !!isHandlerName || !!isHandlerPath || !!hasHandlerExport;
|
|
1226
891
|
}
|
|
1227
892
|
function isServiceFile(node) {
|
|
1228
893
|
const { file, exports: exports2 } = node;
|
|
@@ -1232,11 +897,11 @@ function isServiceFile(node) {
|
|
|
1232
897
|
(pattern) => fileName?.includes(pattern)
|
|
1233
898
|
);
|
|
1234
899
|
const isServicePath = file.toLowerCase().includes("/services/");
|
|
1235
|
-
const hasServiceNamedExport = exports2.some(
|
|
900
|
+
const hasServiceNamedExport = (exports2 || []).some(
|
|
1236
901
|
(e) => e.name.toLowerCase().includes("service") || e.name.toLowerCase().endsWith("service")
|
|
1237
902
|
);
|
|
1238
|
-
const hasClassExport = exports2.some((e) => e.type === "class");
|
|
1239
|
-
return isServiceName || isServicePath || hasServiceNamedExport && hasClassExport;
|
|
903
|
+
const hasClassExport = (exports2 || []).some((e) => e.type === "class");
|
|
904
|
+
return !!isServiceName || !!isServicePath || !!hasServiceNamedExport && !!hasClassExport;
|
|
1240
905
|
}
|
|
1241
906
|
function isEmailTemplate(node) {
|
|
1242
907
|
const { file, exports: exports2 } = node;
|
|
@@ -1254,15 +919,11 @@ function isEmailTemplate(node) {
|
|
|
1254
919
|
const isEmailTemplateName = emailTemplatePatterns.some(
|
|
1255
920
|
(pattern) => fileName?.includes(pattern)
|
|
1256
921
|
);
|
|
1257
|
-
const isSpecificTemplateName = fileName?.includes("receipt") || fileName?.includes("invoice-email") || fileName?.includes("welcome-email") || fileName?.includes("notification-email") || fileName?.includes("writer") && fileName.includes("receipt");
|
|
1258
922
|
const isEmailPath = file.toLowerCase().includes("/emails/") || file.toLowerCase().includes("/mail/") || file.toLowerCase().includes("/notifications/");
|
|
1259
|
-
const hasTemplateFunction = exports2.some(
|
|
923
|
+
const hasTemplateFunction = (exports2 || []).some(
|
|
1260
924
|
(e) => e.type === "function" && (e.name.toLowerCase().startsWith("render") || e.name.toLowerCase().startsWith("generate") || e.name.toLowerCase().includes("template") && e.name.toLowerCase().includes("email"))
|
|
1261
925
|
);
|
|
1262
|
-
|
|
1263
|
-
(e) => e.name.toLowerCase().includes("template") && e.type === "function" || e.name.toLowerCase().includes("render") && e.type === "function" || e.name.toLowerCase().includes("email") && e.type !== "class"
|
|
1264
|
-
);
|
|
1265
|
-
return isEmailPath || isEmailTemplateName || isSpecificTemplateName || hasTemplateFunction && hasEmailExport;
|
|
926
|
+
return !!isEmailPath || !!isEmailTemplateName || !!hasTemplateFunction;
|
|
1266
927
|
}
|
|
1267
928
|
function isParserFile(node) {
|
|
1268
929
|
const { file, exports: exports2 } = node;
|
|
@@ -1274,55 +935,51 @@ function isParserFile(node) {
|
|
|
1274
935
|
"_parser.",
|
|
1275
936
|
"transform",
|
|
1276
937
|
".transform.",
|
|
1277
|
-
"-transform.",
|
|
1278
938
|
"converter",
|
|
1279
|
-
".converter.",
|
|
1280
|
-
"-converter.",
|
|
1281
939
|
"mapper",
|
|
1282
|
-
"
|
|
1283
|
-
"-mapper.",
|
|
1284
|
-
"serializer",
|
|
1285
|
-
".serializer.",
|
|
1286
|
-
"deterministic"
|
|
1287
|
-
// For base-parser-deterministic.ts pattern
|
|
940
|
+
"serializer"
|
|
1288
941
|
];
|
|
1289
942
|
const isParserName = parserPatterns.some(
|
|
1290
943
|
(pattern) => fileName?.includes(pattern)
|
|
1291
944
|
);
|
|
1292
|
-
const isParserPath = file.toLowerCase().includes("/parsers/") || file.toLowerCase().includes("/transformers/")
|
|
1293
|
-
const
|
|
1294
|
-
(e) => e.
|
|
945
|
+
const isParserPath = file.toLowerCase().includes("/parsers/") || file.toLowerCase().includes("/transformers/");
|
|
946
|
+
const hasParseFunction = (exports2 || []).some(
|
|
947
|
+
(e) => e.type === "function" && (e.name.toLowerCase().startsWith("parse") || e.name.toLowerCase().startsWith("transform") || e.name.toLowerCase().startsWith("extract"))
|
|
1295
948
|
);
|
|
1296
|
-
|
|
1297
|
-
(e) => e.type === "function" && (e.name.toLowerCase().startsWith("parse") || e.name.toLowerCase().startsWith("transform") || e.name.toLowerCase().startsWith("convert") || e.name.toLowerCase().startsWith("map") || e.name.toLowerCase().startsWith("extract"))
|
|
1298
|
-
);
|
|
1299
|
-
return isParserName || isParserPath || hasParserExport || hasParseFunction;
|
|
949
|
+
return !!isParserName || !!isParserPath || !!hasParseFunction;
|
|
1300
950
|
}
|
|
1301
951
|
function isSessionFile(node) {
|
|
1302
952
|
const { file, exports: exports2 } = node;
|
|
1303
953
|
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1304
|
-
const sessionPatterns = [
|
|
1305
|
-
"session",
|
|
1306
|
-
".session.",
|
|
1307
|
-
"-session.",
|
|
1308
|
-
"state",
|
|
1309
|
-
".state.",
|
|
1310
|
-
"-state.",
|
|
1311
|
-
"context",
|
|
1312
|
-
".context.",
|
|
1313
|
-
"-context.",
|
|
1314
|
-
"store",
|
|
1315
|
-
".store.",
|
|
1316
|
-
"-store."
|
|
1317
|
-
];
|
|
954
|
+
const sessionPatterns = ["session", "state", "context", "store"];
|
|
1318
955
|
const isSessionName = sessionPatterns.some(
|
|
1319
956
|
(pattern) => fileName?.includes(pattern)
|
|
1320
957
|
);
|
|
1321
|
-
const isSessionPath = file.toLowerCase().includes("/sessions/") || file.toLowerCase().includes("/state/")
|
|
1322
|
-
const hasSessionExport = exports2.some(
|
|
1323
|
-
(e) => e.name.toLowerCase().includes("session") || e.name.toLowerCase().includes("state") || e.name.toLowerCase().includes("
|
|
958
|
+
const isSessionPath = file.toLowerCase().includes("/sessions/") || file.toLowerCase().includes("/state/");
|
|
959
|
+
const hasSessionExport = (exports2 || []).some(
|
|
960
|
+
(e) => e.name.toLowerCase().includes("session") || e.name.toLowerCase().includes("state") || e.name.toLowerCase().includes("store")
|
|
961
|
+
);
|
|
962
|
+
return !!isSessionName || !!isSessionPath || !!hasSessionExport;
|
|
963
|
+
}
|
|
964
|
+
function isConfigFile(node) {
|
|
965
|
+
const { file, exports: exports2 } = node;
|
|
966
|
+
const lowerPath = file.toLowerCase();
|
|
967
|
+
const fileName = file.split("/").pop()?.toLowerCase();
|
|
968
|
+
const configPatterns = [
|
|
969
|
+
".config.",
|
|
970
|
+
"tsconfig",
|
|
971
|
+
"jest.config",
|
|
972
|
+
"package.json",
|
|
973
|
+
"aiready.json",
|
|
974
|
+
"next.config",
|
|
975
|
+
"sst.config"
|
|
976
|
+
];
|
|
977
|
+
const isConfigName = configPatterns.some((p) => fileName?.includes(p));
|
|
978
|
+
const isConfigPath = lowerPath.includes("/config/") || lowerPath.includes("/settings/") || lowerPath.includes("/schemas/");
|
|
979
|
+
const hasSchemaExports = (exports2 || []).some(
|
|
980
|
+
(e) => e.name.toLowerCase().includes("schema") || e.name.toLowerCase().includes("config") || e.name.toLowerCase().includes("setting")
|
|
1324
981
|
);
|
|
1325
|
-
return
|
|
982
|
+
return !!isConfigName || !!isConfigPath || !!hasSchemaExports;
|
|
1326
983
|
}
|
|
1327
984
|
function isNextJsPage(node) {
|
|
1328
985
|
const { file, exports: exports2 } = node;
|
|
@@ -1330,24 +987,19 @@ function isNextJsPage(node) {
|
|
|
1330
987
|
const fileName = file.split("/").pop()?.toLowerCase();
|
|
1331
988
|
const isInAppDir = lowerPath.includes("/app/") || lowerPath.startsWith("app/");
|
|
1332
989
|
const isPageFile = fileName === "page.tsx" || fileName === "page.ts";
|
|
1333
|
-
if (!isInAppDir || !isPageFile)
|
|
1334
|
-
|
|
1335
|
-
}
|
|
1336
|
-
const exportNames = exports2.map((e) => e.name.toLowerCase());
|
|
1337
|
-
const hasDefaultExport = exports2.some((e) => e.type === "default");
|
|
990
|
+
if (!isInAppDir || !isPageFile) return false;
|
|
991
|
+
const hasDefaultExport = (exports2 || []).some((e) => e.type === "default");
|
|
1338
992
|
const nextJsExports = [
|
|
1339
993
|
"metadata",
|
|
1340
994
|
"generatemetadata",
|
|
1341
995
|
"faqjsonld",
|
|
1342
996
|
"jsonld",
|
|
1343
|
-
"icon"
|
|
1344
|
-
"viewport",
|
|
1345
|
-
"dynamic"
|
|
997
|
+
"icon"
|
|
1346
998
|
];
|
|
1347
|
-
const hasNextJsExports =
|
|
1348
|
-
(
|
|
999
|
+
const hasNextJsExports = (exports2 || []).some(
|
|
1000
|
+
(e) => nextJsExports.includes(e.name.toLowerCase())
|
|
1349
1001
|
);
|
|
1350
|
-
return hasDefaultExport || hasNextJsExports;
|
|
1002
|
+
return !!hasDefaultExport || !!hasNextJsExports;
|
|
1351
1003
|
}
|
|
1352
1004
|
function adjustCohesionForClassification(baseCohesion, classification, node) {
|
|
1353
1005
|
switch (classification) {
|
|
@@ -1355,55 +1007,24 @@ function adjustCohesionForClassification(baseCohesion, classification, node) {
|
|
|
1355
1007
|
return 1;
|
|
1356
1008
|
case "type-definition":
|
|
1357
1009
|
return 1;
|
|
1010
|
+
case "nextjs-page":
|
|
1011
|
+
return 1;
|
|
1358
1012
|
case "utility-module": {
|
|
1359
|
-
if (node
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
|
|
1367
|
-
}
|
|
1368
|
-
case "service-file": {
|
|
1369
|
-
if (node?.exports.some((e) => e.type === "class")) {
|
|
1370
|
-
return Math.max(0.78, Math.min(1, baseCohesion + 0.4));
|
|
1371
|
-
}
|
|
1372
|
-
return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
|
|
1373
|
-
}
|
|
1374
|
-
case "lambda-handler": {
|
|
1375
|
-
if (node) {
|
|
1376
|
-
const hasSingleEntry = node.exports.length === 1 || node.exports.some((e) => e.name.toLowerCase() === "handler");
|
|
1377
|
-
if (hasSingleEntry) {
|
|
1378
|
-
return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
|
|
1379
|
-
}
|
|
1013
|
+
if (node && hasRelatedExportNames(
|
|
1014
|
+
(node.exports || []).map((e) => e.name.toLowerCase())
|
|
1015
|
+
)) {
|
|
1016
|
+
return Math.max(0.8, Math.min(1, baseCohesion + 0.45));
|
|
1380
1017
|
}
|
|
1381
1018
|
return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
|
|
1382
1019
|
}
|
|
1383
|
-
case "
|
|
1384
|
-
if (node) {
|
|
1385
|
-
const hasTemplateFunc = node.exports.some(
|
|
1386
|
-
(e) => e.name.toLowerCase().includes("render") || e.name.toLowerCase().includes("generate") || e.name.toLowerCase().includes("template")
|
|
1387
|
-
);
|
|
1388
|
-
if (hasTemplateFunc) {
|
|
1389
|
-
return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1020
|
+
case "service-file":
|
|
1392
1021
|
return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
);
|
|
1399
|
-
if (hasParseFunc) {
|
|
1400
|
-
return Math.max(0.75, Math.min(1, baseCohesion + 0.4));
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1022
|
+
case "lambda-handler":
|
|
1023
|
+
return Math.max(0.75, Math.min(1, baseCohesion + 0.35));
|
|
1024
|
+
case "email-template":
|
|
1025
|
+
return Math.max(0.72, Math.min(1, baseCohesion + 0.3));
|
|
1026
|
+
case "parser-file":
|
|
1403
1027
|
return Math.max(0.7, Math.min(1, baseCohesion + 0.3));
|
|
1404
|
-
}
|
|
1405
|
-
case "nextjs-page":
|
|
1406
|
-
return 1;
|
|
1407
1028
|
case "cohesive-module":
|
|
1408
1029
|
return Math.max(baseCohesion, 0.7);
|
|
1409
1030
|
case "mixed-concerns":
|
|
@@ -1416,113 +1037,41 @@ function hasRelatedExportNames(exportNames) {
|
|
|
1416
1037
|
if (exportNames.length < 2) return true;
|
|
1417
1038
|
const stems = /* @__PURE__ */ new Set();
|
|
1418
1039
|
const domains = /* @__PURE__ */ new Set();
|
|
1040
|
+
const verbs = [
|
|
1041
|
+
"get",
|
|
1042
|
+
"set",
|
|
1043
|
+
"create",
|
|
1044
|
+
"update",
|
|
1045
|
+
"delete",
|
|
1046
|
+
"fetch",
|
|
1047
|
+
"save",
|
|
1048
|
+
"load",
|
|
1049
|
+
"parse",
|
|
1050
|
+
"format",
|
|
1051
|
+
"validate"
|
|
1052
|
+
];
|
|
1053
|
+
const domainPatterns = [
|
|
1054
|
+
"user",
|
|
1055
|
+
"order",
|
|
1056
|
+
"product",
|
|
1057
|
+
"session",
|
|
1058
|
+
"email",
|
|
1059
|
+
"file",
|
|
1060
|
+
"db",
|
|
1061
|
+
"api",
|
|
1062
|
+
"config"
|
|
1063
|
+
];
|
|
1419
1064
|
for (const name of exportNames) {
|
|
1420
|
-
const verbs = [
|
|
1421
|
-
"get",
|
|
1422
|
-
"set",
|
|
1423
|
-
"create",
|
|
1424
|
-
"update",
|
|
1425
|
-
"delete",
|
|
1426
|
-
"fetch",
|
|
1427
|
-
"save",
|
|
1428
|
-
"load",
|
|
1429
|
-
"parse",
|
|
1430
|
-
"format",
|
|
1431
|
-
"validate",
|
|
1432
|
-
"convert",
|
|
1433
|
-
"transform",
|
|
1434
|
-
"build",
|
|
1435
|
-
"generate",
|
|
1436
|
-
"render",
|
|
1437
|
-
"send",
|
|
1438
|
-
"receive"
|
|
1439
|
-
];
|
|
1440
1065
|
for (const verb of verbs) {
|
|
1441
1066
|
if (name.startsWith(verb) && name.length > verb.length) {
|
|
1442
1067
|
stems.add(name.slice(verb.length).toLowerCase());
|
|
1443
1068
|
}
|
|
1444
1069
|
}
|
|
1445
|
-
const domainPatterns = [
|
|
1446
|
-
"user",
|
|
1447
|
-
"order",
|
|
1448
|
-
"product",
|
|
1449
|
-
"session",
|
|
1450
|
-
"email",
|
|
1451
|
-
"file",
|
|
1452
|
-
"db",
|
|
1453
|
-
"s3",
|
|
1454
|
-
"dynamo",
|
|
1455
|
-
"api",
|
|
1456
|
-
"config"
|
|
1457
|
-
];
|
|
1458
1070
|
for (const domain of domainPatterns) {
|
|
1459
|
-
if (name.includes(domain))
|
|
1460
|
-
domains.add(domain);
|
|
1461
|
-
}
|
|
1071
|
+
if (name.includes(domain)) domains.add(domain);
|
|
1462
1072
|
}
|
|
1463
1073
|
}
|
|
1464
|
-
if (stems.size === 1
|
|
1465
|
-
if (domains.size === 1 && exportNames.length >= 2) return true;
|
|
1466
|
-
const prefixes = exportNames.map((name) => {
|
|
1467
|
-
const match = name.match(/^([a-z]+)/);
|
|
1468
|
-
return match ? match[1] : "";
|
|
1469
|
-
}).filter((p) => p.length >= 3);
|
|
1470
|
-
if (prefixes.length >= 2) {
|
|
1471
|
-
const uniquePrefixes = new Set(prefixes);
|
|
1472
|
-
if (uniquePrefixes.size === 1) return true;
|
|
1473
|
-
}
|
|
1474
|
-
const nounSets = exportNames.map((name) => {
|
|
1475
|
-
const tokens = name.replace(/([A-Z])/g, " $1").trim().toLowerCase().split(/[\s_-]+/).filter(Boolean);
|
|
1476
|
-
const skip = /* @__PURE__ */ new Set([
|
|
1477
|
-
"get",
|
|
1478
|
-
"set",
|
|
1479
|
-
"create",
|
|
1480
|
-
"update",
|
|
1481
|
-
"delete",
|
|
1482
|
-
"fetch",
|
|
1483
|
-
"save",
|
|
1484
|
-
"load",
|
|
1485
|
-
"parse",
|
|
1486
|
-
"format",
|
|
1487
|
-
"validate",
|
|
1488
|
-
"convert",
|
|
1489
|
-
"transform",
|
|
1490
|
-
"build",
|
|
1491
|
-
"generate",
|
|
1492
|
-
"render",
|
|
1493
|
-
"send",
|
|
1494
|
-
"receive",
|
|
1495
|
-
"find",
|
|
1496
|
-
"list",
|
|
1497
|
-
"add",
|
|
1498
|
-
"remove",
|
|
1499
|
-
"insert",
|
|
1500
|
-
"upsert",
|
|
1501
|
-
"put",
|
|
1502
|
-
"read",
|
|
1503
|
-
"write",
|
|
1504
|
-
"check",
|
|
1505
|
-
"handle",
|
|
1506
|
-
"process",
|
|
1507
|
-
"pending",
|
|
1508
|
-
"active",
|
|
1509
|
-
"current",
|
|
1510
|
-
"new",
|
|
1511
|
-
"old",
|
|
1512
|
-
"all"
|
|
1513
|
-
]);
|
|
1514
|
-
const singularize2 = (w) => w.endsWith("s") && w.length > 3 ? w.slice(0, -1) : w;
|
|
1515
|
-
return new Set(
|
|
1516
|
-
tokens.filter((t) => !skip.has(t) && t.length > 2).map(singularize2)
|
|
1517
|
-
);
|
|
1518
|
-
});
|
|
1519
|
-
if (nounSets.length >= 2 && nounSets.every((s) => s.size > 0)) {
|
|
1520
|
-
const [first, ...rest] = nounSets;
|
|
1521
|
-
const commonNouns = Array.from(first).filter(
|
|
1522
|
-
(n) => rest.every((s) => s.has(n))
|
|
1523
|
-
);
|
|
1524
|
-
if (commonNouns.length > 0) return true;
|
|
1525
|
-
}
|
|
1074
|
+
if (stems.size === 1 || domains.size === 1) return true;
|
|
1526
1075
|
return false;
|
|
1527
1076
|
}
|
|
1528
1077
|
function adjustFragmentationForClassification(baseFragmentation, classification) {
|
|
@@ -1546,6 +1095,80 @@ function adjustFragmentationForClassification(baseFragmentation, classification)
|
|
|
1546
1095
|
return baseFragmentation * 0.7;
|
|
1547
1096
|
}
|
|
1548
1097
|
}
|
|
1098
|
+
|
|
1099
|
+
// src/cluster-detector.ts
|
|
1100
|
+
function detectModuleClusters(graph, options) {
|
|
1101
|
+
const domainMap = /* @__PURE__ */ new Map();
|
|
1102
|
+
for (const [file, node] of graph.nodes.entries()) {
|
|
1103
|
+
const primaryDomain = node.exports[0]?.inferredDomain || "unknown";
|
|
1104
|
+
if (!domainMap.has(primaryDomain)) {
|
|
1105
|
+
domainMap.set(primaryDomain, []);
|
|
1106
|
+
}
|
|
1107
|
+
domainMap.get(primaryDomain).push(file);
|
|
1108
|
+
}
|
|
1109
|
+
const clusters = [];
|
|
1110
|
+
for (const [domain, files] of domainMap.entries()) {
|
|
1111
|
+
if (files.length < 2 || domain === "unknown") continue;
|
|
1112
|
+
const totalTokens = files.reduce((sum, file) => {
|
|
1113
|
+
const node = graph.nodes.get(file);
|
|
1114
|
+
return sum + (node?.tokenCost || 0);
|
|
1115
|
+
}, 0);
|
|
1116
|
+
let sharedImportRatio = 0;
|
|
1117
|
+
if (files.length >= 2) {
|
|
1118
|
+
const allImportSets = files.map(
|
|
1119
|
+
(f) => new Set(graph.nodes.get(f)?.imports || [])
|
|
1120
|
+
);
|
|
1121
|
+
let intersection = new Set(allImportSets[0]);
|
|
1122
|
+
let union = new Set(allImportSets[0]);
|
|
1123
|
+
for (let i = 1; i < allImportSets.length; i++) {
|
|
1124
|
+
const nextSet = allImportSets[i];
|
|
1125
|
+
intersection = new Set([...intersection].filter((x) => nextSet.has(x)));
|
|
1126
|
+
for (const x of nextSet) union.add(x);
|
|
1127
|
+
}
|
|
1128
|
+
sharedImportRatio = union.size > 0 ? intersection.size / union.size : 0;
|
|
1129
|
+
}
|
|
1130
|
+
const fragmentation = calculateFragmentation(files, domain, {
|
|
1131
|
+
...options,
|
|
1132
|
+
sharedImportRatio
|
|
1133
|
+
});
|
|
1134
|
+
let totalCohesion = 0;
|
|
1135
|
+
files.forEach((f) => {
|
|
1136
|
+
const node = graph.nodes.get(f);
|
|
1137
|
+
if (node) totalCohesion += calculateEnhancedCohesion(node.exports);
|
|
1138
|
+
});
|
|
1139
|
+
const avgCohesion = totalCohesion / files.length;
|
|
1140
|
+
clusters.push({
|
|
1141
|
+
domain,
|
|
1142
|
+
files,
|
|
1143
|
+
totalTokens,
|
|
1144
|
+
fragmentationScore: fragmentation,
|
|
1145
|
+
avgCohesion,
|
|
1146
|
+
suggestedStructure: generateSuggestedStructure(
|
|
1147
|
+
files,
|
|
1148
|
+
totalTokens,
|
|
1149
|
+
fragmentation
|
|
1150
|
+
)
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
return clusters;
|
|
1154
|
+
}
|
|
1155
|
+
function generateSuggestedStructure(files, tokens, fragmentation) {
|
|
1156
|
+
const targetFiles = Math.max(1, Math.ceil(tokens / 1e4));
|
|
1157
|
+
const plan = [];
|
|
1158
|
+
if (fragmentation > 0.5) {
|
|
1159
|
+
plan.push(
|
|
1160
|
+
`Consolidate ${files.length} files scattered across multiple directories into ${targetFiles} core module(s)`
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
if (tokens > 2e4) {
|
|
1164
|
+
plan.push(
|
|
1165
|
+
`Domain logic is very large (${Math.round(tokens / 1e3)}k tokens). Ensure clear sub-domain boundaries.`
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
return { targetFiles, consolidationPlan: plan };
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/remediation.ts
|
|
1549
1172
|
function getClassificationRecommendations(classification, file, issues) {
|
|
1550
1173
|
switch (classification) {
|
|
1551
1174
|
case "barrel-export":
|
|
@@ -1604,9 +1227,252 @@ function getClassificationRecommendations(classification, file, issues) {
|
|
|
1604
1227
|
}
|
|
1605
1228
|
}
|
|
1606
1229
|
|
|
1607
|
-
// src/
|
|
1608
|
-
|
|
1609
|
-
|
|
1230
|
+
// src/analyzer.ts
|
|
1231
|
+
function calculateCohesion(exports2, filePath, options) {
|
|
1232
|
+
if (exports2.length <= 1) return 1;
|
|
1233
|
+
if (filePath && isTestFile(filePath)) return 1;
|
|
1234
|
+
const domains = exports2.map((e) => e.inferredDomain || "unknown");
|
|
1235
|
+
const uniqueDomains = new Set(domains.filter((d) => d !== "unknown"));
|
|
1236
|
+
const hasImports = exports2.some((e) => !!e.imports);
|
|
1237
|
+
if (!hasImports && !options?.weights) {
|
|
1238
|
+
if (uniqueDomains.size <= 1) return 1;
|
|
1239
|
+
return 0.4;
|
|
1240
|
+
}
|
|
1241
|
+
return calculateEnhancedCohesion(exports2, filePath, options);
|
|
1242
|
+
}
|
|
1243
|
+
function analyzeIssues(params) {
|
|
1244
|
+
const {
|
|
1245
|
+
file,
|
|
1246
|
+
importDepth,
|
|
1247
|
+
contextBudget,
|
|
1248
|
+
cohesionScore,
|
|
1249
|
+
fragmentationScore,
|
|
1250
|
+
maxDepth,
|
|
1251
|
+
maxContextBudget,
|
|
1252
|
+
minCohesion,
|
|
1253
|
+
maxFragmentation,
|
|
1254
|
+
circularDeps
|
|
1255
|
+
} = params;
|
|
1256
|
+
const issues = [];
|
|
1257
|
+
const recommendations = [];
|
|
1258
|
+
let severity = "info";
|
|
1259
|
+
let potentialSavings = 0;
|
|
1260
|
+
if (circularDeps.length > 0) {
|
|
1261
|
+
severity = "critical";
|
|
1262
|
+
issues.push(`Part of ${circularDeps.length} circular dependency chain(s)`);
|
|
1263
|
+
recommendations.push(
|
|
1264
|
+
"Break circular dependencies by extracting interfaces or using dependency injection"
|
|
1265
|
+
);
|
|
1266
|
+
potentialSavings += contextBudget * 0.2;
|
|
1267
|
+
}
|
|
1268
|
+
if (importDepth > maxDepth * 1.5) {
|
|
1269
|
+
severity = "critical";
|
|
1270
|
+
issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
|
|
1271
|
+
recommendations.push("Flatten dependency tree or use facade pattern");
|
|
1272
|
+
potentialSavings += contextBudget * 0.3;
|
|
1273
|
+
} else if (importDepth > maxDepth) {
|
|
1274
|
+
if (severity !== "critical") severity = "major";
|
|
1275
|
+
issues.push(
|
|
1276
|
+
`Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`
|
|
1277
|
+
);
|
|
1278
|
+
recommendations.push("Consider reducing dependency depth");
|
|
1279
|
+
potentialSavings += contextBudget * 0.15;
|
|
1280
|
+
}
|
|
1281
|
+
if (contextBudget > maxContextBudget * 1.5) {
|
|
1282
|
+
severity = "critical";
|
|
1283
|
+
issues.push(
|
|
1284
|
+
`Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
|
|
1285
|
+
);
|
|
1286
|
+
recommendations.push(
|
|
1287
|
+
"Split into smaller modules or reduce dependency tree"
|
|
1288
|
+
);
|
|
1289
|
+
potentialSavings += contextBudget * 0.4;
|
|
1290
|
+
} else if (contextBudget > maxContextBudget) {
|
|
1291
|
+
if (severity !== "critical") severity = "major";
|
|
1292
|
+
issues.push(
|
|
1293
|
+
`Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
|
|
1294
|
+
);
|
|
1295
|
+
recommendations.push("Reduce file size or dependencies");
|
|
1296
|
+
potentialSavings += contextBudget * 0.2;
|
|
1297
|
+
}
|
|
1298
|
+
if (cohesionScore < minCohesion * 0.5) {
|
|
1299
|
+
if (severity !== "critical") severity = "major";
|
|
1300
|
+
issues.push(
|
|
1301
|
+
`Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`
|
|
1302
|
+
);
|
|
1303
|
+
recommendations.push(
|
|
1304
|
+
"Split file by domain - separate unrelated functionality"
|
|
1305
|
+
);
|
|
1306
|
+
potentialSavings += contextBudget * 0.25;
|
|
1307
|
+
} else if (cohesionScore < minCohesion) {
|
|
1308
|
+
if (severity === "info") severity = "minor";
|
|
1309
|
+
issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
|
|
1310
|
+
recommendations.push("Consider grouping related exports together");
|
|
1311
|
+
potentialSavings += contextBudget * 0.1;
|
|
1312
|
+
}
|
|
1313
|
+
if (fragmentationScore > maxFragmentation) {
|
|
1314
|
+
if (severity === "info" || severity === "minor") severity = "minor";
|
|
1315
|
+
issues.push(
|
|
1316
|
+
`High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`
|
|
1317
|
+
);
|
|
1318
|
+
recommendations.push("Consolidate with related files in same domain");
|
|
1319
|
+
potentialSavings += contextBudget * 0.3;
|
|
1320
|
+
}
|
|
1321
|
+
if (issues.length === 0) {
|
|
1322
|
+
issues.push("No significant issues detected");
|
|
1323
|
+
recommendations.push("File is well-structured for AI context usage");
|
|
1324
|
+
}
|
|
1325
|
+
if (isBuildArtifact(file)) {
|
|
1326
|
+
issues.push("Detected build artifact (bundled/output file)");
|
|
1327
|
+
recommendations.push("Exclude build outputs from analysis");
|
|
1328
|
+
severity = "info";
|
|
1329
|
+
potentialSavings = 0;
|
|
1330
|
+
}
|
|
1331
|
+
return {
|
|
1332
|
+
severity,
|
|
1333
|
+
issues,
|
|
1334
|
+
recommendations,
|
|
1335
|
+
potentialSavings: Math.floor(potentialSavings)
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
function isBuildArtifact(filePath) {
|
|
1339
|
+
const lower = filePath.toLowerCase();
|
|
1340
|
+
return lower.includes("/node_modules/") || lower.includes("/dist/") || lower.includes("/build/") || lower.includes("/out/") || lower.includes("/.next/");
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// src/scoring.ts
|
|
1344
|
+
var import_core4 = require("@aiready/core");
|
|
1345
|
+
|
|
1346
|
+
// src/defaults.ts
|
|
1347
|
+
var import_core5 = require("@aiready/core");
|
|
1348
|
+
|
|
1349
|
+
// src/summary.ts
|
|
1350
|
+
function generateSummary(results) {
|
|
1351
|
+
if (results.length === 0) {
|
|
1352
|
+
return {
|
|
1353
|
+
totalFiles: 0,
|
|
1354
|
+
totalTokens: 0,
|
|
1355
|
+
avgContextBudget: 0,
|
|
1356
|
+
maxContextBudget: 0,
|
|
1357
|
+
avgImportDepth: 0,
|
|
1358
|
+
maxImportDepth: 0,
|
|
1359
|
+
deepFiles: [],
|
|
1360
|
+
avgFragmentation: 0,
|
|
1361
|
+
fragmentedModules: [],
|
|
1362
|
+
avgCohesion: 0,
|
|
1363
|
+
lowCohesionFiles: [],
|
|
1364
|
+
criticalIssues: 0,
|
|
1365
|
+
majorIssues: 0,
|
|
1366
|
+
minorIssues: 0,
|
|
1367
|
+
totalPotentialSavings: 0,
|
|
1368
|
+
topExpensiveFiles: []
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
const totalFiles = results.length;
|
|
1372
|
+
const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
|
|
1373
|
+
const totalContextBudget = results.reduce(
|
|
1374
|
+
(sum, r) => sum + r.contextBudget,
|
|
1375
|
+
0
|
|
1376
|
+
);
|
|
1377
|
+
const avgContextBudget = totalContextBudget / totalFiles;
|
|
1378
|
+
const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
|
|
1379
|
+
const avgImportDepth = results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
|
|
1380
|
+
const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
|
|
1381
|
+
const deepFiles = results.filter((r) => r.importDepth >= 5).map((r) => ({ file: r.file, depth: r.importDepth })).sort((a, b) => b.depth - a.depth).slice(0, 10);
|
|
1382
|
+
const avgFragmentation = results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
|
|
1383
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
1384
|
+
for (const result of results) {
|
|
1385
|
+
for (const domain of result.domains) {
|
|
1386
|
+
if (!moduleMap.has(domain)) moduleMap.set(domain, []);
|
|
1387
|
+
moduleMap.get(domain).push(result);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
const fragmentedModules = [];
|
|
1391
|
+
for (const [domain, files] of moduleMap.entries()) {
|
|
1392
|
+
let jaccard2 = function(a, b) {
|
|
1393
|
+
const s1 = new Set(a || []);
|
|
1394
|
+
const s2 = new Set(b || []);
|
|
1395
|
+
if (s1.size === 0 && s2.size === 0) return 0;
|
|
1396
|
+
const inter = new Set([...s1].filter((x) => s2.has(x)));
|
|
1397
|
+
const uni = /* @__PURE__ */ new Set([...s1, ...s2]);
|
|
1398
|
+
return uni.size === 0 ? 0 : inter.size / uni.size;
|
|
1399
|
+
};
|
|
1400
|
+
var jaccard = jaccard2;
|
|
1401
|
+
if (files.length < 2) continue;
|
|
1402
|
+
const fragmentationScore = files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
|
|
1403
|
+
if (fragmentationScore < 0.3) continue;
|
|
1404
|
+
const totalTokens2 = files.reduce((sum, f) => sum + f.tokenCost, 0);
|
|
1405
|
+
const avgCohesion2 = files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
|
|
1406
|
+
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
1407
|
+
const filePaths = files.map((f) => f.file);
|
|
1408
|
+
const pathEntropy = calculatePathEntropy(filePaths);
|
|
1409
|
+
const directoryDistance = calculateDirectoryDistance(filePaths);
|
|
1410
|
+
let importSimTotal = 0;
|
|
1411
|
+
let importPairs = 0;
|
|
1412
|
+
for (let i = 0; i < files.length; i++) {
|
|
1413
|
+
for (let j = i + 1; j < files.length; j++) {
|
|
1414
|
+
importSimTotal += jaccard2(
|
|
1415
|
+
files[i].dependencyList || [],
|
|
1416
|
+
files[j].dependencyList || []
|
|
1417
|
+
);
|
|
1418
|
+
importPairs++;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
const importCohesion = importPairs > 0 ? importSimTotal / importPairs : 0;
|
|
1422
|
+
fragmentedModules.push({
|
|
1423
|
+
domain,
|
|
1424
|
+
files: files.map((f) => f.file),
|
|
1425
|
+
totalTokens: totalTokens2,
|
|
1426
|
+
fragmentationScore,
|
|
1427
|
+
avgCohesion: avgCohesion2,
|
|
1428
|
+
importCohesion,
|
|
1429
|
+
pathEntropy,
|
|
1430
|
+
directoryDistance,
|
|
1431
|
+
suggestedStructure: {
|
|
1432
|
+
targetFiles,
|
|
1433
|
+
consolidationPlan: [
|
|
1434
|
+
`Consolidate ${files.length} files across ${new Set(files.map((f) => f.file.split("/").slice(0, -1).join("/"))).size} directories`,
|
|
1435
|
+
`Target ~${targetFiles} core modules to reduce context switching`
|
|
1436
|
+
]
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
const avgCohesion = results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
|
|
1441
|
+
const lowCohesionFiles = results.filter((r) => r.cohesionScore < 0.4).map((r) => ({ file: r.file, score: r.cohesionScore })).sort((a, b) => a.score - b.score).slice(0, 10);
|
|
1442
|
+
const criticalIssues = results.filter(
|
|
1443
|
+
(r) => r.severity === "critical"
|
|
1444
|
+
).length;
|
|
1445
|
+
const majorIssues = results.filter((r) => r.severity === "major").length;
|
|
1446
|
+
const minorIssues = results.filter((r) => r.severity === "minor").length;
|
|
1447
|
+
const totalPotentialSavings = results.reduce(
|
|
1448
|
+
(sum, r) => sum + r.potentialSavings,
|
|
1449
|
+
0
|
|
1450
|
+
);
|
|
1451
|
+
const topExpensiveFiles = results.sort((a, b) => b.contextBudget - a.contextBudget).slice(0, 10).map((r) => ({
|
|
1452
|
+
file: r.file,
|
|
1453
|
+
contextBudget: r.contextBudget,
|
|
1454
|
+
severity: r.severity
|
|
1455
|
+
}));
|
|
1456
|
+
return {
|
|
1457
|
+
totalFiles,
|
|
1458
|
+
totalTokens,
|
|
1459
|
+
avgContextBudget,
|
|
1460
|
+
maxContextBudget,
|
|
1461
|
+
avgImportDepth,
|
|
1462
|
+
maxImportDepth,
|
|
1463
|
+
deepFiles,
|
|
1464
|
+
avgFragmentation,
|
|
1465
|
+
fragmentedModules,
|
|
1466
|
+
avgCohesion,
|
|
1467
|
+
lowCohesionFiles,
|
|
1468
|
+
criticalIssues,
|
|
1469
|
+
majorIssues,
|
|
1470
|
+
minorIssues,
|
|
1471
|
+
totalPotentialSavings,
|
|
1472
|
+
topExpensiveFiles
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1610
1476
|
// src/index.ts
|
|
1611
1477
|
async function analyzeContext(options) {
|
|
1612
1478
|
const {
|
|
@@ -1618,22 +1484,17 @@ async function analyzeContext(options) {
|
|
|
1618
1484
|
includeNodeModules = false,
|
|
1619
1485
|
...scanOptions
|
|
1620
1486
|
} = options;
|
|
1621
|
-
const files = await (0,
|
|
1487
|
+
const files = await (0, import_core7.scanFiles)({
|
|
1622
1488
|
...scanOptions,
|
|
1623
|
-
// Only add node_modules to exclude if includeNodeModules is false
|
|
1624
|
-
// The DEFAULT_EXCLUDE already includes node_modules, so this is only needed
|
|
1625
|
-
// if user overrides the default exclude list
|
|
1626
1489
|
exclude: includeNodeModules && scanOptions.exclude ? scanOptions.exclude.filter(
|
|
1627
1490
|
(pattern) => pattern !== "**/node_modules/**"
|
|
1628
1491
|
) : scanOptions.exclude
|
|
1629
1492
|
});
|
|
1630
1493
|
const pythonFiles = files.filter((f) => f.toLowerCase().endsWith(".py"));
|
|
1631
|
-
const tsJsFiles = files.filter((f) => !f.toLowerCase().endsWith(".py"));
|
|
1632
|
-
void tsJsFiles;
|
|
1633
1494
|
const fileContents = await Promise.all(
|
|
1634
1495
|
files.map(async (file) => ({
|
|
1635
1496
|
file,
|
|
1636
|
-
content: await (0,
|
|
1497
|
+
content: await (0, import_core7.readFileContent)(file)
|
|
1637
1498
|
}))
|
|
1638
1499
|
);
|
|
1639
1500
|
const graph = buildDependencyGraph(
|
|
@@ -1653,7 +1514,6 @@ async function analyzeContext(options) {
|
|
|
1653
1514
|
contextBudget: metric.contextBudget,
|
|
1654
1515
|
cohesionScore: metric.cohesion,
|
|
1655
1516
|
fragmentationScore: 0,
|
|
1656
|
-
// Python analyzer doesn't calculate fragmentation yet
|
|
1657
1517
|
maxDepth,
|
|
1658
1518
|
maxContextBudget,
|
|
1659
1519
|
minCohesion,
|
|
@@ -1667,7 +1527,6 @@ async function analyzeContext(options) {
|
|
|
1667
1527
|
tokenCost: Math.floor(
|
|
1668
1528
|
metric.contextBudget / (1 + metric.imports.length || 1)
|
|
1669
1529
|
),
|
|
1670
|
-
// Estimate
|
|
1671
1530
|
linesOfCode: metric.metrics.linesOfCode,
|
|
1672
1531
|
importDepth: metric.importDepth,
|
|
1673
1532
|
dependencyCount: metric.imports.length,
|
|
@@ -1679,13 +1538,11 @@ async function analyzeContext(options) {
|
|
|
1679
1538
|
),
|
|
1680
1539
|
cohesionScore: metric.cohesion,
|
|
1681
1540
|
domains: ["python"],
|
|
1682
|
-
// Generic for now
|
|
1683
1541
|
exportCount: metric.exports.length,
|
|
1684
1542
|
contextBudget: metric.contextBudget,
|
|
1685
1543
|
fragmentationScore: 0,
|
|
1686
1544
|
relatedFiles: [],
|
|
1687
1545
|
fileClassification: "unknown",
|
|
1688
|
-
// Python files not yet classified
|
|
1689
1546
|
severity,
|
|
1690
1547
|
issues,
|
|
1691
1548
|
recommendations,
|
|
@@ -1720,7 +1577,7 @@ async function analyzeContext(options) {
|
|
|
1720
1577
|
break;
|
|
1721
1578
|
}
|
|
1722
1579
|
}
|
|
1723
|
-
const {
|
|
1580
|
+
const { issues } = analyzeIssues({
|
|
1724
1581
|
file,
|
|
1725
1582
|
importDepth,
|
|
1726
1583
|
contextBudget,
|
|
@@ -1732,14 +1589,10 @@ async function analyzeContext(options) {
|
|
|
1732
1589
|
maxFragmentation,
|
|
1733
1590
|
circularDeps
|
|
1734
1591
|
});
|
|
1735
|
-
void severity;
|
|
1736
|
-
void issues;
|
|
1737
|
-
void recommendations;
|
|
1738
|
-
void potentialSavings;
|
|
1739
1592
|
const domains = [
|
|
1740
1593
|
...new Set(node.exports.map((e) => e.inferredDomain || "unknown"))
|
|
1741
1594
|
];
|
|
1742
|
-
const fileClassification = classifyFile(node
|
|
1595
|
+
const fileClassification = classifyFile(node);
|
|
1743
1596
|
const adjustedCohesionScore = adjustCohesionForClassification(
|
|
1744
1597
|
cohesionScore,
|
|
1745
1598
|
fileClassification,
|
|
@@ -1764,7 +1617,6 @@ async function analyzeContext(options) {
|
|
|
1764
1617
|
importDepth,
|
|
1765
1618
|
contextBudget,
|
|
1766
1619
|
cohesionScore: adjustedCohesionScore,
|
|
1767
|
-
// Use adjusted cohesion
|
|
1768
1620
|
fragmentationScore: adjustedFragmentationScore,
|
|
1769
1621
|
maxDepth,
|
|
1770
1622
|
maxContextBudget,
|
|
@@ -1781,7 +1633,6 @@ async function analyzeContext(options) {
|
|
|
1781
1633
|
dependencyList,
|
|
1782
1634
|
circularDeps: circularDeps.filter((cycle) => cycle.includes(file)),
|
|
1783
1635
|
cohesionScore: adjustedCohesionScore,
|
|
1784
|
-
// Report adjusted cohesion
|
|
1785
1636
|
domains,
|
|
1786
1637
|
exportCount: node.exports.length,
|
|
1787
1638
|
contextBudget,
|
|
@@ -1798,262 +1649,19 @@ async function analyzeContext(options) {
|
|
|
1798
1649
|
});
|
|
1799
1650
|
}
|
|
1800
1651
|
const allResults = [...results, ...pythonResults];
|
|
1801
|
-
|
|
1652
|
+
return allResults.sort((a, b) => {
|
|
1802
1653
|
const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
|
|
1803
1654
|
const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
|
|
1804
1655
|
if (severityDiff !== 0) return severityDiff;
|
|
1805
1656
|
return b.contextBudget - a.contextBudget;
|
|
1806
1657
|
});
|
|
1807
|
-
return sorted;
|
|
1808
|
-
}
|
|
1809
|
-
function generateSummary(results) {
|
|
1810
|
-
if (results.length === 0) {
|
|
1811
|
-
return {
|
|
1812
|
-
totalFiles: 0,
|
|
1813
|
-
totalTokens: 0,
|
|
1814
|
-
avgContextBudget: 0,
|
|
1815
|
-
maxContextBudget: 0,
|
|
1816
|
-
avgImportDepth: 0,
|
|
1817
|
-
maxImportDepth: 0,
|
|
1818
|
-
deepFiles: [],
|
|
1819
|
-
avgFragmentation: 0,
|
|
1820
|
-
fragmentedModules: [],
|
|
1821
|
-
avgCohesion: 0,
|
|
1822
|
-
lowCohesionFiles: [],
|
|
1823
|
-
criticalIssues: 0,
|
|
1824
|
-
majorIssues: 0,
|
|
1825
|
-
minorIssues: 0,
|
|
1826
|
-
totalPotentialSavings: 0,
|
|
1827
|
-
topExpensiveFiles: []
|
|
1828
|
-
};
|
|
1829
|
-
}
|
|
1830
|
-
const totalFiles = results.length;
|
|
1831
|
-
const totalTokens = results.reduce((sum, r) => sum + r.tokenCost, 0);
|
|
1832
|
-
const totalContextBudget = results.reduce(
|
|
1833
|
-
(sum, r) => sum + r.contextBudget,
|
|
1834
|
-
0
|
|
1835
|
-
);
|
|
1836
|
-
const avgContextBudget = totalContextBudget / totalFiles;
|
|
1837
|
-
const maxContextBudget = Math.max(...results.map((r) => r.contextBudget));
|
|
1838
|
-
const avgImportDepth = results.reduce((sum, r) => sum + r.importDepth, 0) / totalFiles;
|
|
1839
|
-
const maxImportDepth = Math.max(...results.map((r) => r.importDepth));
|
|
1840
|
-
const deepFiles = results.filter((r) => r.importDepth >= 5).map((r) => ({ file: r.file, depth: r.importDepth })).sort((a, b) => b.depth - a.depth).slice(0, 10);
|
|
1841
|
-
const avgFragmentation = results.reduce((sum, r) => sum + r.fragmentationScore, 0) / totalFiles;
|
|
1842
|
-
const moduleMap = /* @__PURE__ */ new Map();
|
|
1843
|
-
for (const result of results) {
|
|
1844
|
-
for (const domain of result.domains) {
|
|
1845
|
-
if (!moduleMap.has(domain)) {
|
|
1846
|
-
moduleMap.set(domain, []);
|
|
1847
|
-
}
|
|
1848
|
-
moduleMap.get(domain).push(result);
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
const fragmentedModules = [];
|
|
1852
|
-
for (const [domain, files] of moduleMap.entries()) {
|
|
1853
|
-
let jaccard2 = function(a, b) {
|
|
1854
|
-
const s1 = new Set(a || []);
|
|
1855
|
-
const s2 = new Set(b || []);
|
|
1856
|
-
if (s1.size === 0 && s2.size === 0) return 0;
|
|
1857
|
-
const inter = new Set([...s1].filter((x) => s2.has(x)));
|
|
1858
|
-
const uni = /* @__PURE__ */ new Set([...s1, ...s2]);
|
|
1859
|
-
return uni.size === 0 ? 0 : inter.size / uni.size;
|
|
1860
|
-
};
|
|
1861
|
-
var jaccard = jaccard2;
|
|
1862
|
-
if (files.length < 2) continue;
|
|
1863
|
-
const fragmentationScore = files.reduce((sum, f) => sum + f.fragmentationScore, 0) / files.length;
|
|
1864
|
-
if (fragmentationScore < 0.3) continue;
|
|
1865
|
-
const totalTokens2 = files.reduce((sum, f) => sum + f.tokenCost, 0);
|
|
1866
|
-
const avgCohesion2 = files.reduce((sum, f) => sum + f.cohesionScore, 0) / files.length;
|
|
1867
|
-
const targetFiles = Math.max(1, Math.ceil(files.length / 3));
|
|
1868
|
-
const filePaths = files.map((f) => f.file);
|
|
1869
|
-
const pathEntropy = calculatePathEntropy(filePaths);
|
|
1870
|
-
const directoryDistance = calculateDirectoryDistance(filePaths);
|
|
1871
|
-
let importSimTotal = 0;
|
|
1872
|
-
let importPairs = 0;
|
|
1873
|
-
for (let i = 0; i < files.length; i++) {
|
|
1874
|
-
for (let j = i + 1; j < files.length; j++) {
|
|
1875
|
-
importSimTotal += jaccard2(
|
|
1876
|
-
files[i].dependencyList || [],
|
|
1877
|
-
files[j].dependencyList || []
|
|
1878
|
-
);
|
|
1879
|
-
importPairs++;
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
const importCohesion = importPairs > 0 ? importSimTotal / importPairs : 0;
|
|
1883
|
-
fragmentedModules.push({
|
|
1884
|
-
domain,
|
|
1885
|
-
files: files.map((f) => f.file),
|
|
1886
|
-
totalTokens: totalTokens2,
|
|
1887
|
-
fragmentationScore,
|
|
1888
|
-
pathEntropy,
|
|
1889
|
-
directoryDistance,
|
|
1890
|
-
importCohesion,
|
|
1891
|
-
avgCohesion: avgCohesion2,
|
|
1892
|
-
suggestedStructure: {
|
|
1893
|
-
targetFiles,
|
|
1894
|
-
consolidationPlan: [
|
|
1895
|
-
`Consolidate ${files.length} ${domain} files into ${targetFiles} cohesive file(s)`,
|
|
1896
|
-
`Current token cost: ${totalTokens2.toLocaleString()}`,
|
|
1897
|
-
`Estimated savings: ${Math.floor(totalTokens2 * 0.3).toLocaleString()} tokens (30%)`
|
|
1898
|
-
]
|
|
1899
|
-
}
|
|
1900
|
-
});
|
|
1901
|
-
}
|
|
1902
|
-
fragmentedModules.sort((a, b) => b.fragmentationScore - a.fragmentationScore);
|
|
1903
|
-
const avgCohesion = results.reduce((sum, r) => sum + r.cohesionScore, 0) / totalFiles;
|
|
1904
|
-
const lowCohesionFiles = results.filter((r) => r.cohesionScore < 0.6).map((r) => ({ file: r.file, score: r.cohesionScore })).sort((a, b) => a.score - b.score).slice(0, 10);
|
|
1905
|
-
const criticalIssues = results.filter(
|
|
1906
|
-
(r) => r.severity === "critical"
|
|
1907
|
-
).length;
|
|
1908
|
-
const majorIssues = results.filter((r) => r.severity === "major").length;
|
|
1909
|
-
const minorIssues = results.filter((r) => r.severity === "minor").length;
|
|
1910
|
-
const totalPotentialSavings = results.reduce(
|
|
1911
|
-
(sum, r) => sum + r.potentialSavings,
|
|
1912
|
-
0
|
|
1913
|
-
);
|
|
1914
|
-
const topExpensiveFiles = results.sort((a, b) => b.contextBudget - a.contextBudget).slice(0, 10).map((r) => ({
|
|
1915
|
-
file: r.file,
|
|
1916
|
-
contextBudget: r.contextBudget,
|
|
1917
|
-
severity: r.severity
|
|
1918
|
-
}));
|
|
1919
|
-
return {
|
|
1920
|
-
totalFiles,
|
|
1921
|
-
totalTokens,
|
|
1922
|
-
avgContextBudget,
|
|
1923
|
-
maxContextBudget,
|
|
1924
|
-
avgImportDepth,
|
|
1925
|
-
maxImportDepth,
|
|
1926
|
-
deepFiles,
|
|
1927
|
-
avgFragmentation,
|
|
1928
|
-
fragmentedModules: fragmentedModules.slice(0, 10),
|
|
1929
|
-
avgCohesion,
|
|
1930
|
-
lowCohesionFiles,
|
|
1931
|
-
criticalIssues,
|
|
1932
|
-
majorIssues,
|
|
1933
|
-
minorIssues,
|
|
1934
|
-
totalPotentialSavings,
|
|
1935
|
-
topExpensiveFiles
|
|
1936
|
-
};
|
|
1937
|
-
}
|
|
1938
|
-
function analyzeIssues(params) {
|
|
1939
|
-
const {
|
|
1940
|
-
file,
|
|
1941
|
-
importDepth,
|
|
1942
|
-
contextBudget,
|
|
1943
|
-
cohesionScore,
|
|
1944
|
-
fragmentationScore,
|
|
1945
|
-
maxDepth,
|
|
1946
|
-
maxContextBudget,
|
|
1947
|
-
minCohesion,
|
|
1948
|
-
maxFragmentation,
|
|
1949
|
-
circularDeps
|
|
1950
|
-
} = params;
|
|
1951
|
-
const issues = [];
|
|
1952
|
-
const recommendations = [];
|
|
1953
|
-
let severity = "info";
|
|
1954
|
-
let potentialSavings = 0;
|
|
1955
|
-
if (circularDeps.length > 0) {
|
|
1956
|
-
severity = "critical";
|
|
1957
|
-
issues.push(`Part of ${circularDeps.length} circular dependency chain(s)`);
|
|
1958
|
-
recommendations.push(
|
|
1959
|
-
"Break circular dependencies by extracting interfaces or using dependency injection"
|
|
1960
|
-
);
|
|
1961
|
-
potentialSavings += contextBudget * 0.2;
|
|
1962
|
-
}
|
|
1963
|
-
if (importDepth > maxDepth * 1.5) {
|
|
1964
|
-
severity = severity === "critical" ? "critical" : "critical";
|
|
1965
|
-
issues.push(`Import depth ${importDepth} exceeds limit by 50%`);
|
|
1966
|
-
recommendations.push("Flatten dependency tree or use facade pattern");
|
|
1967
|
-
potentialSavings += contextBudget * 0.3;
|
|
1968
|
-
} else if (importDepth > maxDepth) {
|
|
1969
|
-
severity = severity === "critical" ? "critical" : "major";
|
|
1970
|
-
issues.push(
|
|
1971
|
-
`Import depth ${importDepth} exceeds recommended maximum ${maxDepth}`
|
|
1972
|
-
);
|
|
1973
|
-
recommendations.push("Consider reducing dependency depth");
|
|
1974
|
-
potentialSavings += contextBudget * 0.15;
|
|
1975
|
-
}
|
|
1976
|
-
if (contextBudget > maxContextBudget * 1.5) {
|
|
1977
|
-
severity = severity === "critical" ? "critical" : "critical";
|
|
1978
|
-
issues.push(
|
|
1979
|
-
`Context budget ${contextBudget.toLocaleString()} tokens is 50% over limit`
|
|
1980
|
-
);
|
|
1981
|
-
recommendations.push(
|
|
1982
|
-
"Split into smaller modules or reduce dependency tree"
|
|
1983
|
-
);
|
|
1984
|
-
potentialSavings += contextBudget * 0.4;
|
|
1985
|
-
} else if (contextBudget > maxContextBudget) {
|
|
1986
|
-
severity = severity === "critical" || severity === "major" ? severity : "major";
|
|
1987
|
-
issues.push(
|
|
1988
|
-
`Context budget ${contextBudget.toLocaleString()} exceeds ${maxContextBudget.toLocaleString()}`
|
|
1989
|
-
);
|
|
1990
|
-
recommendations.push("Reduce file size or dependencies");
|
|
1991
|
-
potentialSavings += contextBudget * 0.2;
|
|
1992
|
-
}
|
|
1993
|
-
if (cohesionScore < minCohesion * 0.5) {
|
|
1994
|
-
severity = severity === "critical" ? "critical" : "major";
|
|
1995
|
-
issues.push(
|
|
1996
|
-
`Very low cohesion (${(cohesionScore * 100).toFixed(0)}%) - mixed concerns`
|
|
1997
|
-
);
|
|
1998
|
-
recommendations.push(
|
|
1999
|
-
"Split file by domain - separate unrelated functionality"
|
|
2000
|
-
);
|
|
2001
|
-
potentialSavings += contextBudget * 0.25;
|
|
2002
|
-
} else if (cohesionScore < minCohesion) {
|
|
2003
|
-
severity = severity === "critical" || severity === "major" ? severity : "minor";
|
|
2004
|
-
issues.push(`Low cohesion (${(cohesionScore * 100).toFixed(0)}%)`);
|
|
2005
|
-
recommendations.push("Consider grouping related exports together");
|
|
2006
|
-
potentialSavings += contextBudget * 0.1;
|
|
2007
|
-
}
|
|
2008
|
-
if (fragmentationScore > maxFragmentation) {
|
|
2009
|
-
severity = severity === "critical" || severity === "major" ? severity : "minor";
|
|
2010
|
-
issues.push(
|
|
2011
|
-
`High fragmentation (${(fragmentationScore * 100).toFixed(0)}%) - scattered implementation`
|
|
2012
|
-
);
|
|
2013
|
-
recommendations.push("Consolidate with related files in same domain");
|
|
2014
|
-
potentialSavings += contextBudget * 0.3;
|
|
2015
|
-
}
|
|
2016
|
-
if (issues.length === 0) {
|
|
2017
|
-
issues.push("No significant issues detected");
|
|
2018
|
-
recommendations.push("File is well-structured for AI context usage");
|
|
2019
|
-
}
|
|
2020
|
-
if (isBuildArtifact(file)) {
|
|
2021
|
-
issues.push("Detected build artifact (bundled/output file)");
|
|
2022
|
-
recommendations.push(
|
|
2023
|
-
"Exclude build outputs (e.g., cdk.out, dist, build, .next) from analysis"
|
|
2024
|
-
);
|
|
2025
|
-
severity = downgradeSeverity(severity);
|
|
2026
|
-
potentialSavings = 0;
|
|
2027
|
-
}
|
|
2028
|
-
return {
|
|
2029
|
-
severity,
|
|
2030
|
-
issues,
|
|
2031
|
-
recommendations,
|
|
2032
|
-
potentialSavings: Math.floor(potentialSavings)
|
|
2033
|
-
};
|
|
2034
|
-
}
|
|
2035
|
-
function isBuildArtifact(filePath) {
|
|
2036
|
-
const lower = filePath.toLowerCase();
|
|
2037
|
-
return lower.includes("/node_modules/") || lower.includes("/dist/") || lower.includes("/build/") || lower.includes("/out/") || lower.includes("/output/") || lower.includes("/cdk.out/") || lower.includes("/.next/") || /\/asset\.[^/]+\//.test(lower);
|
|
2038
|
-
}
|
|
2039
|
-
function downgradeSeverity(s) {
|
|
2040
|
-
switch (s) {
|
|
2041
|
-
case "critical":
|
|
2042
|
-
return "minor";
|
|
2043
|
-
case "major":
|
|
2044
|
-
return "minor";
|
|
2045
|
-
case "minor":
|
|
2046
|
-
return "info";
|
|
2047
|
-
default:
|
|
2048
|
-
return "info";
|
|
2049
|
-
}
|
|
2050
1658
|
}
|
|
2051
1659
|
|
|
2052
1660
|
// src/cli.ts
|
|
2053
1661
|
var import_chalk = __toESM(require("chalk"));
|
|
2054
1662
|
var import_fs2 = require("fs");
|
|
2055
1663
|
var import_path2 = require("path");
|
|
2056
|
-
var
|
|
1664
|
+
var import_core8 = require("@aiready/core");
|
|
2057
1665
|
var import_prompts = __toESM(require("prompts"));
|
|
2058
1666
|
var program = new import_commander.Command();
|
|
2059
1667
|
program.name("aiready-context").description("Analyze AI context window cost and code structure").version("0.1.0").addHelpText(
|
|
@@ -2093,7 +1701,7 @@ program.name("aiready-context").description("Analyze AI context window cost and
|
|
|
2093
1701
|
exclude: void 0,
|
|
2094
1702
|
maxResults: 10
|
|
2095
1703
|
};
|
|
2096
|
-
let finalOptions = await (0,
|
|
1704
|
+
let finalOptions = await (0, import_core8.loadMergedConfig)(directory, defaults, {
|
|
2097
1705
|
maxDepth: options.maxDepth ? parseInt(options.maxDepth) : void 0,
|
|
2098
1706
|
maxContextBudget: options.maxContext ? parseInt(options.maxContext) : void 0,
|
|
2099
1707
|
minCohesion: options.minCohesion ? parseFloat(options.minCohesion) : void 0,
|
|
@@ -2108,7 +1716,7 @@ program.name("aiready-context").description("Analyze AI context window cost and
|
|
|
2108
1716
|
finalOptions = await runInteractiveSetup(directory, finalOptions);
|
|
2109
1717
|
}
|
|
2110
1718
|
const results = await analyzeContext(finalOptions);
|
|
2111
|
-
const elapsedTime = (0,
|
|
1719
|
+
const elapsedTime = (0, import_core8.getElapsedTime)(startTime);
|
|
2112
1720
|
const summary = generateSummary(results);
|
|
2113
1721
|
if (options.output === "json") {
|
|
2114
1722
|
const jsonOutput = {
|
|
@@ -2117,12 +1725,12 @@ program.name("aiready-context").description("Analyze AI context window cost and
|
|
|
2117
1725
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2118
1726
|
analysisTime: elapsedTime
|
|
2119
1727
|
};
|
|
2120
|
-
const outputPath = (0,
|
|
1728
|
+
const outputPath = (0, import_core8.resolveOutputPath)(
|
|
2121
1729
|
options.outputFile,
|
|
2122
1730
|
`context-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
|
|
2123
1731
|
directory
|
|
2124
1732
|
);
|
|
2125
|
-
(0,
|
|
1733
|
+
(0, import_core8.handleJSONOutput)(
|
|
2126
1734
|
jsonOutput,
|
|
2127
1735
|
outputPath,
|
|
2128
1736
|
`
|
|
@@ -2132,7 +1740,7 @@ program.name("aiready-context").description("Analyze AI context window cost and
|
|
|
2132
1740
|
}
|
|
2133
1741
|
if (options.output === "html") {
|
|
2134
1742
|
const html = generateHTMLReport(summary, results);
|
|
2135
|
-
const outputPath = (0,
|
|
1743
|
+
const outputPath = (0, import_core8.resolveOutputPath)(
|
|
2136
1744
|
options.outputFile,
|
|
2137
1745
|
`context-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.html`,
|
|
2138
1746
|
directory
|
|
@@ -2154,7 +1762,7 @@ program.name("aiready-context").description("Analyze AI context window cost and
|
|
|
2154
1762
|
);
|
|
2155
1763
|
displayTuningGuidance(results, finalOptions);
|
|
2156
1764
|
} catch (error) {
|
|
2157
|
-
(0,
|
|
1765
|
+
(0, import_core8.handleCLIError)(error, "Analysis");
|
|
2158
1766
|
}
|
|
2159
1767
|
});
|
|
2160
1768
|
program.parse();
|